DbContext
私は、さまざまな DI フレームワークを使用して、HTTP Web 要求ごとに Entity Framework が 1 つだけ作成され使用されるように設定する方法を説明する記事をたくさん読んできました。
そもそもなぜこれが良いアイデアなのでしょうか? このアプローチを使用することでどのような利点が得られますか? これが良いアイデアとなる特定の状況はありますか? リポジトリ メソッド呼び出しごとにインスタンス化するときにはできないが、この手法を使用するとできることはありますかDbContext
?
ベストアンサー1
注: この回答は Entity Framework の について説明していますが、LINQ to SQL のや NHibernate の
DbContext
など、あらゆる種類の Unit of Work 実装に適用できます。DataContext
ISession
まず、Ian の意見に同調しましょう。アプリケーション全体に単一の を使用するDbContext
のは悪い考えです。これが意味をなすのは、シングル スレッド アプリケーションと、その単一のアプリケーション インスタンスによってのみ使用されるデータベースがある場合だけです。 はDbContext
スレッド セーフではなく、 はDbContext
データをキャッシュするため、すぐに古くなります。複数のユーザー/アプリケーションがそのデータベースを同時に操作すると (もちろん、これは非常によくあることです)、さまざまな問題が発生します。しかし、あなたはすでにそれを知っていると思います。そして、 の新しいインスタンス (つまり、一時的なライフ スタイルを持つ) を、それを必要とする人に挿入しない理由を知りたいだけでしょう。(単一の(またはスレッドごとに 1 つのコンテキスト) が悪いDbContext
理由の詳細については、以下をお読みください。DbContext
この答え)。
まず、 をDbContext
一時的なものとして登録することは可能ですが、通常は特定のスコープ内でそのような作業単位のインスタンスを 1 つだけ持つ必要があります。Web アプリケーションでは、Web 要求の境界でそのようなスコープを定義することが実用的です。つまり、Web 要求ごとのライフスタイルです。これにより、一連のオブジェクト全体を同じコンテキスト内で動作させることができます。つまり、それらは同じビジネス トランザクション内で動作します。
一連の操作を同じコンテキスト内で実行することを目標としていない場合は、一時的なライフスタイルでも問題ありませんが、注意すべき点がいくつかあります。
- すべてのオブジェクトは独自のインスタンスを取得するため、システムの状態を変更するすべてのクラスはを呼び出す必要があります
_context.SaveChanges()
(そうしないと変更が失われます)。これによりコードが複雑になり、コードに2番目の責任(コンテキストを制御する責任)が追加され、単一責任の原則。 - エンティティ( によってロードおよび保存された)がそのようなクラスのスコープから外れないようにする必要があります。エンティティは
DbContext
別のクラスのコンテキスト インスタンスでは使用できないためです。エンティティが必要になったときに ID で再度ロードする必要があるため、コードが非常に複雑になり、パフォーマンスの問題が発生する可能性もあります。 DbContext
は を実装しているのでIDisposable
、作成されたすべてのインスタンスを破棄する必要があるでしょう。これを実行する場合、基本的に 2 つのオプションがあります。 を呼び出した直後に同じメソッドでそれらを破棄する必要がありますcontext.SaveChanges()
が、その場合、ビジネス ロジックは外部から渡されたオブジェクトの所有権を取得します。2 番目のオプションは、作成されたすべてのインスタンスを HTTP リクエストの境界で破棄することですが、その場合でも、それらのインスタンスをいつ破棄する必要があるかをコンテナーに知らせるための何らかのスコープ設定が必要です。
もう 1 つのオプションは、をまったく注入しないことです。代わりに、新しいインスタンスを作成できる を注入します (以前はこのアプローチを使用していました)。この方法では、ビジネス ロジックがコンテキストを明示的に制御します。次のようになります。DbContext
DbContextFactory
public void SomeOperation()
{
using (var context = this.contextFactory.CreateNew())
{
var entities = this.otherDependency.Operate(
context, "some value");
context.Entities.InsertOnSubmit(entities);
context.SaveChanges();
}
}
これの利点は、 の存続期間をDbContext
明示的に管理し、設定が簡単なことです。また、特定のスコープ内で単一のコンテキストを使用することもできます。これには、単一のビジネス トランザクションでコードを実行したり、同じ から派生したエンティティを渡したりできるなど、明らかな利点がありますDbContext
。
欠点は、メソッドからメソッドへ渡す必要があることですDbContext
(これはメソッド インジェクションと呼ばれます)。このソリューションは、ある意味では「スコープ」アプローチと同じですが、スコープはアプリケーション コード自体で制御されます (何度も繰り返される可能性もあります)。作業単位の作成と破棄はアプリケーションが担当します。DbContext
依存関係グラフが構築された後に が作成されるため、コンストラクター インジェクションは関係なく、あるクラスから別のクラスにコンテキストを渡す必要がある場合は、メソッド インジェクションに従う必要があります。
メソッド インジェクションはそれほど悪くはありませんが、ビジネス ロジックが複雑になり、より多くのクラスが関与するようになると、メソッドからメソッドへ、クラスからクラスへと渡す必要があり、コードが非常に複雑になる可能性があります (過去にこのようなことを経験しました)。ただし、単純なアプリケーションの場合は、このアプローチで十分です。
このファクトリーアプローチには欠点があるため、より大きなシステムでは、コンテナまたはインフラストラクチャコードに任せる別のアプローチが役立つ場合があります。構成ルート作業単位を管理します。これがあなたの質問の対象となるスタイルです。
コンテナやインフラストラクチャにこれを処理させることで、UoW インスタンスの作成、(オプションで) コミット、および破棄によってアプリケーション コードが汚染されることがなくなり、ビジネス ロジックがシンプルでクリーンな状態 (単一の責任のみ) になります。このアプローチにはいくつか問題があります。たとえば、インスタンスをコミットおよび破棄する場所はどこですか?
作業単位の破棄は、Web 要求の最後に行うことができます。しかし、多くの人は、これが作業単位をコミットする場所でもあると誤って想定しています。ただし、アプリケーションのその時点では、作業単位を実際にコミットする必要があるかどうかを確実に判断することはできません。たとえば、ビジネス レイヤー コードがコールスタックの上位でキャッチされた例外をスローした場合、間違いなくコミットする必要はありません。
本当の解決策は、やはり何らかのスコープを明示的に管理することですが、今回はコンポジションルート内で行います。コマンド/ハンドラーパターン、これを可能にする各コマンド ハンドラーをラップできるデコレータを記述できるようになります。例:
class TransactionalCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand>
{
readonly DbContext context;
readonly ICommandHandler<TCommand> decorated;
public TransactionCommandHandlerDecorator(
DbContext context,
ICommandHandler<TCommand> decorated)
{
this.context = context;
this.decorated = decorated;
}
public void Handle(TCommand command)
{
this.decorated.Handle(command);
context.SaveChanges();
}
}
ICommandHandler<T>
これにより、このインフラストラクチャ コードを 1 回だけ記述するだけで済みます。堅牢な DI コンテナーを使用すると、このようなデコレータをすべての実装に一貫した方法でラップするように構成できます。