これはクエリです:
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 ID列に挿入されている場合は、テーブル 'Items'のID列に明示的な値を指定する必要があります。
これは製品コードでは決して使用しないでください、それは楽しみのためだけです
私がまだ受け入れられている回答であることがわかります。これを使用しないでください(この問題を解決するため)、以下の他の回答を確認してください
クレイジーなハックなので、これはお勧めしませんが、とにかく。
SQLコマンドを傍受してコマンドテキストを変更することで、それを実現できると思います
(DbCommandInterceptorから継承してReaderExecutingをオーバーライドできます)
現時点では実用的な例がないので、行かなければなりませんが、それは可能だと思います
サンプルコード
public class MyDbInterceptor : DbCommandInterceptor
{
public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
if (is your table)
{
command.CommandText = "Set Identity off ,update insert into ,Set Identity off"
return;
}
base.ReaderExecuting(command, interceptionContext);
}
}
ORMは素晴らしい抽象概念であり、私はそれらを本当に気に入っていますが、より低い(dbに近い)レベルの操作をサポートするためにORMを「ハッキング」しようとするのは理に適っていないと思います。
私はストアドプロシージャを避けようとしますが、(例外的に言ったように)このケースでは、1つを使用する必要があると思います
この以前の Question によると、コンテキストのトランザクションを開始する必要があります。変更を保存した後、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がinsertステートメントにIDを含めません。
したがって、モデルをその場で変更し、必要に応じてエンティティIDを構成する必要があります。
問題は、モデルがキャッシュされており、その場で変更するのが非常に難しいことです(私はそれを実行したと確信していますが、実際にはコードを見つけることができません。おそらく捨ててしまいました)。 。最短の方法は、DatabaseGeneratedOption.None
(IDを書き込む必要がある場合)とDatabaseGeneratedOption.Identity
(自動採番IDが必要な場合)の2つの異なる方法でエンティティを構成する2つの異なるコンテキストを作成することです。
私は非常に似た問題を抱えていました。
解決策は次のようなものでした:
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
役に立つこともできます
答えは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");
}
同様の問題がありました。私のプロダクションコードでは、エンティティはID生成に依存しています。しかし、統合テストでは、いくつかのIDを手動で設定する必要があります。明示的に設定する必要がない場合は、 テストデータビルダー で生成しました。これを実現するために、本番コードの1つを継承するDbContext
を作成し、各エンティティのID生成を次のように構成しました。
_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のドキュメント のとおり、セッション中に一度に1つのテーブルに対してのみ_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;
}
}
_
IDENTITY_INSERT
をオフにしても、Identityを送信することをSQLに伝えただけで、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();
}
}