Skip to content

MS_EntityFrameworkResearch

nishi_74322014 edited this page Aug 18, 2026 · 1 revision

Entity Framework の調査

概要

Entity Framework の懸念に対する調査結果。

Entity Frameworkのメモリ使用量の例

  • ユーザー定義の DbContext を生成
  • SqlQuery メソッドで最小限のデータを取得
  • DbContext へ格納
  • DbContext のライフタイムの管理

ユーザー定義のDbContext

例えば、ProductContext クラスは

  • DbContext クラスから派生する必要がある。
  • DbSet のプロパティを含める必要がある。
  • DbSet プロパティは、コンテキストで指定されたエンティティのコレクションを表す。
public class ProductContext : DbContext
{
  public DbSet<Category> Categories { get; set; }
  • Entity Framework Designer を使用している場合、
    コンテキストは、Visual Studio のテンプレートによって自動的に生成される。
  • コードファースト テクニックを使用している場合は、手動でコンテキストを作成する。

DbContextのライフタイム

メモリのデータ保持は、DbContext オブジェクト インスタンスのライフタイムと同じ。

以下のように、using を使うと、ライフタイムは明確になる。

using (ProductContext context = new ProductContext())
{
  // Perform data access using the context
}

補足(ライフタイムは短く保つ): DbContext
変更追跡のために読み込んだエンティティをすべて保持し続けるため、
長寿命にするとメモリを食い、キャッシュが古くなる
また、スレッド セーフではない

ASP.NET Core では AddDbContext の既定が Scoped(リクエスト単位)で、
この原則がフレームワーク側で担保されている。
シングルトンにしたりバックグラウンド処理で使い回したりしてはならない
IDbContextFactory<T> を使って都度生成する)。

SqlQueryメソッド

using (ProductContext context = new ProductContext())
{
  // Below code is enumerated using ToList() method.
  // Stores all records in “myCategories” object (Memory)
  IList<Category> myCategories =
  context.Database.SqlQuery<Category>("Select * from Category").ToList();
}

補足(最新化:EF Core での生 SQL): EF Core では
Database.SqlQuery<T> の代わりに以下を使う。

用途 EF6 EF Core
エンティティ型を返す Set<T>().SqlQuery(...) Set<T>().FromSql($"...")
任意の型・スカラーを返す Database.SqlQuery<T>(...) Database.SqlQuery<T>($"...")(EF Core 8 以降)
更新系 Database.ExecuteSqlCommand(...) Database.ExecuteSql($"...")

重要: EF Core の FromSql / ExecuteSql
FormattableString を受け取り、補間した値を自動的にパラメータ化する。

// OK: @p0 としてパラメータ化される
var rows = await context.Categories
    .FromSql($"SELECT * FROM Category WHERE Id = {id}")
    .ToListAsync();

文字列を自分で連結して渡す FromSqlRaw / ExecuteSqlRaw
SQL インジェクションの危険があるため、原則使わない。

SaveChangesメソッド使用時のコネクションやトランザクション

  • 各トランザクションのための新しい接続を作成する。
  • SaveChanges メソッドが呼び出されたとき内部でトランザクションを維持する。
  • 一回の SaveChanges メソッドで複数エンティティ更新のトランザクションを維持する。

Entity Framework 5.0およびそれ以前のバージョン

TransactionScope

TransactionScopeを使用してトランザクションを処理する。

using (EntitiesContext context = new EntitiesContext())
{
  using (TransactionScope scope = new TransactionScope())
  {
    //Code here
  }
}

補足: このコードは scope.Complete() の呼び出しが省略されているため、
このままでは必ずロールバックされる(説明用の骨格と読むべき)。
また、TransactionScope の既定の分離レベルは Serializable であり、
明示指定が推奨される。詳細はTransactionScopeを参照。

手動の制御も可能

ASP.NET MVC で DbContext のトランザクションと
独自 SQL 処理のトランザクションを同一のスコープにする
http://blog.makotoishida.com/2012/12/aspnet-mvcdbcontextsql.html

Entity Framework 6.0

トランザクションを維持するために、2 つの新しい API を導入している。

DbContext.Database.BeginTransaction API

  • トランザクションを開始する。
  • トランザクションの分離レベルを指定することができる。
  • いくつかの操作を組み合わせて、同じトランザクション内で結合することができる。
  • したがって、全てのトランザクションをコミットまたはロールバックすることができる。
