-
Notifications
You must be signed in to change notification settings - Fork 0
MS_EntityFrameworkResearch
- 戻る(Entity Framework)
- Entity Framework の調査
- Entity Framework の懸念
Entity Framework の懸念に対する調査結果。
- ユーザー定義の
DbContextを生成 -
SqlQueryメソッドで最小限のデータを取得 -
DbContextへ格納 -
DbContextのライフタイムの管理
例えば、ProductContext クラスは
-
DbContextクラスから派生する必要がある。 -
DbSetのプロパティを含める必要がある。 -
DbSetプロパティは、コンテキストで指定されたエンティティのコレクションを表す。
public class ProductContext : DbContext
{
public DbSet<Category> Categories { get; set; }- Entity Framework Designer を使用している場合、
コンテキストは、Visual Studio のテンプレートによって自動的に生成される。 - コードファースト テクニックを使用している場合は、手動でコンテキストを作成する。
メモリのデータ保持は、DbContext オブジェクト インスタンスのライフタイムと同じ。
以下のように、using を使うと、ライフタイムは明確になる。
using (ProductContext context = new ProductContext())
{
// Perform data access using the context
}補足(ライフタイムは短く保つ):
DbContextは
変更追跡のために読み込んだエンティティをすべて保持し続けるため、
長寿命にするとメモリを食い、キャッシュが古くなる。
また、スレッド セーフではない。ASP.NET Core では
AddDbContextの既定が Scoped(リクエスト単位)で、
この原則がフレームワーク側で担保されている。
シングルトンにしたりバックグラウンド処理で使い回したりしてはならない
(IDbContextFactory<T>を使って都度生成する)。
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メソッドで複数エンティティ更新のトランザクションを維持する。
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
トランザクションを維持するために、2 つの新しい 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を書かない。
- 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の破棄時に接続を閉じない、という意味である。
Entity Framework で更新処理を実行する場合、
基本的には、SaveChanges メソッドを使用する。
ExecuteSqlCommand メソッドは、
-
コンテキスト内のエンティティの内容に関係なく、
INSERT、UPDATE、DELETE などのクエリを実行できる。 -
このため
ExecuteSqlCommandメソッドの実行後は、
エンティティと DB の内容は一致していない。 -
従って、エンティティと DB の内容を一致させる必要がある場合、
コンテキスト内のエンティティをリフレッシュする必要がある。
((IObjectContextAdapter)context).ObjectContext.Detach(myCategory);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()するのが定石である。
- EDM を複数のエンティティに統合した場合、
単一のSaveChangesメソッドの呼び出しで容易に複数のエンティティの更新が可能。 - EDM を複数のエンティティに分割した場合、
SaveChangesメソッドを複数回呼び出す必要があり、
接続とトランザクションの明示的な管理が必要。
DB に、50~100 のテーブルが含まれている場合、
すべてのエンティティを含む 1 つの大きな EDM を持つことは良い方法ではない。
複数エンティティを 1 つの大きい EDM に集約すると、
下記のようないくつかの問題を起こす。
-
性能の問題
- メタデータ ロード時間の性能
- ビュー生成における性能
-
雑然とする。
- デザイナー
- インテリセンス
- CLR ネームスペース
上記を回避するため、
ベスト・プラクティスは任意の単位に Entity Data モデルを分割する方法である。
補足(最新化:EDMX が無くなっても論点は残る): EF Core には EDMX が無いため
「ビュー生成の性能」という問題は消えたが、
DbContextの粒度という論点は残る。
- 1 つの巨大な
DbContextは、初回のモデル構築(起動時)が遅くなる- 業務境界ごとに
DbContextを分けると、
- モデル構築が軽くなり、責務も明確になる
- ただしトランザクションをまたぐ場合は接続の共有が必要
(前述のUseTransaction/ EF Core ではUseTransaction+ 同一接続)マイクロサービス的に境界を切るなら分割、
単一 DB のモノリスなら 1 つに、というのが実務的な落とし所である。
なお、EF Core 6 以降はコンパイル済みモデル(dotnet ef dbcontext optimize)で
起動時のモデル構築コストを大幅に削減できる。
https://github.com/OpenTouryoProject/SampleProgram/issues/3
補足(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)のような
専用の仕組みが必要になる。
- ef5 Entity Framework cache problems - Bruno Câmara
http://www.bfcamara.com/post/37112709703/ef5-entity-framework-cache-problems
補足(EF の「キャッシュ」は 2 種類): 混同されやすいので整理する。
種類 内容 第 1 レベル キャッシュ(変更追跡) DbContextが読み込んだエンティティを保持する。Findは先にここを見る。無効化できないクエリ プラン キャッシュ LINQ 式 → SQL の変換結果をアプリ内でキャッシュする 参考リンクの「キャッシュの問題」は前者の話で、
「別の処理が DB を更新したのに、DbContextは古い値を返す」という現象。
対策は**DbContextを長生きさせない**ことに尽きる
(読み取り専用ならAsNoTracking()も有効)。
-
poco - Entity Framework 4 Does it make sense to create a single diagram for all entities - Stack Overflow
http://stackoverflow.com/questions/3867479/entity-framework-4-does-it-make-sense-to-create-a-single-diagram-for-all-entiti/3868075 -
Working With Large Models In Entity Framework – ADO.NET Blog
https://learn.microsoft.com/en-us/archive/blogs/adonet/
- Tips to improve Entity Framework Performance
http://www.dotnet-tricks.com/Tutorial/entityframework/J8bO140912-Tips-to-improve-Entity-Framework-Performance.html
補足(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, 性能
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。