Webリクエストごとに1つのDbContext ...なぜですか?


398

Entity Frameworkの設定方法を説明する記事をたくさん読んでいます DbContextて、さまざまなDIフレームワークを使用するHTTP Webリクエストごとに1つだけが作成および使用されるます。

そもそもなぜこれが良いアイデアなのでしょうか?このアプローチを使用すると、どのような利点がありますか?これが良い考えとなる特定の状況はありますか?この手法を使用して、DbContextリポジトリメソッドの呼び出しごとにをインスタンス化するときに実行できないことはありますか?


9
中Gueddari mehdi.me/ambient-dbcontext-in-ef6リポジトリメソッド呼び出しアンチパターンごとのコールDbContextインスタンス。引用:「これを行うと、1次キャッシュ、アイデンティティマップ、作業単位、変更追跡機能、遅延読み込み機能など、Etity FrameworkがDbContextを介して提供するほぼすべての機能が失われます。 」DBContextsのライフサイクルを処理するための優れた提案がある優れた記事。間違いなく読む価値があります。
クリストフ

回答:


565

注:この回答はEntity Frameworkについて説明しDbContextていますがDataContext、LINQ to SQL やNHibernateのようなあらゆる種類の作業単位の実装に適用できますISession

まず、Ianのエコーから始めましょうDbContext。アプリケーション全体で1つにすることは悪い考えです。これが意味を持つ唯一の状況は、シングルスレッドアプリケーションと、その単一のアプリケーションインスタンスによってのみ使用されるデータベースがある場合です。DbContextスレッドセーフとしているためではないDbContextキャッシュデータ、それはかなりすぐに古くなります。これにより、複数のユーザー/アプリケーションがそのデータベースで同時に作業する場合に、あらゆる種類の問題が発生します(もちろん、これは非常に一般的です)。しかし、私はあなたがすでにそれを知っていて、なぜDbContextそれを必要とする人にの新しいインスタンス(つまり、一時的なライフスタイルを伴う)を注入しないのかを知りたいと思っています。(シングルDbContext-またはスレッドごとのコンテキストでも-が悪い理由の詳細については、この回答をお読みください)。

まずは、 DbContext一時的なものとしての可能性があることをおきますが、通常は、特定のスコープ内でそのような作業単位の単一のインスタンスが必要です。Webアプリケーションでは、そのようなスコープをWebリクエストの境界に定義することが実際的です。したがって、Webリクエストごとのライフスタイルです。これにより、オブジェクトのセット全体を同じコンテキスト内で動作させることができます。つまり、同じビジネストランザクション内で動作します。

一連の操作を同じコンテキスト内で動作させるという目標がない場合、その場合、一時的なライフスタイルは問題ありませんが、注意すべき点がいくつかあります。

  • すべてのオブジェクトは独自のインスタンスを取得するため、システムの状態を変更するすべてのクラスを呼び出す必要があります_context.SaveChanges()(そうしないと、変更が失われます)。これはコードを複雑にし、コードに2番目の責任(コンテキストを制御する責任)を追加する可能性があり、単一責任の原則に違反します。
  • [によってロードおよび保存されたDbContext] エンティティは、別のクラスのコンテキストインスタンスでは使用できないため、そのようなクラスのスコープを離れないようにする必要があります。これらのエンティティが必要な場合は、IDでエンティティを再度読み込む必要があるため、コードが非常に複雑になる可能性があります。これにより、パフォーマンスの問題が発生する可能性もあります。
  • はをDbContext実装しているのでIDisposable、作成されたすべてのインスタンスを破棄する必要があります。これを行う場合、基本的に2つのオプションがあります。を呼び出した直後に同じメソッドでそれらを破棄する必要がありますcontext.SaveChanges()が、その場合、ビジネスロジックは外部から渡されるオブジェクトの所有権を取得します。2番目のオプションは、作成されたすべてのインスタンスをHttpリクエストの境界で破棄することですが、その場合でも、それらのインスタンスを破棄する必要があるときにコンテナに通知するために、何らかのスコープが必要です。

別のオプションは、まったく注入しない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();
    } 
}

これにより、このインフラストラクチャコードを1回だけ記述する必要があります。ソリッドDIコンテナーを使用すると、このようなデコレーターを構成ICommandHandler<T>して、すべての実装を一貫した方法でラップすることができます。


2
うわー-完全な答えをありがとう。私が2回賛成できれば、そうします。上記では、「...操作全体を同じコンテキスト内で動作させるつもりはありません。その場合、一時的なライフスタイルは問題ありません...」と言います。具体的には、「一時的」とはどういう意味ですか?
アンドリュー