using (EntitiesContext context = new EntitiesContext())
{
  using (var transaction = context.Database.BeginTransaction())
  {
    try
    {
      EmployeeMaster employee = new EmployeeMaster();

      employee.Code = "A0001";
      employee.Name = "Jignesh Trivedi";
      employee.DepartmentId = 1;

      context.Employees.Add(employee);
      context.SaveChanges();

      DepartmentMaster dept = new DepartmentMaster();

      dept.Code = "DEP0001";
      dept.Name = "Department 1";

      context.Departments.Add(dept);
      context.SaveChanges();

      transaction.Commit();
    }
    catch (Exception ex)
    {
      transaction.Rollback();
    }
  }
}

補足(catch での Rollback は不要): using を抜ける際、
コミットされていないトランザクションは Dispose で自動的に
ロールバックされるため、catch 内の明示的な Rollback() は本来不要である
Dapperの節にある指摘と同じ)。

また、上記のように例外を握りつぶす catch (Exception ex)
呼び出し元がエラーを検知できなくなるため、
実務では再スローするか、そもそも catch を書かない。

DbContext.Database.UseTransaction API

  • Entity Framework 外で明記されたトランザクションを使用する
    DbContext インスタンスを許可する。
  • Entity Framework で、この API を使用し任意の既存トランザクションを使用することができる。
using (SqlConnection con = new SqlConnection("connectionString"))
{
  con.Open();
  using (var transaction = con.BeginTransaction())
  {
    // Do something....

    //Pass this transaction to Entity Framework....
    using (EntitiesContext context = new EntitiesContext(con, false))
    {
      context.Database.UseTransaction(transaction);
      EmployeeMaster employee = new EmployeeMaster();
      employee.Code = "A0001";
      employee.Name = "Jignesh Trivedi";
      employee.DepartmentId = 1;
      context.Employees.Add(employee);

      context.SaveChanges();
    }
  }
}

補足(UseTransaction の価値): これは
EF と Dapper / 生の ADO.NET を同一トランザクションで併用する
ための API である。
接続を共有できるため、MS-DTCへの昇格を伴わずに済む
TransactionScopeの昇格の話を参照)。

ADO.NET vs ORM (Entity Framework, Dapper)で述べた
「EF と Dapper のハイブリッド構成」を成立させる要の機能と言える。
なお、コンストラクタの第 2 引数 false
「接続の所有権を DbContext に渡さない」=
DbContext の破棄時に接続を閉じない、という意味である。

ExecuteSqlCommandメソッドの実行時の挙動

Entity Framework で更新処理を実行する場合、
基本的には、SaveChanges メソッドを使用する。

ExecuteSqlCommand実行後、エンティティとDBは不一致状態に陥る

ExecuteSqlCommand メソッドは、

  • コンテキスト内のエンティティの内容に関係なく、
    INSERT、UPDATE、DELETE などのクエリを実行できる。

  • このため ExecuteSqlCommand メソッドの実行後は、
    エンティティと DB の内容は一致していない。

  • 従って、エンティティと DB の内容を一致させる必要がある場合、
    コンテキスト内のエンティティをリフレッシュする必要がある。

エンティティをリフレッシュする方法(エンティティとDBの同期)

エンティティをデタッチし再度問い合わせる。

((IObjectContextAdapter)context).ObjectContext.Detach(myCategory);

Refreshメソッドでリフレッシュする。

ObjectContext context = ((IObjectContextAdapter)myDbContext).ObjectContext;
// Refresh specific Entity object in the context
context.Refresh(System.Data.Objects.RefreshMode.StoreWins, myCategory);

(OR)

// Refresh All Entities in the context.
var refreshableObjects = myDbContext.ChangeTracker.Entries().Select(c => c.Entity).ToList();
context.Refresh(System.Data.Objects.RefreshMode.StoreWins, refreshableObjects);

補足(最新化:EF Core での同期): EF Core には
ObjectContext / Refresh が存在しない。代わりに、

// 個別のエンティティを DB の値で上書き
await context.Entry(myCategory).ReloadAsync();

// 追跡状態をすべて破棄
context.ChangeTracker.Clear();   // EF Core 5 以降

を使う。

なお、EF Core 7 以降の ExecuteUpdate / ExecuteDelete
同じく変更追跡を経由しないため、本節と同じ不一致が起きる。
「一括更新した後、同じ DbContext で読み直すと古い値が返る」
という事故につながるので、

  • 一括更新は専用の短命な DbContext で行う
  • もしくは実行後に ChangeTracker.Clear() する

のが定石である。

Entity Data Model(以下EDM)のベスト・プラクティス

更新処理

  • EDM を複数のエンティティに統合した場合、
    単一の SaveChanges メソッドの呼び出しで容易に複数のエンティティの更新が可能。
  • EDM を複数のエンティティに分割した場合、
    SaveChanges メソッドを複数回呼び出す必要があり、
    接続とトランザクションの明示的な管理が必要。

