web-dev-qa-db-ja.com

Webリクエストごとに1つのDbContextがあります。

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

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

372
Andrew

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

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

一時変数としてDbContextを登録するとうまくいくことがありますが、通常はそのような作業単位のインスタンスを特定のスコープ内に1つ持つことを望みます。 Webアプリケーションでは、Web要求の境界でこのような範囲を定義するのが実用的です。したがって、Web要求ごとのライフスタイルです。これにより、オブジェクト全体を同じコンテキスト内で動作させることができます。言い換えれば、それらは同じビジネストランザクション内で動作します。

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

  • すべてのオブジェクトはそれ自身のインスタンスを取得するので、システムの状態を変更するすべてのクラスは_context.SaveChanges()を呼び出す必要があります(そうでなければ変更は失われます)。これはあなたのコードを複雑にし、コードに第二の責任(文脈を制御する責任)を追加することになり、そして Single Responsibility Principle の違反です。
  • 他のクラスのコンテキストインスタンスでは使用できないため、[DbContextによってロードされ保存された]エンティティがそのようなクラスの範囲から外れないようにする必要があります。これらのエンティティが必要な場合は、IDでそれらを再度読み込む必要があるため、これによってコードが非常に複雑になる可能性があります。これもパフォーマンスの問題を引き起こす可能性があります。
  • DbContextIDisposableを実装しているので、おそらくまだ作成したすべてのインスタンスを破棄したいと思うでしょう。これを実行したい場合は、基本的に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を渡す必要があるということです(これはMethod Injectionと呼ばれます)。ある意味では、この解決策は 'スコープ付き'のアプローチと同じですが、今度はスコープはアプリケーションコード自体で制御されます(そしておそらく何度も繰り返されます)。作業単位の作成および廃棄を担当するのはアプリケーションです。 DbContextはディペンデンシーグラフが構築された後に作成されるので、コンストラクタインジェクションは図外であり、あるクラスから他のクラスにコンテキストを渡す必要がある場合は、メソッドインジェクションに従う必要があります。

メソッドインジェクションはそれほど悪くありませんが、ビジネスロジックがより複雑になり、より多くのクラスが関与するようになると、メソッドからメソッドへ、クラスからクラスへと渡さなければならず、コードが非常に複雑になります。過去にこれ)。単純なアプリケーションでは、この方法はうまくいきます。

欠点のために、このファクトリアプローチはより大きなシステムに適用されます。他のアプローチは便利かもしれません、そしてそれはコンテナまたはインフラストラクチャコードに作業単位を管理させるものです/ Composition Root これがあなたの質問がしているスタイルです。

コンテナーやインフラストラクチャーにこれを処理させることで、UoWインスタンスを作成、(オプションで)コミット、破棄しなければならないことによってアプリケーション・コードが汚染されることがなくなります。このアプローチにはいくつかの問題があります。例えば、あなたはそのインスタンスをコミットして破棄しましたか?

作業単位の廃棄は、Web要求の最後に行うことができます。しかし、多くの人が誤ってこれが作業単位をコミットする場所でもあると想定しています。ただし、アプリケーションのその時点では、作業単位を実際にコミットする必要があることを確実に判断することはできません。例えばビジネスレイヤのコードがコールスタックの上位で捕捉された例外をスローした場合は、間違いなくコミットしたくない

本当の解決策は、再びある種のスコープを明示的に管理することですが、今回はComposition Root内で行います。 command/handler pattern の背後にあるすべてのビジネスロジックを抽象化すると、これを可能にする各コマンドハンドラを囲むことができるデコレータを書くことができます。例:

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();
    } 
}

これにより、このインフラストラクチャコードを一度だけ記述するだけで済みます。どのようなソリッドDIコンテナでも、そのようなデコレータをすべてのICommandHandler<T>実装の周りに一貫した方法でラップするように設定できます。

528
Steven

ここでの1つの答えだけでは実際には答えられません。 OPは、シングルトン/アプリケーションごとのDbContext設計については尋ねず、(Web)要求ごとの設計と、どのような潜在的な利点があるのか​​について尋ねました。

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

パフォーマンスが向上する可能性があります

各DbContextインスタンスは、データベースからロードしたすべてのエンティティの第1レベルのキャッシュを管理します。主キーでエンティティをクエリするたびに、DbContextは最初にデータベースからのクエリを実行する前に、その最初のレベルのキャッシュからエンティティを取得しようとします。データクエリパターンによっては、DbContextの第1レベルのキャッシュにより、同じDbContextを複数の連続したビジネストランザクションで再利用すると、データベースクエリの数が減ることがあります。

遅延ロードを有効にします。

サービスが(ビューモデルや他の種類のDTOを返すのではなく)永続エンティティを返し、それらのエンティティで遅延ロードを利用する場合は、それらのエンティティが取得されたDbContextインスタンスの有効期間を超えて延長する必要があります。業務トランザクションの範囲サービスメソッドが、返す前に使用していたDbContextインスタンスを破棄した場合、返されたエンティティのプロパティを遅延ロードしようとしても失敗します(遅延ロードを使用するのが良いアイデアであるかどうかはまったく別の議論です)。ここに)。このWebアプリケーションの例では、レイジーローディングは通常、別のサービスレイヤによって返されるエンティティのコントローラアクションメソッドで使用されます。その場合、これらのエンティティをロードするためにサービスメソッドによって使用されたDbContextインスタンスは、Web要求の間(または少なくともアクションメソッドが完了するまで)存続する必要があります。

短所もあることを覚えておいてください。そのリンクは主題について読むための他の多くのリソースを含んでいます。

他の誰かがこの質問につまずいて、実際に質問に答えていない答えに夢中にならない場合に備えて、これを投稿するだけです。

30
user4893106

Microsoftによる2つの矛盾する推奨があり、多くの人がDbContextをまったく異なる方法で使用しています。

  1. 1つの推奨は"可能な限り早くDbContextを破棄する"ということです。DbContextAliveを持つことはdb接続などの貴重なリソースを占有するからです。
  2. 他のものは要求ごとに1つのDbContextが強く推奨されています

あなたのリクエストがDbのものとは無関係な多くのことをしているならば、あなたのDbContextは理由もなく保持されるので、それらは互いに矛盾します。したがって、リクエストがランダムなものが処理されるのを待っている間、DbContextを有効に保つのは無駄です...

したがって、ルール1に従う多くの人は、"リポジトリパターン"の中にDbContextを持っていて--- データベースクエリごとの新しいインスタンスそうX * DbContextリクエストごと

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

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

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

しかし多くの場合、これは正しくありません。 Logging独立したUnitOfWorkであるため、ポストリクエストロギング用の新しいDbContextを持つことができます非同期スレッド完全に受け入れ可能です。

そのため、DbContextの有効期間はこれら2つのパラメータに制限されていることがわかりました。 nitOfWorkおよびThread

27

DbContextがスレッドセーフではないからだと確信しています。だから物事を共有することは決して良い考えではありません。

22
Ian

質問や議論で実際に扱われていないことの1つは、DbContextが変更をキャンセルできないという事実です。変更を送信することはできますが、変更ツリーをクリアすることはできません。そのため、リクエストごとのコンテキストを使用している場合、何らかの理由で変更を破棄する必要がある場合は不運です。

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

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

13
Rick Strahl

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

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

9
Miroslav Holec

私が気に入っているのは、ORMの意味で、作業単位(ユーザーが表示したとおりのページ送信)を作業単位に合わせることです。

そのため、ページ送信全体をトランザクションにすることができます。これは、それぞれ新しいコンテキストを作成するたびにCRUDメソッドを公開している場合には実行できませんでした。

3
RB.

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

これが問題なのは、シングルトンDbContextが時々爆弾になってデータベース全体をキャッシュする可能性があるためです。

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

2
Dmitry S.

Entity Frameworkで特に注意が必要なもう1つの問題は、新しいエンティティの作成、遅延読み込み、そしてそれらの新しいエンティティの使用(同じコンテキストから)の組み合わせを使用する場合です。 IDbSet.Createを使用しない場合(新しい場合とは対照的に)、そのエンティティに対するレイジーロードは、そのエンティティが作成されたコンテキストから取得されたときには機能しません。例:

 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.
1
Ted Elliott