DbSet.Attach(entity)とDbContext.Entry(entity).State = EntityState.Modified


115

切り離されたシナリオで、クライアントにdtoを取得し、エンティティにマップして保存すると、次のようになります。

context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();

その時は何ですか DbSet.Attach(entity)

または、EntityState.Modifiedが既にエンティティをアタッチしているときに.Attachメソッドを使用する必要があるのはなぜですか?


バージョン情報を追加することをお勧めします。これが新しい質問に値するかどうかはわかりません。
Henk Holterman、2015年

回答:


278

するとcontext.Entry(entity).State = EntityState.Modified;、エンティティをにアタッチするだけでなく、エンティティDbContext全体をダーティとしてマークすることになります。つまり、実行するとcontext.SaveChanges()、EFはエンティティのすべてのフィールドを更新する更新ステートメントを生成します。

これは常に望ましいとは限りません。

一方、DbSet.Attach(entity)エンティティをダーティとしてマークせずにコンテキストにアタッチします。することと同じですcontext.Entry(entity).State = EntityState.Unchanged;

この方法でアタッチする場合、エンティティのプロパティの更新を続行しない限り、次にを呼び出したときにcontext.SaveChanges()、EFはこのエンティティのデータベース更新を生成しません。

エンティティの更新を計画している場合でも、エンティティに多くのプロパティ(db列)があり、いくつかのみを更新する場合は、を実行してDbSet.Attach(entity)から、いくつかのプロパティのみを更新する方が有利な場合があります。更新する必要があります。この方法で行うと、EFからより効率的な更新ステートメントが生成されます。EFは変更されたプロパティのみを更新します(対照的にcontext.Entry(entity).State = EntityState.Modified;、すべてのプロパティ/列が更新されます)。

関連ドキュメント:Add / Attach and Entity States

コード例

次のエンティティがあるとします。

public class Person
{
    public int Id { get; set; } // primary key
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

コードが次のようになっている場合:

context.Entry(personEntity).State = EntityState.Modified;
context.SaveChanges();

生成されるSQLは次のようになります。

UPDATE person
SET FirstName = 'whatever first name is',
    LastName = 'whatever last name is'
WHERE Id = 123; -- whatever Id is.

上記のupdateステートメントが、実際に値を変更したかどうかに関係なく、すべての列を更新する方法に注意してください。

対照的に、コードが「通常の」アタッチを次のように使用する場合:

context.People.Attach(personEntity); // State = Unchanged
personEntity.FirstName = "John"; // State = Modified, and only the FirstName property is dirty.
context.SaveChanges();

次に、生成される更新ステートメントは異なります。

UPDATE person
SET FirstName = 'John'
WHERE Id = 123; -- whatever Id is.

ご覧のとおり、更新ステートメントは、エンティティをコンテキストにアタッチした後に実際に変更された値のみを更新します。テーブルの構造によっては、これがパフォーマンスにプラスの影響を与える可能性があります。

ここで、どちらのオプションが適しているかは、実行しようとしていることに完全に依存します。


1
EFは、この方法でWHERE句を生成しません。newで作成されたエンティティ(つまり、新しいEntity())をアタッチし、それを変更に設定した場合、楽観的ロックのため、すべての元のフィールドを設定する必要があります。UPDATEクエリで生成されるWHERE句には通常、元のフィールド(IDだけでなく)がすべて含まれているため、そうしないと、EFが同時実行例外をスローします。
bubi 2015年

3
@budi:ご意見ありがとうございます。確認のために再テストしました。基本エンティティの場合はWHERE、主キーのみを含む句を使用して、同時実行性チェックを行わずに、前述のように動作します。同時実行チェックを行うには、列を同時実行トークンまたはrowVersionとして明示的に構成する必要があります。その場合、WHERE句には、すべてのフィールドではなく、主キーと同時実行トークン列のみが含まれます。あなたのテストがそうでなければ、私はそれについて聞きたいです。
スタン、2015年

魔女のプロパティが変更されていることを動的に見つけるにはどうすればよいですか?
Navid_pdp11

2
@ Navid_pdp11 DbContext.Entry(person).CurrentValuesおよびDbContext.Entry(person).OriginalValues
Shimmy Weitzhandler

少し外れているかもしれませんが、リポジトリパターンを使用する場合は、すべてのモデルに新しいレコードをdbに挿入するときに追跡されていない状態にする必要があるエンティティがあるため、すべてのモデルのリポジトリを作成する必要があります。挿入時にエンティティをコンテキストに添付する汎用リポジトリ。これをどのように最もうまく処理しますか?
jayasurya_j

3

このDbSet.Updateメソッドを使用すると、Entity FrameworkはエンティティのすべてのプロパティをとしてマークしEntityState.Modified、追跡します。すべてではなく一部のプロパティのみを変更する場合は、を使用しますDbSet.Attach。このメソッドはすべてのEntityState.Unchangedプロパティを作成するため、更新するプロパティを作成する必要がありますEntityState.Modified。したがって、アプリがにヒットするとDbContext.SaveChanges、変更されたプロパティのみを操作します。


0

さらに(マークされた回答に加えて)と(EF Coreでは)の間に重要な違いcontext.Entry(entity).State = EntityState.Unchangedありcontext.Attach(entity)ます。

私は自分でそれをより理解するためにいくつかのテストを行ったので(これにはいくつかの一般的な参照テストも含まれています)、これが私のテストシナリオです。

  • EF Core 3.1.3を使用しました
  • 使った QueryTrackingBehavior.NoTracking
  • マッピングには属性のみを使用しました(以下を参照)
  • 異なるコンテキストを使用して注文を取得し、注文を更新しました
  • すべてのテストでdb全体を消去しました

これらはモデルです:

public class Order
{
    public int Id { get; set; }
    public string Comment { get; set; }
    public string ShippingAddress { get; set; }
    public DateTime? OrderDate { get; set; }
    public List<OrderPos> OrderPositions { get; set; }
    [ForeignKey("OrderedByUserId")]
    public User OrderedByUser { get; set; }
    public int? OrderedByUserId { get; set; }
}

public class OrderPos
{
    public int Id { get; set; }
    public string ArticleNo { get; set; }
    public int Quantity { get; set; }
    [ForeignKey("OrderId")]
    public Order Order { get; set; }
    public int? OrderId { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

これは、データベース内の(元の)テストデータです。 ここに画像の説明を入力してください

注文を取得するには:

order = db.Orders.Include(o => o.OrderPositions).Include(o => o.OrderedByUser).FirstOrDefault();

今テスト:

EntityStateを使用した単純な更新:

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1

アタッチを使用した単純な更新:

db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1

EntityStateを使用して子IDを変更して更新します。

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.Id = 3; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1

Attachで子IDを変更して更新します。

db.Attach(order);
order.ShippingAddress = "Germany"; // would be UPDATED
order.OrderedByUser.Id = 3; // will throw EXCEPTION
order.OrderedByUser.FirstName = "William (CHANGED)"; // would be UPDATED
order.OrderPositions[0].Id = 3; // will throw EXCEPTION
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // would be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // would be INSERTED
db.SaveChanges();
// Throws Exception: The property 'Id' on entity type 'User' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.)

注:これは、Idが変更されたか、元の値に設定されたかに関係なく、例外をスローします。Idの状態が「変更済み」に設定されているようで、これは許可されていません(主キーであるため)

子IDを新しいものに変更して更新します(EntityStateとAttachの間に違いはありません):

db.Attach(order); // or db.Entry(order).State = EntityState.Unchanged;
order.OrderedByUser = new User();
order.OrderedByUser.Id = 3; // // Reference will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on User 3)
db.SaveChanges();
// Will generate SQL in 2 Calls:
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 3

注:新規(上記)なしのEntityStateを使用した更新との違いを参照してください。今回は、新しいUserインスタンスのため、名前が更新されます。

EntityStateで Reference-Idを変更して更新します。

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.Id = 2; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1

Attachで Reference-Idを変更して更新します。

db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on FIRST User!)
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1

注意:参照は、ユーザー3に変更されますが、また、利用者1が更新されます、私はので、これがあると思いますorder.OrderedByUser.Id(それはまだ1です)変更されません。

結論 EntityStateを使用すると、より詳細に制御できますが、サブプロパティ(第2レベル)を自分で更新する必要があります。Attachを使用すると、すべてを更新できます(すべてのレベルのプロパティを使用していると思います)が、参照を監視する必要があります。例:User(OrderedByUser)がdropDownの場合、dropDownを介して値を変更すると、Userオブジェクト全体が上書きされる可能性があります。この場合、参照ではなく元のdropDown-Valueが上書きされます。

私にとって最良のケースは、OrderedByUserなどのオブジェクトをnullに設定し、(EntityStateまたはAttachに関係なく)参照のみを変更する場合にのみ、order.OrderedByUserIdを新しい値に設定することです。

これが役に立てば幸い、私はそれがたくさんのテキストであることを知っています:D

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.