Почему эта вставка 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
Может также быть полезным