14
@Andrew:「一時的」は依存性注入の概念です。つまり、サービスが一時的であるように構成されている場合、サービスのインスタンスがコンシューマに注入されるたびに作成されます。
スティーブン

1
@ user981375:CRUD操作の場合、ジェネリックCreateCommand<TEnity>とジェネリックを作成できますCreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>(そして、更新と削除についても同じことを行い、単一のGetByIdQuery<TEntity>クエリがありました)。それでも、このモデルがCRUD操作の有用な抽象化なのか、それとも複雑さを追加するだけなのかを自問する必要があります。それでも、このモデルを使用すると、クロスデコレーションの問題を(デコレーターを介して)簡単に追加できる可能性から利益を得る可能性があります。長所と短所を比較検討する必要があります。
Steven

3
+1 実際にこれを読む前に、私がこの答えをすべて書いたと思いますか?BTW IMO最後にDbContextの破棄について議論することが重要だと思います(ただし、コンテナにとらわれないままでいるのは素晴らしいことです)
Ruben Bartelink

1
ただし、装飾クラスにコンテキストを渡さないでTransactionCommandHandlerDecoratorください。装飾クラスは、?たとえば、装飾されたクラスがInsertCommandHandlerクラスの場合、挿入操作をコンテキスト(EFのDbContext)にどのように登録できますか?
Masoud

35

マイクロソフトによる2つの矛盾する推奨事項があり、多くの人々は完全に異なる方法でDbContextsを使用します。

  1. 一つの勧告は、にある「すぐにとりうるほどの処分DbContexts」 DbContextアライブを有するDB接続などのような貴重な資源を占有しているので....
  2. もう1つは、リクエストごとに1つのDbContextを強くお勧めします

リクエストがDbに関係のない多くのことを行っている場合、理由もなくDbContextが保持されるため、これらは互いに矛盾します。したがって、リクエストがランダムな処理が完了するのを待っている間だけ、DbContextを有効にしておくのは無駄です。

ルール1に従う多くの人々は、「リポジトリパターン」内にDbContextを持ち、データベースクエリごとに新しいインスタンスを作成するため、X * DbContextリクエストあたり

彼らはデータを取得し、コンテキストをできるだけ早く破棄します。これはによって考慮されるMANY許容練習人々 。これには、最小限の時間でdbリソースを占有するという利点がありますが、EFが提供するすべてのUnitOfWorkおよびCaching candyを明らかに犠牲にします。

DbContextの単一の多目的インスタンスを存続させると、キャッシュの利点が最大になりますが、DbContextはスレッドセーフでなく、各Web要求は独自のスレッドで実行されるため、要求ごとのDbContextは、それを維持できる最長です。

そのため、リクエストごとに1 Dbコンテキストを使用することに関するEFのチーム推奨は、WebアプリケーションではUnitOfWorkが1つのリクエスト内にあり、そのリクエストには1つのスレッドがあるという事実に明らかに基づいています。したがって、リクエストごとに1つのDbContextは、UnitOfWorkおよびCachingの理想的な利点のようです。

しかし、多くの場合、これは真実ではありません。私は別のUnitOfWorkをログに記録することを検討しているため、非同期スレッドでリクエスト後のログ記録に新しいDbContextを使用することは完全に許容可能です。

したがって、最後に、DbContextの存続期間がこれらの2つのパラメーターに制限されることが断られます。UnitOfWorkスレッド


3
公平に言えば、HTTPリクエストはかなり速く(数ミリ秒)完了するはずです。それより長くなる場合は、外部ジョブスケジューラなどでバックグラウンド処理を行い、リクエストがすぐに返されるようにすることを検討してください。そうは言っても、あなたのアーキテクチャは実際にはHTTPに依存すべきではありません。全体的に、しかし良い答えです。
クラッシュ

34

ここでは単一の答えではなく、実際に質問に答えます。OPは、シングルトン/アプリケーションごとのDbContext設計については尋ねず、(Web)リクエストごとの設計と、存在する可能性のある潜在的な利点について尋ねました。

Mehdiは素晴らしいリソースなので、私はhttp://mehdi.me/ambient-dbcontext-in-ef6/を参照します。

可能なパフォーマンスの向上。

