Почему эта вставка EF с IDENTITY_INSERT не работает?

Это запрос:

using (var db = new AppDbContext())
{
    var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
    db.IdentityItems.Add(item);
    db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
    db.SaveChanges();
}

Когда выполнено, Id вставленной записи в новой таблице по-прежнему 1.

NEW: Когда я использую либо транзакцию, либо ответ TGlatzer, я получаю исключение:

Явное значение должно быть указано для столбца идентификаторов в таблице "Элементы", когда для IDENTITY_INSERT задано значение ON или когда пользователь репликации вставляет в столбец идентификаторов NOT FOR REPLICATION.

6 ответов

Решение

Это никогда не должно использоваться в производственном коде, это просто для удовольствия

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

Я думаю, что мы можем достичь этого, перехватив команду sql и изменив текст команды
(вы можете наследовать от DbCommandInterceptor и от ovveride ReaderExecuting)

У меня нет рабочего примера на данный момент, и я должен идти, но я думаю, что это выполнимо

Образец кода

    public class MyDbInterceptor : DbCommandInterceptor
    {
        public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {

            if (is your table)
            {
                command.CommandText = "Set Identoty off ,update insert into ,Set Identity off"
                return;
            }
            base.ReaderExecuting(command, interceptionContext);

        }

    }

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

Согласно этому предыдущему Вопросу, вам нужно начать транзакцию в вашем контексте. После сохранения изменения вы должны также переформулировать столбец Identity Insert и, наконец, вам необходимо зафиксировать транзакцию.

using (var db = new AppDbContext())
using (var transaction = db .Database.BeginTransaction())
{
    var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
    db.IdentityItems.Add(item);
    db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
    db.SaveChanges();
    db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items OFF");
    transaction.Commit();
}

Я не соблюдаю теги вопроса о том, что речь идет о EF6.
Этот ответ будет работать для EF Core

Настоящим виновником здесь является не отсутствие транзакции, а небольшое неудобство, которое Database.ExectueSqlCommand() не будет держать соединение открытым, если оно не было открыто ранее.

using (var db = new AppDbContext())
{
    var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
    db.IdentityItems.Add(item);
    db.Database.OpenConnection();
    db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
    db.SaveChanges();
}

также сделаю, так как SET IDENTITY_INSERT [...] ON/OFF будет привязан к вашей связи.

Чтобы заставить EF писать ID вашей сущности, вы должны сконфигурировать ID как не сгенерированное хранилище, иначе EF никогда не будет включать этот идентификатор в оператор вставки.

Таким образом, вам нужно изменить модель на лету и настроить идентификатор объекта так, как вам нужно.
Проблема в том, что модель кешируется и довольно сложно изменить ее на лету (я вполне уверен, что сделал это, но на самом деле я не могу найти код, возможно, я его выбросил). Самый короткий путь - создать два разных контекста, в которых вы настраиваете свою сущность двумя разными способами, как DatabaseGeneratedOption.None (когда вам нужно написать идентификатор) и как DatabaseGeneratedOption.Identity (когда вам нужен идентификатор нумерации).

У меня была аналогичная проблема. В моем производственном коде сущности полагаются на создание идентификаторов. Но для интеграционного тестирования мне нужно вручную установить некоторые идентификаторы. Там, где мне не нужно их явно устанавливать, я сгенерировал их в своих построителях тестовых данных. Для этого я создалDbContext наследуя тот, что находится в моем производственном коде, и настроил генерацию идентификаторов для каждой сущности следующим образом:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<Entity1>().Property(e => e.Id).ValueGeneratedNever();
    modelBuilder.Entity<Entity2>().Property(e => e.Id).ValueGeneratedNever();
    ...
}

Но этого было недостаточно, и мне пришлось отключить SQL Server. IDENTITY_INSERT. Это работало при вставке данных в одну таблицу. Но когда у вас есть объекты, связанные друг с другом, и вы хотите вставить граф объектов, это не удаетсяDbContext.SaveChanges(). Причина в том, что согласно документации SQL Server у вас может бытьIDENTITY_INSERT ONтолько для одного стола за один сеанс. Мой коллега предложил использоватьDbCommandInterceptorчто похоже на другой ответ на этот вопрос. Я заставил это работатьINSERT INTOтолько но концепция могла быть расширена дальше. В настоящее время он перехватывает и изменяет несколькоINSERT INTO заявления в рамках одного DbCommand.CommandText. Код можно оптимизировать для использования Span.Slice, чтобы избежать слишком большого количества памяти из-за манипуляций со строками, но поскольку я не мог найтиSplitметод Я не тратил на это время. Я использую этоDbCommandInterceptorв любом случае для интеграционного тестирования. Не стесняйтесь использовать его, если сочтете это полезным.

