Подтвердить что ты не робот

Как я могу заставить сущность framework вставлять столбцы идентификаторов?

Я хочу написать код С# для инициализации моей базы данных некоторыми семенными данными. Очевидно, что это потребует способности устанавливать значения различных столбцов Identity при вставке. Я использую подход, основанный на кодах. По умолчанию DbContext обрабатывает соединение с базой данных, поэтому вы не можете SET IDENTITY_INSERT [dbo].[MyTable] ON. Итак, что я сделал до сих пор, это использовать конструктор DbContext, который позволяет мне указать используемое соединение БД. Затем я установил IDENTITY_INSERT в ON в этом соединении DB, а затем попытаюсь вставить мои записи, используя сущность framework. Вот пример того, что у меня до сих пор:

public class MyUserSeeder : IEntitySeeder {
    public void InitializeEntities(AssessmentSystemContext context, SqlConnection connection) {
        context.MyUsers.Add(new MyUser { MyUserId = 106, ConceptPersonId = 520476, Salutation = "Mrs", Firstname = "Novelette", Surname = "Aldred", Email = null, LoginId = "520476", Password="28c923d21b68fdf129b46de949b9f7e0d03f6ced8e9404066f4f3a75e115147489c9f68195c2128e320ca9018cd711df", IsEnabled = true, SpecialRequirements = null });
        try {
            connection.Open();
            SqlCommand cmd = new SqlCommand("SET IDENTITY_INSERT [dbo].[MyUser] ON", connection);
            int retVal = cmd.ExecuteNonQuery();
            context.SaveChanges();
        }
        finally {
            connection.Close();
        }
    }
}

Так близко, но пока - потому что, хотя cmd.ExecuteNonQuery() отлично работает, когда я запускаю context.SaveChanges(), мне сообщают, что "Явное значение должно быть указано для столбца идентификации в таблице" MyUser ", когда IDENTITY_INSERT установите значение ON или когда пользователь репликации вставлен в столбец идентификации NOT FOR REPLICATION."

Предположительно, поскольку MyUserId (который является столбцом Identity в таблице MyUser) является первичным ключом, инфраструктура сущности не пытается установить его, когда я вызываю context.SaveChanges(), хотя я дал объекту MyUser значение для свойства MyUserId.

Есть ли способ заставить сущность-структуру попробовать и вставить даже значения первичного ключа для объекта? Или, может быть, способ временно маркировать MyUserId как не значение первичного ключа, поэтому EF пытается его вставить?

4b9b3361

Ответ 1

После тщательного рассмотрения, я решил, что отказ структуры фреймов вставки столбцов идентификации - это функция, а не ошибка.:-) Если бы я должен был вставлять все записи в моей базе данных, включая их идентификационные значения, мне также нужно было бы создать сущность для каждой таблицы ссылок, которая автоматически создала для меня структуру сущностей! Это не правильный подход.

Итак, я занимаюсь настройкой классов посева, которые просто используют код С# и создают объекты EF, а затем используйте DbContext для сохранения вновь созданных данных. Требуется немного больше времени, чтобы взять сбрасываемый SQL и превратить его в код С#, но нет (и не должно быть) слишком большого количества данных только для "посевных" данных - это должен быть небольшой объем данных, который является репрезентативным данных, которые будут находиться в живой базе данных, которая может быть быстро добавлена ​​в новую БД для целей отладки/разработки. Это означает, что если я хочу связать сущности вместе, мне нужно делать запросы по тому, что уже было вставлено, или мой код не знал бы их генерируемого значения идентификации, например. Этот вид вещей появится в коде семян после того, как я установил и выполнил context.SaveChanges для MyRoles:

var roleBasic = context.MyRoles.Where(rl => rl.Name == "Basic").First();
var roleAdmin = context.MyRoles.Where(rl => rl.Name == "Admin").First();
var roleContentAuthor = context.MyRoles.Where(rl => rl.Name == "ContentAuthor").First();

MyUser thisUser = context.MyUsers.Add(new MyUser {
    Salutation = "Mrs", Firstname = "Novelette", Surname = "Aldred", Email = null, LoginUsername = "naldred", Password="c1c966821b68fdf129c46de949b9f7e0d03f6cad8ea404066f4f3a75e11514748ac9f68695c2128e520ca0275cd711df", IsEnabled = true, SpecialRequirements = null
});
thisUser.Roles.Add(roleBasic);

Выполнение этого способа также делает его более вероятным. Я обновлю данные о посеве, когда я изменю схему, потому что я, скорее всего, сломаю код семени, когда я его поменю (если я удалю поле или сущность, существующий код посева, который использует это поле/объект, который не сможет скомпилировать). С SQL script для выполнения посева это не так, и SQL script не был бы агрегирован для базы данных.

Итак, я думаю, что если вы пытаетесь установить поля идентификаторов сущностей для выполнения данных посева БД, вы определенно приняли неправильный подход.

Если бы я действительно перетаскивал нагрузку данных с SQL Server на PostgreSQL (полный живой БД, а не только некоторые данные для посева), я мог бы сделать это через EF, но я бы хотел, чтобы два контекста были открыты на в то же время, и напишите некоторый код, чтобы захватить все различные объекты из исходного контекста и поместить их в целевой контекст, а затем сохранить изменения.

Как правило, единственное время, когда ему нужно вставлять значения идентификатора, является копирование из одного БД в другой БД в пределах одной СУБД (SQL Server → SQL Server, PostgreSQL → PostgreSQL и т.д.), а затем вы 'd сделать это в SQL script, а не в коде с кодом EF (SQL script не будет DB-агностиком, но это не обязательно, вы не собираетесь между разными СУБД).

Ответ 2

EF 6, используя статью msdn:

    using (var dataContext = new DataModelContainer())
    using (var transaction = dataContext.Database.BeginTransaction())
    {
      var user = new User()
      {
        ID = id,
        Name = "John"
      };

      dataContext.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] ON");

      dataContext.User.Add(user);
      dataContext.SaveChanges();

      dataContext.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] OFF");

      transaction.Commit();
    }

Обновление: Чтобы избежать ошибки "Явное значение должно быть указано для столбца с идентификатором в таблице" Имя_таблицы ", если IDENTITY_INSERT установлен в ON или когда пользователь репликации вставляется в столбец идентификации NOT FOR REPLICATION", вы должны изменить значение свойства StoreGeneratedPattern столбца идентификатора от Identity до None в дизайнере модели.

Обратите внимание, что изменение StoreGeneratedPattern на None не приведет к вставке объекта без указанного id (обычным способом) с ошибкой "Невозможно вставить явное значение для столбца идентификации в таблице" Имя таблицы ", если для параметра IDENTITY_INSERT установлено значение" ВЫКЛ ".

Ответ 3

Вам не нужно делать какие-либо забавные дела с подключением, вы можете вырезать среднего человека и просто использовать ObjectContext.ExecuteStoreCommand.

Тогда вы можете достичь того, чего хотите:

context.ExecuteStoreCommand("SET IDENTITY_INSERT [dbo].[MyUser] ON");

Я не знаю ни одного встроенного способа сказать EF, чтобы установить идентификационную вставку, хотя.

Это не идеально, но было бы более гибким и менее "взломанным", чем ваш текущий подход.

Update:

Я только понял, что есть вторая часть вашей проблемы. Теперь, когда вы сказали SQL, что хотите вставлять вставки, EF даже не пытается вставить значения для указанной идентификации (почему бы нам это не сказать).

У меня нет опыта с первым подходом к коду, но из некоторых быстрых поисков кажется, что вам нужно сообщить EF, что ваш столбец не должен быть создан из хранилища. Вам нужно будет сделать что-то вроде этого.

Property(obj => obj.MyUserId)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
    .HasColumnName("MyUserId");

Надеюсь, это заставит вас указывать в правильном направлении: -)

Ответ 4

Бит опоздал на вечеринку, но в случае, если кто-то встречает эту проблему в EF5 с БД первым: я не мог заставить любое решение работать, но нашел другое обходное решение:

Перед запуском команды .SaveChanges() я reset счетчик идентификаторов таблицы:

Entities.Database.ExecuteSqlCommand(String.Format("DBCC CHECKIDENT ([TableNameHere], RESEED, {0})", newObject.Id-1););
Entities.YourTable.Add(newObject);
Entities.SaveChanges();