各DbContextインスタンスは、データベースからロードするすべてのエンティティの第1レベルのキャッシュを維持します。主キーでエンティティにクエリを実行するときは常に、DbContextは、デフォルトでデータベースからクエリする前に、最初のレベルのキャッシュからエンティティを取得しようとします。データクエリのパターンによっては、複数の連続するビジネストランザクションで同じDbContextを再利用すると、DbContextの1次レベルキャッシュのおかげで、データベースクエリが少なくなる場合があります。

遅延読み込みを有効にします。

(ビューモデルやその他の種類のDTOを返すのではなく)サービスが永続エンティティを返し、それらのエンティティのレイジーロードを利用したい場合、それらのエンティティが取得されたDbContextインスタンスのライフタイムは、ビジネストランザクションの範囲。サービスメソッドが返す前に使用したDbContextインスタンスを破棄した場合、返されたエンティティのプロパティをレイジーロードする試みは失敗します(レイジーロードを使用するかどうかは別の議論であり、ここでは取り上げません。ここに)。Webアプリケーションの例では、通常、レイジーロードは、別のサービスレイヤーから返されるエンティティのコントローラーアクションメソッドで使用されます。その場合、

短所もあることに注意してください。そのリンクには、主題について読むための他の多くのリソースが含まれています。

他の誰かがこの質問に遭遇し、実際に質問に対応していない回答に夢中にならない場合に備えて、これを投稿するだけです。


良いリンク!DBContextを明示的に管理することは、最も安全なアプローチのように見えます。
aggsol

22

これは、DbContextがスレッドセーフではないためだと思います。したがって、物事を共有することは決して良い考えではありません。


HTTPリクエスト間で共有することは決して良い考えではないということですか?
アンドリュー

2
はい、アンドリューは彼が意味したものです。コンテキストの共有は、シングルスレッドのデスクトップアプリ専用です。
エリザベス

10
1つのリクエストのコンテキストを共有するのはどうですか。したがって、1つのリクエストで、異なるリポジトリにアクセスし、1つの同じコンテキストを共有することで、それらのリポジトリ間でトランザクションを実行できますか?
Lyubomir Velchev 2015

16

質問またはディスカッションで実際に扱われていないことの1つは、DbContextが変更をキャンセルできないことです。変更を送信することはできますが、変更ツリーをクリアすることはできないため、要求ごとのコンテキストを使用する場合、何らかの理由で変更を破棄する必要がある場合は運が悪かったです。

個人的には、必要に応じてDbContextのインスタンスを作成します。通常、必要に応じてコンテキストを再作成する機能を持つビジネスコンポーネントにアタッチされます。そうすれば、単一のインスタンスを強制的に実行させるのではなく、プロセスを制御できます。また、実際に使用されるかどうかに関係なく、コントローラーの起動ごとにDbContextを作成する必要はありません。その後、リクエストごとのインスタンスが必要な場合は、CTORで(DI経由または手動で)作成するか、必要に応じて各コントローラーメソッドで作成できます。個人的に私は通常、後者のアプローチをとって、実際に必要でないときにDbContextインスタンスを作成しないようにします。

それはあなたがそれをどの角度から見るかにもよる。私にとってリクエストごとのインスタンスは意味がありません。DbContextは本当にHttpリクエストに属していますか?行動の点では、それは間違った場所です。ビジネスコンポーネントは、Httpリクエストではなく、コンテキストを作成する必要があります。その後、必要に応じてビジネスコンポーネントを作成または破棄でき、コンテキストの存続期間について心配する必要はありません。


1
これは興味深い答えであり、私は部分的にあなたに同意します。私にとって、DbContextはWebリクエストに関連付ける必要はありませんが、「ビジネストランザクション」のように、常に1つの「リクエスト」に入力されます。また、コンテキストをビジネストランザクションに関連付けると、変更のキャンセルは非常に奇妙になります。ただし、Webリクエストの境界にない場合でも、ビジネスコンポーネント(BC)がコンテキストを作成する必要があるという意味ではありません。それは彼らの責任ではないと私は思います。代わりに、BCの周りにデコレータを使用してスコープを適用できます。このようにして、コードを変更せずにスコープを変更することもできます。
Steven

1
その場合、ビジネスオブジェクトへの注入は、ライフタイム管理を処理する必要があります。私の見解では、ビジネスオブジェクトはコンテキストを所有しているため、存続期間を制御する必要があります。
Rick Strahl 2016年

簡単に言えば、「必要に応じてコンテキストを再作成する機能」とはどういう意味ですか?あなた自身のロールバック能力をロールしていますか?少し詳しく説明できますか?
tntwyckoff 2016