/// <summary>
/// When enabled intercepts each INSERT INTO statement and detects which table is being inserted into, if any.
/// Then adds the "SET IDENTITY_INSERT table ON;" (and same for OFF) statement before (and after) the actual insertion.
/// </summary>
public class IdentityInsertInterceptor : DbCommandInterceptor
{
    public bool IsEnabled { get; set; }

    public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        if (IsEnabled)
        {
            ModifyAllStatements(command);
        }

        return base.ReaderExecuting(command, eventData, result);
    }

    private static void ModifyAllStatements(DbCommand command)
    {
        string[] statements = command.CommandText.Split(';', StringSplitOptions.RemoveEmptyEntries);
        var commandTextBuilder = new StringBuilder(capacity: command.CommandText.Length * 2);

        foreach (string statement in statements)
        {
            string modified = ModifyStatement(statement);
            commandTextBuilder.Append(modified);
        }

        command.CommandText = commandTextBuilder.ToString();
    }

    private static string ModifyStatement(string statement)
    {
        const string insertIntoText = "INSERT INTO [";
        int insertIntoIndex = statement.IndexOf(insertIntoText, StringComparison.InvariantCultureIgnoreCase);
        if (insertIntoIndex < 0)
            return $"{statement};";

        int closingBracketIndex = statement.IndexOf("]", startIndex: insertIntoIndex, StringComparison.InvariantCultureIgnoreCase);
        string tableName = statement.Substring(
            startIndex: insertIntoIndex + insertIntoText.Length,
            length: closingBracketIndex - insertIntoIndex - insertIntoText.Length);

        // we should probably check whether the table is expected - list with allowed/disallowed tables
        string modified = $"SET IDENTITY_INSERT [{tableName}] ON; {statement}; SET IDENTITY_INSERT [{tableName}] OFF;";
        return modified;
    }
}

Ответ работает для Entity Framework 6. Просто используйте IDENTITY_INSERT вне транзакции.

using (var db = new AppDbContext())
{
    db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
    using (var transaction = db .Database.BeginTransaction())
    {
       var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
       db.IdentityItems.Add(item);
       db.SaveChanges();
       transaction.Commit();
    }
    db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items OFF");
}

Даже если вы выключите IDENTITY_INSERT, вы только что сказали SQL, что я отправлю вам Identity, вы не указали инфраструктуре сущностей для отправки Identity на SQL-сервер.

В общем, вы должны создать DbContext, как показано ниже.

// your existing context
public abstract class BaseAppDbContext : DbContext { 


    private readonly bool turnOfIdentity = false;
    protected AppDbContext(bool turnOfIdentity = false){
        this.turnOfIdentity = turnOfIdentity;
    }


    public DbSet<IdentityItem> IdentityItems {get;set;}

    protected override void OnModelCreating(DbModelBuilder modelBuilder){
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<IdentityItem>()
           .HasKey( i=> i.Id )

           // BK added the "Property" line.
           .Property(e => e.Id)
           .HasDatabaseGeneratedOption(
               turnOfIdentity ?
                   DatabaseGeneratedOption.None,
                   DatabaseGeneratedOption.Identity
           );

    }
}

public class IdentityItem{

}


public class AppDbContext: BaseAppDbContext{
    public AppDbContext(): base(false){}
}

public class AppDbContextWithIdentity : BaseAppDbContext{
    public AppDbContext(): base(true){}
}

Теперь используйте это так...

using (var db = new AppDbContextWithIdentity())
{
    using(var tx = db.Database.BeginTransaction()){
       var item = new IdentityItem {Id = 418, Name = "Abrahadabra" };
       db.IdentityItems.Add(item);
       db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items ON;");
       db.SaveChanges();
       db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT Test.Items OFF");
       tx.Commit();
    }
}

У меня была очень похожая проблема.

Решение было что-то вроде:

db.Database.ExecuteSqlCommand("disable trigger all on  myTable ;") 
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT myTable  ON;");
db.SaveChanges();
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT myTable  OFF");
db.Database.ExecuteSqlCommand("enable trigger all on  myTable ;") 

В моем случае сообщение Explicit value must be specified for identity... потому что при вставке вызывается триггер и вставляется что-то еще.

ALTER TABLE myTable NOCHECK CONSTRAINT all

Может также быть полезным

Другие вопросы по тегам