Это означает, что .SaveChanges() необходимо применять после каждого добавления - но по крайней мере он работает!

Ответ 5

Вот решение проблемы. Я попробовал это на EF6, и это сработало для меня. Ниже приведен некоторый псевдокод, который должен работать.

Прежде всего вам нужно создать перегрузку по умолчанию dbcontext. Если вы проверите базовый класс, вы найдете тот, у кого есть существующее dbConnection. Проверьте следующий код -

public MyDbContext(DbConnection existingConnection, bool contextOwnsConnection)
        : base(existingConnection, contextOwnsConnection = true)
    {
        //optional
        this.Configuration.ProxyCreationEnabled = true;
        this.Configuration.LazyLoadingEnabled = true;
        this.Database.CommandTimeout = 360;
    }

И в режиме создания модели remove db сгенерированный параметр, например,

protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyTable>()
            .Property(a => a.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

        base.OnModelCreating(modelBuilder);
    }

Теперь в коде нужно явно передать объект подключения,

using (var connection = new System.Data.SqlClient.SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionStringName"].ConnectionString))
        {
            connection.Open();
            using (var context = new MyDbContext(connection, true))
            {
                context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[MyTable] ON");
                context.MyTable.AddRange(objectList);
                context.SaveChanges();
                context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[MyTable] OFF");
            }

            connection.Close();
        }

Ответ 6

Эта идея работает только надежно, если целевая таблица пуста или записи вставляются с идентификаторами выше всех уже существующих идентификаторов в таблице!

3 года, и я столкнулся с аналогичной проблемой, передающей производственные данные в тестовую систему. Пользователи хотели иметь возможность копировать производственные данные в тестовую систему, когда захотят, поэтому вместо того, чтобы настраивать задание на передачу в SQL Server, я искал способ выполнить передачу в приложении с использованием существующих классов EF. Таким образом, я мог бы предоставить пользователям пункт меню, чтобы начать передачу, когда захочет.

Приложение использует базу данных MS SQL Server 2008 и EF 6. Поскольку две базы данных, как правило, имеют одинаковую структуру, я думал, что могу легко переносить данные из одного экземпляра DbContext в другой, читая записи каждого объекта с помощью AsNoTracking() и просто Add() (или AddRange()) записи в соответствующее свойство на целевом экземпляре DbContext.

Вот DbContext с одним сущностью, чтобы проиллюстрировать:

public class MyDataContext: DbContext
{
    public virtual DbSet<Person> People { get; set; }
}

Чтобы скопировать данные People, я сделал следующее:

private void CopyPeople()
{
    var records = _sourceContext.People.AsNoTracking().ToArray();
    _targetContext.People.AddRange(records);
    _targetContext.SaveChanges();
}

Пока таблицы были скопированы в правильном порядке (чтобы избежать проблем с ограничениями внешнего ключа), это работало очень хорошо. К сожалению, таблицы, использующие столбцы идентификаторов, делали немного сложнее, поскольку EF игнорировал значения id и просто позволял SQL Server вставлять следующее значение идентификации. Для таблиц с идентификационными столбцами я закончил делать следующее:

  • Прочитать все записи данного объекта
  • Заказать записи по идентификатору в порядке возрастания
  • установите для семантику идентификатора значение первого id
  • отслеживание следующего значения идентификации, добавьте записи один за другим. Если идентификатор не совпадает с ожидаемым следующим значением идентификации, установите для семени идентификатора следующее требуемое значение

Пока таблица пуста (или все новые записи имеют идентификаторы, превышающие текущий текущий идентификатор), и идентификаторы находятся в порядке возрастания, EF и MS SQL вставляют требуемые идентификаторы, и ни одна система не будет жаловаться.

Вот несколько примеров для иллюстрации:

private void InsertRecords(Person[] people)
{
    // setup expected id - presumption: empty table therefore 1
    int expectedId = 1;

    // now add all people in order of ascending id
    foreach(var person in people.OrderBy(p => p.PersonId))
    {
        // if the current person doesn't have the expected next id
        // we need to reseed the identity column of the table
        if (person.PersonId != expectedId)
        {
            // we need to save changes before changing the seed value
            _targetContext.SaveChanges();

            // change identity seed: set to one less than id
            //(SQL Server increments current value and inserts that)
            _targetContext.Database.ExecuteSqlCommand(
                String.Format("DBCC CHECKIDENT([Person], RESEED, {0}", person.PersonId - 1)
            );

            // update the expected id to the new value
            expectedId = person.PersonId;
        }

        // now add the person
        _targetContext.People.Add(person);

        // bump up the expectedId to the next value
        // Assumption: increment interval is 1
        expectedId++;
    }

    // now save any pending changes
    _targetContext.SaveChanges();
}

Используя отражение, я смог написать метод Load и Save, который работал для всех объектов в DbContext.

Это немного взломанный, но он позволяет мне использовать стандартные методы EF для чтения и записи сущностей и преодолевает проблему того, как устанавливать столбцы идентификаторов для конкретных значений в соответствии с набором данных обстоятельств.

Я надеюсь, что это поможет кому-то другому, столкнувшись с аналогичной проблемой.

Ответ 7

После экспериментов с несколькими вариантами, найденными на этом сайте, для меня работал следующий код (EF 6). Обратите внимание, что он сначала пытается нормальное обновление, если элемент уже существует. Если это не так, попробуйте обычную вставку, если ошибка возникла из-за IDENTITY_INSERT, затем попытается обходной путь. Также обратите внимание, что db.SaveChanges не сработает, следовательно, инструкция db.Database.Connection.Open() и дополнительный этап проверки. Помните, что это не обновление контекста, но в моем случае это не обязательно. Надеюсь, это поможет!

public static bool UpdateLeadTime(int ltId, int ltDays)
{
    try
    {
        using (var db = new LeadTimeContext())
        {
            var result = db.LeadTimes.SingleOrDefault(l => l.LeadTimeId == ltId);

            if (result != null)
            {
                result.LeadTimeDays = ltDays;
                db.SaveChanges();
                logger.Info("Updated ltId: {0} with ltDays: {1}.", ltId, ltDays);
            }
            else
            {
                LeadTime leadtime = new LeadTime();
                leadtime.LeadTimeId = ltId;
                leadtime.LeadTimeDays = ltDays;

                try
                {
                    db.LeadTimes.Add(leadtime);
                    db.SaveChanges();
                    logger.Info("Inserted ltId: {0} with ltDays: {1}.", ltId, ltDays);
                }
                catch (Exception ex)
                {
                    logger.Warn("Error captured in UpdateLeadTime({0},{1}) was caught: {2}.", ltId, ltDays, ex.Message);
                    logger.Warn("Inner exception message: {0}", ex.InnerException.InnerException.Message);
                    if (ex.InnerException.InnerException.Message.Contains("IDENTITY_INSERT"))
                    {
                        logger.Warn("Attempting workaround...");
                        try
                        {
                            db.Database.Connection.Open();  // required to update database without db.SaveChanges()
                            db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT[dbo].[LeadTime] ON");
                            db.Database.ExecuteSqlCommand(
                                String.Format("INSERT INTO[dbo].[LeadTime]([LeadTimeId],[LeadTimeDays]) VALUES({0},{1})", ltId, ltDays)
                                );
                            db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT[dbo].[LeadTime] OFF");
                            logger.Info("Inserted ltId: {0} with ltDays: {1}.", ltId, ltDays);
                            // No need to save changes, the database has been updated.
                            //db.SaveChanges(); <-- causes error

                        }
                        catch (Exception ex1)
                        {
                            logger.Warn("Error captured in UpdateLeadTime({0},{1}) was caught: {2}.", ltId, ltDays, ex1.Message);
                            logger.Warn("Inner exception message: {0}", ex1.InnerException.InnerException.Message);
                        }
                        finally
                        {
                            db.Database.Connection.Close();
                            //Verification
                            if (ReadLeadTime(ltId) == ltDays)
                            {
                                logger.Info("Insertion verified. Workaround succeeded.");
                            }
                            else
                            {
                                logger.Info("Error!: Insert not verified. Workaround failed.");
                            }
                        }
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        logger.Warn("Error in UpdateLeadTime({0},{1}) was caught: {2}.", ltId.ToString(), ltDays.ToString(), ex.Message);
        logger.Warn("Inner exception message: {0}", ex.InnerException.InnerException.Message);
        Console.WriteLine(ex.Message);
        return false;
    }
    return true;
}

Ответ 8

Я просто администратор базы данных, но всякий раз, когда что-то подобное появляется, я считаю его запахом кода. То есть, почему у вас есть что-то, что зависит от определенных строк, имеющих определенные значения идентичности? То есть, в приведенном выше примере, почему миссис Новелетт нуждается в значении 106? Вместо того, чтобы полагаться на то, что всегда так, вы можете получить ее ценность и использовать ее везде, где бы вы не были жестко закодированы 106. Немного более громоздкий, но более гибкий (по моему мнению).

Ответ 9

Есть ли способ заставить сущность framework попробовать и вставить даже значения первичного ключа для объекта?

Да, но не так чисто, как хотелось бы.

Предполагая, что вы используете автоматически сгенерированный идентификационный ключ, EF полностью игнорирует вашу попытку сохранить значение ключа. По-видимому, это "по дизайну" по многим веским причинам, описанным выше, но еще есть моменты, когда вы хотите полностью контролировать свои данные семян (или исходную нагрузку). Я предлагаю, чтобы EF разместил этот вид посева в будущей версии. Но пока они это делают, просто напишите небольшой код, который работает в рамках и автоматизирует беспорядочные детали.

Eventho VendorID игнорируется EF, вы можете использовать его с базовым циклом и подсчетом, чтобы определить, сколько записей о держателях места добавить между вашими живыми записями. Держателям мест присваивается следующий доступный идентификационный номер при их добавлении. Когда ваши живые записи имеют запрошенные идентификаторы, вам просто нужно удалить мусор.

public class NewsprintInitializer: DropCreateDatabaseIfModelChanges<NewsprintContext>
{
    protected override void Seed(NewsprintContext context)
    {
        var vendorSeed = new List<Vendor>
        {
            new Vendor { VendorID = 1, Name = "#1 Papier Masson / James McClaren" },
            new Vendor { VendorID = 5, Name = "#5 Abitibi-Price" },
            new Vendor { VendorID = 6, Name = "#6 Kruger Inc." },
            new Vendor { VendorID = 8, Name = "#8 Tembec" }
        };

        //  Add desired records AND Junk records for gaps in the IDs, because .VendorID is ignored on .Add
        int idx = 1;
        foreach (Vendor currentVendor in vendorSeed)
        {
            while (idx < currentVendor.VendorID)
            {
                context.Vendors.Add(new Vendor { Name = "**Junk**" });
                context.SaveChanges();
                idx++;
            }
            context.Vendors.Add(currentVendor);
            context.SaveChanges();
            idx++;
        }
        //  Cleanup (Query/Find and Remove/delete) the Junk records
        foreach (Vendor del in context.Vendors.Where(v => v.Name == "**Junk**"))
        {
            context.Vendors.Remove(del);
        }
        context.SaveChanges();

        // setup for other classes

    }
}

Он работал, как ожидалось, за исключением того, что мне приходилось часто делать "SaveChanges", чтобы сохранить идентификаторы в порядке.

Ответ 10

Я не мог найти способ вставки записей в таблицу. В принципе, я создал SQL script с чем-то вроде этого...

            sb.Append("SET IDENTITY_INSERT [dbo].[tblCustomer] ON;");

            foreach(...)
            {
                var insert = string.Format("INSERT INTO [dbo].[tblCustomer]
                     ([ID],[GivenName],[FamilyName],[NINumber],[CustomerIdent],
                      [InputterID],[CompanyId],[Discriminator]) 
                      VALUES({0}, '{1}', '{2}', '{3}', '{4}', 2, 2, 'tblCustomer'); ", 
                          customerId, firstName, surname, nINumber, Guid.NewGuid());

            sb.Append(insert);
                ...
            }

            sb.Append("SET IDENTITY_INSERT [dbo].[tblCustomer] OFF;");
            using (var sqlConnection = new SqlConnection(connectionString))
            {
                var svrConnection = new ServerConnection(sqlConnection);
                var server = new Server(svrConnection);
                server.ConnectionContext.ExecuteNonQuery(sb.ToString());
        }

Я использую EF 6.