2
個人的には、最初にDbContextを強制するのは少し厄介だと思います。データベースにアクセスする必要さえあるという保証はありません。多分あなたはその側で状態を変えるサードパーティのサービスを呼んでいるでしょう。または、実際に同時に作業しているデータベースが2つまたは3つある場合もあります。DbContextsを使用してしまう場合に備えて、最初に多数のDbContextsを作成することはありません。ビジネスは自分が使用しているデータを知っているので、それに属しています。必要な場合は、TransactionScopeを最初に配置するだけです。すべての呼び出しに1つ必要なわけではありません。リソースが必要です。
Daniel Lorenz

それは、コンテナがdbcontextの存続期間を制御できるようにするかどうかの問題です。dbcontextは次に、親コントロールの存続期間を制御します。コントローラーに単純なサービスシングルトンを注入したい場合、リクエストごとのセマンティックにより、コンストラクターインジェクトを使用できません。
davidcarr

10

私は以前の意見に同意します。シングルスレッドアプリでDbContextを共有する場合は、より多くのメモリが必要になると言うのは良いことです。たとえば、AzureのWebアプリケーション(1つの余分な小さなインスタンス)にはさらに150 MBのメモリが必要で、1時間あたり約30人のユーザーがいます。 HTTPリクエストでDBContextを共有するアプリケーション

これが実際のサンプル画像です:アプリケーションは12PMにデプロイされています


おそらく、1つの要求のコンテキストを共有するという考えです。さまざまなリポジトリと-DBSetクラスにアクセスし、それらの操作をトランザクションにしたい場合は、優れたソリューションとなるはずです。オープンソースプロジェクトmvcforum.comを見てください。これは、Unit Of Workデザインパターンの実装で行われたと思います。
Lyubomir Velchev 2015

3

私がそれについて気に入っているのは、ORMの意味で、作業単位(ユーザーが見たとおり、つまりページ送信)を作業単位に揃えることです。

したがって、ページ送信全体をトランザクション化することができます。これは、新しいコンテキストを作成するたびにCRUDメソッドを公開する場合は不可能でした。


3

シングルスレッドのシングルユーザーアプリケーションでさえ、シングルトンDbContextを使用しないもう1つの控えめな理由は、使用するIDマップパターンが原因です。つまり、クエリまたはIDを使用してデータを取得するたびに、取得したエンティティインスタンスがキャッシュに保持されます。次回同じエンティティを取得するときに、キャッシュされたエンティティのインスタンスが利用可能であれば、同じセッションで行った変更が反映されます。これは、SaveChangesメソッドが同じデータベースレコードの複数の異なるエンティティインスタンスで終了しないようにするために必要です。そうでなければ、コンテキストは何らかの形でそれらすべてのエンティティインスタンスからのデータをマージする必要があります。

問題である理由は、シングルトンのDbContextが時々爆弾になり、最終的にデータベース全体と.NETオブジェクトのオーバーヘッドをメモリにキャッシュする可能性があるためです。

.NoTracking()拡張メソッドでLinqクエリのみを使用することにより、この動作を回避する方法があります。また、最近のPCには大量のRAMが搭載されています。しかし、通常、それは望ましい動作ではありません。


これは正しいですが、ガベージコレクタが機能し、この問題を実際よりも仮想的にすることを前提とする必要があります。
tocqueville 2016

3
ガベージコレクターは、アクティブな静的/シングルトンオブジェクトによって保持されているオブジェクトインスタンスを収集しません。それらは、ヒープの第2世代になります。
ドミトリーS.

1

Entity Frameworkで特に注意すべきもう1つの問題は、新しいエンティティの作成、遅延読み込み、およびそれらの新しいエンティティ(同じコンテキストから)の組み合わせを使用する場合です。IDbSet.Create(vs new)を使用しない場合、そのエンティティが作成されたコンテキストから取得されたときに、そのエンティティの遅延読み込みは機能しません。例:

 public class Foo {
     public string Id {get; set; }
     public string BarId {get; set; }
     // lazy loaded relationship to bar
     public virtual Bar Bar { get; set;}
 }
 var foo = new Foo {
     Id = "foo id"
     BarId = "some existing bar id"
 };
 dbContext.Set<Foo>().Add(foo);
 dbContext.SaveChanges();

 // some other code, using the same context
 var foo = dbContext.Set<Foo>().Find("foo id");
 var barProp = foo.Bar.SomeBarProp; // fails with null reference even though we have BarId set.
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.