EDMのサイズ

DB に、50~100 のテーブルが含まれている場合、
すべてのエンティティを含む 1 つの大きな EDM を持つことは良い方法ではない。

複数エンティティを単一EDMに結合する

複数エンティティを 1 つの大きい EDM に集約すると、
下記のようないくつかの問題を起こす。

  • 性能の問題

    • メタデータ ロード時間の性能
    • ビュー生成における性能
  • 雑然とする。

    • デザイナー
    • インテリセンス
    • CLR ネームスペース

EDMを任意の単位に分割

上記を回避するため、
ベスト・プラクティスは任意の単位に Entity Data モデルを分割する方法である。

補足(最新化:EDMX が無くなっても論点は残る): EF Core には EDMX が無いため
「ビュー生成の性能」という問題は消えたが、
DbContext の粒度という論点は残る。

  • 1 つの巨大な DbContext は、初回のモデル構築(起動時)が遅くなる
  • 業務境界ごとに DbContext を分けると、
    • モデル構築が軽くなり、責務も明確になる
    • ただしトランザクションをまたぐ場合は接続の共有が必要
      (前述の UseTransaction / EF Core では UseTransaction + 同一接続)

マイクロサービス的に境界を切るなら分割、
単一 DB のモノリスなら 1 つに、というのが実務的な落とし所である。
なお、EF Core 6 以降はコンパイル済みモデルdotnet ef dbcontext optimize)で
起動時のモデル構築コストを大幅に削減できる。

動的SQLを実装する方法

https://github.com/OpenTouryoProject/SampleProgram/issues/3

System.Linq.Dynamic

Expression Tree

context.Database.SqlQuery

補足(3つの手段の使い分け): 見出しのみだが、要点を補足する。

手段 内容 向き
System.Linq.Dynamic(現在は System.Linq.Dynamic.Core Where("Name == @0", name) のように文字列で LINQ を書く 検索条件が実行時に決まる画面
式木(Expression Tree)(MS_ExpressionTree.md Expression<Func<T,bool>> を組み立てて合成する 型安全に条件を積み上げたい場合
Database.SqlQuery / FromSql 生 SQL を組み立てる SQL 自体を動的に編集したい場合

現実的には、条件の有無で Where を積み上げるだけで
大半の要件は満たせる(IQueryable は遅延評価なので、
最後に ToListAsync() するまで SQL は発行されない)。

var q = context.Employees.AsQueryable();
if (!string.IsNullOrEmpty(name))  q = q.Where(e => e.Name.Contains(name));
if (deptId is int d)              q = q.Where(e => e.DepartmentId == d);
var rows = await q.ToListAsync();

それを超える「パラメタセットによって SQL 定義そのものを編集する」要件は、
ADO.NET vs ORM (Entity Framework, Dapper)
述べられているとおり ORM では表現しきれず、
動的パラメタライズド・クエリ(OTR_DynamicParameterizedQuery.md)のような
専用の仕組みが必要になる。

参考

キャッシュの問題

補足(EF の「キャッシュ」は 2 種類): 混同されやすいので整理する。

種類 内容
第 1 レベル キャッシュ(変更追跡) DbContext が読み込んだエンティティを保持する。Find は先にここを見る。無効化できない
クエリ プラン キャッシュ LINQ 式 → SQL の変換結果をアプリ内でキャッシュする

参考リンクの「キャッシュの問題」は前者の話で、
「別の処理が DB を更新したのに、DbContext は古い値を返す」という現象。
対策は**DbContext を長生きさせない**ことに尽きる
(読み取り専用なら AsNoTracking() も有効)。

Entity Dataモデルのサイズ

性能

補足(EF Core の性能チェックリスト): 現在押さえるべき項目。

項目 内容
AsNoTracking() 参照専用のクエリでは必ず付ける
射影(Select 必要な列だけ取る。DTO へ直接射影する
Include の設計 N+1 を避ける。ただし過剰な Include は行の重複(カルテシアン爆発)を招く。EF Core 5 以降は AsSplitQuery() で分割できる
一括処理 ExecuteUpdate / ExecuteDelete、大量挿入は SqlBulkCopy
コンパイル済みクエリ EF.CompileAsyncQuery でホット パスの変換コストを削減
コンパイル済みモデル dotnet ef dbcontext optimize で起動時間を短縮
ログ確認 発行 SQL を必ず目視する(SQL Server のログ

Tags: 移行, .NET開発, データアクセス, ADO.NET, Entity Framework, 性能

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally