Entity Framework
EFにおける最適化手法
サーチ…
AsNoTrackingの使用
悪い例:
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location;
上記のコードは、エンティティを変更または追加せずに返すだけなので、コストの追跡は避けることができます。
良い例え:
var location = dbContext.Location.AsNoTracking()
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location;
AsNoTracking()
関数を使用するとき、エンティティがコンテキストによって追跡されないことを明示的にEntity Frameworkに伝えています。これは、データストアから大量のデータを取得する場合に特に便利です。ただし、追跡されていないエンティティに変更を加えたい場合は、 SaveChanges
呼び出す前にそれらを添付することを忘れないでください。
必要なデータのみの読み込み
コードでよく見られる問題の1つは、すべてのデータをロードすることです。これにより、サーバーの負荷が大幅に増加します。
たとえば、「場所」というモデルがあり、その中に10個のフィールドがありますが、すべてのフィールドが同時に必要なわけではありません。そのモデルの 'LocationName'パラメータだけが必要だとしましょう。
悪い例
var location = dbContext.Location.AsNoTracking()
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location.Name;
良い例え
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.Select(l => l.LocationName);
.SingleOrDefault();
return location;
"良い例"のコードは "LocationName"だけを取得し、それ以外は取得しません。
この例では実体化されていないため、 AsNoTracking()
は必要ありません。とにかく追跡するものは何もありません。
匿名型でさらにフィールドを取得する
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.Select(l => new { Name = l.LocationName, Area = l.LocationArea })
.SingleOrDefault();
return location.Name + " has an area of " + location.Area;
前の例と同じように、フィールド 'LocationName'と 'LocationArea'のみがデータベースから取得されます。匿名タイプには、必要な数の値を保持できます。
メモリではなく、可能な場合はデータベース内でクエリを実行します。
テキサスにいくつの郡があるのかを数えたいとします。
var counties = dbContext.States.Single(s => s.Code == "tx").Counties.Count();
クエリーは正しいが、非効率的である。 States.Single(…)
は、データベースから状態をロードします。次に、 Counties
は、すべてのフィールドを持つ254郡すべてを2番目のクエリでロードします。ロードされたCounties
コレクションのメモリ内で.Count()
が実行されます 。
私たちは必要のない多くのデータをロードしましたが、もっとうまくいくことができます:
var counties = dbContext.Counties.Count(c => c.State.Code == "tx");
ここでは、1つのクエリのみを実行します.SQLでは、カウントと結合に変換されます。行、フィールド、およびオブジェクトの作成を保存しました。
コレクションの型: IQueryable<T>
対IEnumerable<T>
を調べることで、クエリの作成場所を簡単に知ることができます。
複数のクエリを非同期で並列実行する
非同期クエリを使用する場合、複数のクエリを同時に実行できますが、同じコンテキストでは実行できません。 1つのクエリの実行時間が10秒の場合、悪い例の時間は20秒になりますが、良い例の時間は10秒になります。
悪い例
IEnumerable<TResult1> result1;
IEnumerable<TResult2> result2;
using(var context = new Context())
{
result1 = await context.Set<TResult1>().ToListAsync().ConfigureAwait(false);
result2 = await context.Set<TResult1>().ToListAsync().ConfigureAwait(false);
}
良い例え
public async Task<IEnumerable<TResult>> GetResult<TResult>()
{
using(var context = new Context())
{
return await context.Set<TResult1>().ToListAsync().ConfigureAwait(false);
}
}
IEnumerable<TResult1> result1;
IEnumerable<TResult2> result2;
var result1Task = GetResult<TResult1>();
var result2Task = GetResult<TResult2>();
await Task.WhenAll(result1Task, result2Task).ConfigureAwait(false);
var result1 = result1Task.Result;
var result2 = result2Task.Result;
変更追跡とプロキシ生成を無効にする
データを取得したいだけで何も変更しない場合は、変更の追跡とプロキシの作成をオフにすることができます。これにより、パフォーマンスが向上し、遅延ロードも防止されます。
悪い例:
using(var context = new Context())
{
return await context.Set<MyEntity>().ToListAsync().ConfigureAwait(false);
}
良い例え:
using(var context = new Context())
{
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ProxyCreationEnabled = false;
return await context.Set<MyEntity>().ToListAsync().ConfigureAwait(false);
}
特に、これらをソリューション全体に設定したい場合は、コンテキストのコンストラクタ内からこれらをオフにすることが一般的です。
public class MyContext : DbContext
{
public MyContext()
: base("MyContext")
{
Configuration.AutoDetectChangesEnabled = false;
Configuration.ProxyCreationEnabled = false;
}
//snip
}
スタブエンティティの操作
Product
とCategory
が多対多の関係にあるとします。
public class Product
{
public Product()
{
Categories = new HashSet<Category>();
}
public int ProductId { get; set; }
public string ProductName { get; set; }
public virtual ICollection<Category> Categories { get; private set; }
}
public class Category
{
public Category()
{
Products = new HashSet<Product>();
}
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
Category
をProduct
に追加する場合は、 Product
を読み込んでそのCategories
にカテゴリを追加する必要があります。例:
悪い例:
var product = db.Products.Find(1);
var category = db.Categories.Find(2);
product.Categories.Add(category);
db.SaveChanges();
( db
はDbContext
サブクラスです)。
これにより、 Product
とCategory
間のジャンクション表に1つのレコードが作成されます。ただし、この表には2つのId
値しか含まれていません。 1つの小さなレコードを作成するために、完全なエンティティを2つロードするのは無駄です。
より効率的な方法は、最小限のデータ(通常はId
値のみ)を含むスタブエンティティ 、つまりエンティティオブジェクトをメモリ内に作成することです。これは次のようになります。
良い例え:
// Create two stub entities
var product = new Product { ProductId = 1 };
var category = new Category { CategoryId = 2 };
// Attach the stub entities to the context
db.Entry(product).State = System.Data.Entity.EntityState.Unchanged;
db.Entry(category).State = System.Data.Entity.EntityState.Unchanged;
product.Categories.Add(category);
db.SaveChanges();
最終的な結果は同じですが、データベースへの2回のラウンドトリップは回避されます。
重複を防ぐ
関連がすでに存在するかどうかを確認したい場合は、安価なクエリで十分です。例えば:
var exists = db.Categories.Any(c => c.Id == 1 && c.Products.Any(p => p.Id == 14));
この場合も、エンティティ全体がメモリにロードされません。これは事実上接合テーブルを照会し、真偽値を返すだけです。