37
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

EF Coreで正しくUPDATEする方法

Last updated at Posted at 2023-05-12

EF CoreのUpdateは結構勘違いされている

海外オフショアの方が作成したソースコードのメンテナンスをしているのですが、EF Coreでのレコード更新処理がとても無駄の多いものになっていました。恐らく、EF Coreに対する根本的な勘違いがあるように思います。

正しい理解の促進のためにこの記事を書きます。

主なポイントは次の2つです。

  • 更新の為にDbContext.Update(entity) を呼び出す必要は(必ずしも)ありません
  • 更新する前にエンティティをDBから取得する必要は(必ずしも)ありません

無駄の多いソースコード

以下のコードは正しく動作しますが、無駄が多く、ある意味間違っています。

// DBからエンティティを取得
var article = await _context.Article.FindAsync(model.Id);

// 入力値をエンティティに反映
article.Title = model.Title;
article.Content = model.Content;

// DBを更新
_context.Update(article);
await _context.SaveChangesAsync();

DbContext.Update(entity)を呼び出す必要は(必ずしも)ありません

上記の場合、DbContext.Update(entiry)の呼び出しは不要です。

// DBからエンティティを取得
var article = await _context.Article.FindAsync(model.Id);

// 入力値をエンティティに反映
article.Title = model.Title;
article.Content = model.Content;

// DBを更新
- _context.Update(article); // 不要
await _context.SaveChangesAsync();

DbContextから取得したエンティティは、最初、「変更されていない」状態になっています。
これは、次のコードで確認することができます。

var article = await _context.Article.FindAsync(model.Id);
var state = _context.Entry(article).State; // state == EntityState.Unchanged (未変更)

この状態でSaveChangesAsync()を呼び出しても、変更を検出できないので、UPDATE文は実行されません。

ここで、Titleプロパティに新しい値を設定します。すると、StateがModified(変更済)に変わります。
同時に、Titleプロパティのプロパティ情報も、変更済としてマークされます。変更されていないContentプロパティは、未変更のままです。

var article = await _context.Article.FindAsync(model.Id);
article.Title = model.Title;

// エンティティ状態が変更済になる
var state = _context.Entry(article).State; // state == EntityState.Modified

// プロパティの変更状態
var isTitleModified = _context.Entry(article).Property(e => e.Title).IsModified; // true
var isContentModified = _context.Entry(article).Property(e => e.Content).IsModified; // false

ここでSaveChangesAsync()を呼び出すと、上記の状態に基づいてTITLEのみをUPDATEするSQLを発行します。

これに対して_context.Update(article)は、全てのプロパティを「変更済」としてマークします。

var article = await _context.Article.FindAsync(model.Id);
article.Title = model.Title;

// 全てのプロパティを変更済としてマーク
_context.Update(article);

// エンティティ状態が変更済になる
var state = _context.Entry(article).State; // state == EntityState.Modified

// プロパティの変更状態も全て変更済になる
var isTitleModified = _context.Entry(article).Property(e => e.Title).IsModified; // true
var isContentModified = _context.Entry(article).Property(e => e.Content).IsModified; // true

Entity Framework(Core)について良くわかっていない人は、更新したいエンティティについて無条件にこのUpdateメソッドを呼び出しがちなのですが、ここまで読んだ方にはお分かりのように、本来、適切にエンティティのプロパティを設定したのであれば、Updateメソッドの呼び出しは不要です。Updateメソッドを呼び出してしまうと、本来不要なフィールドまで更新するUPDATE文が発行されてしまうからです。

もし、ORマッパーなどが自動的に全てのプロパティを設定してしまう為に全てのプロパティのIsModifiedtrueになってしまう場合には、次のように、対象外としたいプロパティのみ、変更済のマークを外すことができます。

var article = await _context.Article.FindAsync(model.Id);

// 全ての入力値をエンティティに反映
MapModelToEntity(model, article);

// Contentプロパティは更新対象から外す
_context.Entry(article).Property(e => e.Content).IsModified = false;

var state = _context.Entry(article).State; // state == EntityState.Modified
var isTitleModified = _context.Entry(article).Property(e => e.Title).IsModified; // true
var isContentModified = _context.Entry(article).Property(e => e.Content).IsModified; // false

もちろん、意図して「全てのフィールドを更新対象としたい」場合には、Updateメソッドの呼び出しは有効です。

参考記事:
https://learn.microsoft.com/ja-jp/ef/core/change-tracking/entity-entries

更新する前にエンティティをDBから取得する必要は(必ずしも)ありません

これも同様に勘違いされやすい点なのですが、更新対象のレコードのキーを既に知っている場合には、DbContextから取得するのは必須ではありません。

直接エンティティを作成して、更新すればOKです。

もし、articleがDbContextから取得されたものではなく直接生成されたものである場合、StateEntityState.Detachedになっています。

var article = new Article { Id = 123, Title = "New Title" };
var state = _context.Entry(article).State; // state == EntityState.Detached

この、自分で生成したエンティティを使ってTitleプロパティのみを更新したい場合、次のようにします。

var article = new Article { Id = 123, Title = "New Title" };

// エンティティを追跡状態にする
_context.Article.Attach(article);

// Titleプロパティを変更済にマークする
_context.Entry(article).Property(e => e.Title).IsModified = true;

// 更新
await _context.SaveChangesAsync();	// Titleのみを更新するUPDATE文が実行される

IdがDBに存在しない場合には例外が発生しますので、ご注意ください。

追記:更新前にエンティティを読み込む方が適切なケース

上記の件は、「事前にエンティティを読み込むのは必須ではない」という説明であって、一般的には事前に読み込んだ方が適切です。例えば単純なレコード1件分の入力更新画面(マスタメンテ画面)などにおいては、DbContextから取得したエンティティを更新してSaveChangesAsync()、という流れが一般的です。

そうすることで、レコードの存在チェック、レコードのTimeStampの自動更新、楽観的同時実行制御(排他制御)、等のEF Coreが提供する様々な恩恵を受けることができます。特に、いまだに排他制御を自前で書いている人は、一度IsConcurrencyToken()メソッドや[TimeStamp]属性について調べてみると幸せになれるでしょう。

しかし、バッチ処理などで大量のレコードを一括更新する必要があるケースでは、パフォーマンスが優先されることがあります。
その場合には、上記で説明した方法で、レコードの読み込みを行わずに直接UPDATEを行うことができます。

補足:追跡トラッキングとAsNoTracking()について

twitterにて、「AsNoTrackingについての説明も入れて欲しい」という声がありましたので、補足します。

この記事で説明されているような「変更済」だとか「未変更」だとかの状態管理は、EF Coreの「追跡トラッキング」という機能が行っています。

以下のようにDbContextから取得したエンティティは、自動的に追跡トラッキング対象となり、この記事で説明したような変更の検出が行われ、SaveChangesAsync()の対象となります。

var article = await _context.Article.FindAsync(model.Id); // 自動的に追跡トラッキング対象となる

しかし、追跡トラッキングには当然ながら余分なメモリやCPUパワーを消費します。
単純なSELECTの結果を読み込みたいだけで更新が不要であるケースでは、追跡トラッキングは不要なことがほとんどでしょう。

その場合には、次のようにAsNoTracking()を付けることで、追跡トラッキングが行われないエンティティを取得し、メモリやCPUパワーを節約することができます。

var article = await _context.Article.AsNoTracking()	// 追跡トラッキングを行わない
	.FindAsync(model.Id); 

上記の方法で取得したエンティティに対してプロパティを変更しても追跡トラッキングされない為、SaveChangesAsync()の対象にもならない点に注意してください。

37
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?