良いデザインで。データベースへのアクセスは、別のビジネスロジックレイヤー(asp.net MVCモデル)で処理する必要がありますか、それともIQueryable
sまたはDbContext
オブジェクトをコントローラーに渡しても問題ありませんか?
どうして?それぞれの長所と短所は何ですか?
C#でASP.NETMVCアプリケーションを構築しています。 EntityFramework をORMとして使用します。
このシナリオを少し単純化してみましょう。
ふわふわのかわいい子猫のデータベーステーブルがあります。各子猫には、子猫の画像リンク、子猫のふわふわインデックス、子猫の名前、子猫のIDがあります。これらは、Kitten
と呼ばれるEF生成のPOCOにマップされます。このクラスは、asp.net MVCプロジェクトだけでなく、他のプロジェクトでも使用する可能性があります。
KittenController
があり、/Kittens
で最新のふわふわの子猫をフェッチする必要があります。子猫を選択するロジックが含まれている場合がありますが、ロジックは多すぎません。私はこれを実装する方法について友人と議論してきました、私は側面を開示しません:)
public ActionResult Kittens() // some parameters might be here
{
using(var db = new KittenEntities()){ // db can also be injected,
var result = db.Kittens // this explicit query is here
.Where(kitten=>kitten.fluffiness > 10)
.Select(kitten=>new {
Name=kitten.name,
Url=kitten.imageUrl
}).Take(10);
return Json(result,JsonRequestBehavior.AllowGet);
}
}
public class Kitten{
public string Name {get; set; }
public string Url {get; set; }
private Kitten(){
_fluffiness = fluffinessIndex;
}
public static IEnumerable<Kitten> GetLatestKittens(int fluffinessIndex=10){
using(var db = new KittenEntities()){ //connection can also be injected
return db.Kittens.Where(kitten=>kitten.fluffiness > 10)
.Select(entity=>new Kitten(entity.name,entity.imageUrl))
.Take(10).ToList();
}
} // it's static for simplicity here, in fact it's probably also an object method
// Also, in practice it might be a service in a services directory creating the
// Objects and fetching them from the DB, and just the kitten MVC _type_ here
}
//----Then the controller:
public ActionResult Kittens() // some parameters might be here
{
return Json(Kittens.GetLatestKittens(10),JsonRequestBehavior.AllowGet);
}
注:GetLatestKittens
は、コードの他の場所で使用される可能性は低いですが、使用される可能性があります。静的な構築メソッドの代わりにKitten
のコンストラクターを使用して、Kittensのクラスを変更することができます。基本的にはレイヤーであると想定されていますaboveデータベースエンティティであるため、コントローラーは実際のデータベース、マッパー、またはエンティティフレームワークを認識する必要はありません。
注:もちろん、代替アプローチはvery回答としても評価されます。
説明1:これはnot実際の些細なアプリケーションです。これは、数十のコントローラーと数千行のコードを備えたアプリケーションであり、エンティティはここだけでなく、他の数十のC#プロジェクトでも使用されます。ここでの例は縮小されたテストケースです。
オプション1と2は少し極端で、悪魔と真っ青な海のどちらかを選択するのが好きですが、2つから選択する必要がある場合は、オプション1を選択します。
まず、オプション2は、Entity Frameworkがエンティティへの射影をサポートしていないため(Select(e => new Kitten(...))
であり、射影内のパラメーターでコンストラクターを使用できないため、ランタイム例外をスローします。このコンテキストでは少しペダンティックですが、エンティティに投影してKitten
(またはKitten
sの列挙)を返すことにより、そのアプローチの実際の問題を隠しています。
明らかに、メソッドは、ビューで使用するエンティティの2つのプロパティ(子猫のname
とimageUrl
)を返します。これらはすべてのKitten
プロパティの選択にすぎないため、(半分埋められた)Kitten
エンティティを返すことは適切ではありません。では、このメソッドから実際に返すタイプは何ですか?
object
(または_IEnumerable<object>
_)を返すことができます(これが "object method")に関するコメントを理解する方法です。結果をJson(...)
に渡せば問題ありません。後でJavascriptで処理されます。しかし、コンパイル時の型情報はすべて失われ、object
結果型が他の何かに役立つとは思えません。さて、これは1つのビュー(子猫を一覧表示するビュー)に対する唯一の方法です。次に、1匹の子猫を表示する詳細ビュー、編集ビュー、削除確認ビューがあります。既存のKitten
エンティティの4つのビュー。それぞれに異なるプロパティが必要になる可能性があり、それぞれに個別のメソッドとプロジェクション、および異なるDTOタイプが必要になります。 Dog
エンティティとプロジェクト内の100エンティティについても同じで、おそらく400のメソッドと400の戻り値の型が得られます。
そして、おそらく、この特定のビュー以外の場所で再利用されるものは1つもありません。なぜ、もう一度Take
とname
だけで10匹の子猫をimageUrl
したいのですか? 2番目の子猫のリストビューはありますか?もしそうなら、それには理由があり、クエリは偶然に同じであり、一方がもう一方を変更しても必ずしもそうではありません。そうでない場合、リストビューは適切に「再利用」されず、2回存在するべきではありません。それとも、Excelのエクスポートで使用されているのと同じリストですか?ただし、Excelユーザーは明日1000匹の子猫を飼いたいのに、ビューには10匹しか表示されないはずです。または、ビューには子猫のAge
が明日表示されますが、Excelユーザーは、Excelマクロが正しく実行されないため、子猫を飼いたくないでしょう。もうその変更で。 2つのコードが同一であるという理由だけで、それらが異なるコンテキストにあるか、異なるセマンティクスを持っている場合、それらを共通の再利用可能なコンポーネントに分解する必要はありません。 GetLatestKittensForListView
とGetLatestKittensForExcelExport
のままにしておくことをお勧めします。または、サービス層にそのようなメソッドをまったく含まない方がよいでしょう。
これらの考慮事項に照らして、最初のアプローチが優れている理由の例えとしてピザショップへの遠足:)
「カスタムピザショップBigPizzaへようこそ。ご注文を承りますか?」 「まあ、オリーブのピザが欲しいのですが、上にトマトソース、下にチーズを入れて、花崗岩の平らな岩のように黒く固くなるまでオーブンで90分間焼きます。」 「OK、サー、カスタムピザは私たちの職業です、私たちはそれを作ります。」
レジ係は台所に行きます。 「カウンターにサイコがいます。彼はピザを食べたいと思っています...それは花崗岩の岩です...待ってください...最初に名前を付ける必要があります」と彼は料理人に言います。
「いいえ!」と料理人は叫びます、「二度と!私たちはすでにそれを試したのを知っています」。彼は400ページの紙の束を取ります。「ここに花崗岩の岩 2005年からありますが...オリーブはありませんでしたが、代わりにパプリカがあります...またはここにトップトマト ...しかし、顧客はそれを30分だけ焼きたいと思っていました。」 「多分それをTopTomatoGraniteRockSpecialと呼ぶべきですか?」 「しかし、それは一番下のチーズを考慮に入れていません...」レジ係:「それは特別が表現することになっていることです。」 「しかし、ピザの岩をピラミッドのように形成することも特別なことです」と料理人は答えます。 「うーん...難しい...」と、絶望的なレジ係は言います。
「私のピザはもうオーブンに入っていますか?」と突然、キッチンのドアから叫びました。 「この議論をやめましょう。このピザの作り方を教えてください。二度とそのようなピザを食べるつもりはありません」と料理人は決めます。 「OK、オリーブのピザですが、上にトマトソース、下にチーズを入れて、花崗岩の平らな岩のように黒く固くなるまでオーブンで90分間焼きます。」
オプション1がビューレイヤーでデータベースコンテキストを使用することにより関心の分離の原則に違反する場合、オプション2はサービスまたはビジネスレイヤーにプレゼンテーション中心のクエリロジックを使用することにより同じ原則に違反します。技術的な観点からはそうではありませんが、プレゼンテーション層の外側で「再利用可能」以外のサービス層になってしまいます。また、コントローラーアクションで必要なすべてのデータに対して、サービス、メソッド、および戻り値の型を作成する必要があるため、開発とメンテナンスのコストがはるかに高くなります。
さて、実際には可能性がありますクエリまたは頻繁に再利用されるクエリパーツがあります。そのため、オプション1はオプション2とほぼ同じくらい極端だと思います。たとえば、キーによるWhere
句(おそらく使用されます)詳細には、確認ビューの編集と削除)、「ソフト削除」エンティティの除外、マルチテナントアーキテクチャのテナントによるフィルタリング、変更追跡の無効化など。このような非常に反復的なクエリロジックの場合、これをサービスに抽出することを想像できます。またはリポジトリレイヤー(ただし、再利用可能な拡張メソッドのみ)は、次のように意味があります。
_public IQueryable<Kitten> GetKittens()
{
return context.Kittens.AsNoTracking().Where(k => !k.IsDeleted);
}
_
プロパティの投影など、その後に続くものはすべてビュー固有であり、このレイヤーには含めたくありません。このアプローチを可能にするには、_IQueryable<T>
_をサービス/リポジトリから公開する必要があります。 select
がコントローラーアクションに直接含まれている必要があるという意味ではありません。特に、太くて複雑なプロジェクション(ナビゲーションプロパティによって他のエンティティを結合したり、グループ化を実行したりする可能性があります)は、他のファイル、ディレクトリ、または別のプロジェクトに収集される_IQueryable<T>
_の拡張メソッドに移動できますが、それでもプロジェクトですこれはプレゼンテーション層の付録であり、サービス層よりもはるかに近いものです。その場合、アクションは次のようになります。
_public ActionResult Kittens()
{
var result = kittenService.GetKittens()
.Where(kitten => kitten.fluffiness > 10)
.OrderBy(kitten => kitten.name)
.Select(kitten => new {
Name=kitten.name,
Url=kitten.imageUrl
})
.Take(10);
return Json(result,JsonRequestBehavior.AllowGet);
}
_
またはこのように:
_public ActionResult Kittens()
{
var result = kittenService.GetKittens()
.ToKittenListViewModel(10, 10);
return Json(result,JsonRequestBehavior.AllowGet);
}
_
ToKittenListViewModel()
の場合:
_public static IEnumerable<object> ToKittenListViewModel(
this IQueryable<Kitten> kittens, int minFluffiness, int pageItems)
{
return kittens
.Where(kitten => kitten.fluffiness > minFluffiness)
.OrderBy(kitten => kitten.name)
.Select(kitten => new {
Name = kitten.name,
Url = kitten.imageUrl
})
.Take(pageItems)
.AsEnumerable()
.Cast<object>();
}
_
これは基本的な考え方であり、別のソリューションがオプション1と2の中間にある可能性があるというスケッチです。
まあ、それはすべて全体的なアーキテクチャと要件に依存し、私が上で書いたものはすべて役に立たず、間違っているかもしれません。 ORMまたはデータアクセステクノロジーが将来変更される可能性があることを考慮する必要がありますか?コントローラーとデータベースの間に物理的な境界がある可能性がありますか?コントローラーはコンテキストから切断されていますか?また、将来、たとえばWebサービスを介してデータをフェッチする必要がありますか?これには、オプション2にさらに傾く、非常に異なるアプローチが必要になります。
そのようなアーキテクチャは非常に異なっているので、私の意見では、「多分」または「今ではないが、将来の要件になる可能性があるか、そうでない可能性がある」とは言えません。これは、プロジェクトの利害関係者がアーキテクチャの決定を進める前に定義する必要があるものです。これは、開発コストを劇的に増加させ、「たぶん」が実現しないことが判明した場合、開発と保守にお金を浪費するからです。
私は、「ビジネスロジック」と呼ぶことはめったにないWebアプリのクエリまたはGETリクエストについてのみ話していました。 POSTリクエストとデータの変更はまったく別の話です。たとえば、請求後に注文を変更することが禁止されている場合、これは通常、どちらに関係なく適用される一般的な「ビジネスルール」です。ビュー、Webサービス、バックグラウンドプロセス、または注文を変更しようとするものは何でも、注文ステータスのそのようなチェックをビジネスサービスまたは一般的なコンポーネントに入れ、コントローラーには入れません。
コントローラーアクションで_IQueryable<T>
_を使用することに反対する議論があるかもしれません。これは、LINQ-to-Entitiesに結合されており、単体テストが困難になるためです。しかし、ビジネスロジックを含まないコントローラーアクションでテストする単体テストとは何ですか?通常、モデルのバインドまたはルーティングを介してビューから渡されるパラメーターを取得します-単体テストではカバーされません-モックを使用します_IEnumerable<T>
_を返すリポジトリ/サービス-データベースクエリとアクセスはテストされていません-そしてそれはView
を返します-ビューの正しいレンダリングはテストされていませんか?
2番目のアプローチが優れています。不完全なアナロジーを試してみましょう:
あなたはピザ屋に入り、カウンターまで歩いて行きます。 「McPizzaMaestroDouble Deluxeへようこそ、ご注文を承りますか?」しわくちゃのレジ係があなたに尋ねます、彼の目の隙間はあなたを誘惑することを脅かしています。 「オーケー」、レジ係は「o」の音の真ん中で応答し、彼の声が鳴きます。彼はキッチンに向かって「OneJimmyCarter!」と叫びます。
そして、少し待った後、オリーブが入った大きなピザを手に入れます。何か変わったことに気づきましたか?レジ係は「生地を取り、クリスマスのように回転させ、チーズとトマトソースを注ぎ、オリーブを振りかけ、オーブンに約8分間入れます!」とは言いませんでした。考えてみると、それはまったく珍しいことではありません。レジ係は、ピザを欲しがる顧客と、ピザを作る料理人という2つの世界の間の単なる玄関口です。レジ係が知っているすべての人にとって、料理人はエイリアンからピザを入手するか、ジミー・カーターからピザをスライスします(彼は減少しているリソースです、人々)。
それがあなたの状況です。あなたのレジ係は馬鹿ではありません。彼はピザの作り方を知っています。それは彼がすべきピザを作っている、または誰かに方法ピザを作るように言っているという意味ではありません。それが料理人の仕事です。他の回答(特にFlorianMargaineとMadaraUchiha)が示すように、責任の分離があります。モデルはあまり機能しないかもしれません、それはただ1つの関数呼び出しであるかもしれません、それは1行でさえあるかもしれません-しかしそれは問題ではありません、なぜならコントローラー気にしない。
さて、オーナーがピザは単なる流行(冒涜!)であると判断し、あなたがより現代的な何か、派手なハンバーガージョイントに切り替えたとしましょう。何が起こるかを確認しましょう:
派手なハンバーガージョイントに入り、カウンターまで歩いて行きます。 「ルバーガーマエストロダブルデラックスへようこそ、ご注文を承りますか?」 「ええ、オリーブ入りの大きなハンバーガーを1つ食べます」。 「オーケー」、そして彼は台所に向きを変えます、「1人のジミー・カーター!」
そして、オリーブが入った大きなハンバーガーを手に入れます(ew)。
これが重要なフレーズです。
このクラスは、asp.net MVCプロジェクトだけでなく、他のプロジェクトでも使用する可能性があります。
コントローラーはHTTP中心です。HTTP要求を処理するためだけにあります。モデルを他のプロジェクト、つまりビジネスロジックで使用する場合は、コントローラーにロジックを含めることはできません。モデルを取り外して別の場所に配置できなければなりませんが、すべてのビジネスロジックは引き続き機能します。
したがって、いいえ、コントローラーからデータベースにアクセスしないでください。それはあなたが今までに得るかもしれないどんな可能な再利用も殺します。
簡単なメソッドを再利用できるときに、すべてのプロジェクトのすべてのdb/linqリクエストを本当に書き直したいですか?
もう1つのこと:オプション1の関数には、2つの責任があります。マッパーオブジェクトから結果をフェッチし、それを表示します。それはあまりにも多くの責任です。責任のリストには「and」があります。オプション2には、モデルとビューの間のリンクであるという1つの責任しかありません。
ASP.NETまたはC#がどのように機能するかはわかりません。しかし、私はMVCを知っています。
MVCでは、アプリケーションを2つの主要なレイヤーに分割します。プレゼンテーションレイヤー(コントローラーとビューを含む)とモデルレイヤー(...モデルを含む)。
重要なのは、アプリケーションの3つの主要な責任を分離することです。
ご覧のとおり、データベース処理はモデルにあり、いくつかの利点があります。
詳細については、こちらの優れた回答を参照してください:MVCでモデルをどのように構成する必要がありますか?
私は2番目のアプローチを好みます。少なくとも、コントローラーとビジネスロジックを分離します。ユニットテストを行うのはまだ少し難しいです(私はモックが苦手かもしれません)。
私は個人的に次のアプローチを好みます。主な理由は、プレゼンテーション、ビジネスロジック、データアクセスなどの各レイヤーの単体テストが簡単なことです。その上、あなたは多くのオープンソースプロジェクトでそのアプローチを見ることができます。
namespace MyProject.Web.Controllers
{
public class MyController : Controller
{
private readonly IKittenService _kittenService ;
public MyController(IKittenService kittenService)
{
_kittenService = kittenService;
}
public ActionResult Kittens()
{
// var result = _kittenService.GetLatestKittens(10);
// Return something.
}
}
}
namespace MyProject.Domain.Kittens
{
public class Kitten
{
public string Name {get; set; }
public string Url {get; set; }
}
}
namespace MyProject.Services.KittenService
{
public interface IKittenService
{
IEnumerable<Kitten> GetLatestKittens(int fluffinessIndex=10);
}
}
namespace MyProject.Services.KittenService
{
public class KittenService : IKittenService
{
public IEnumerable<Kitten> GetLatestKittens(int fluffinessIndex=10)
{
using(var db = new KittenEntities())
{
return db.Kittens // this explicit query is here
.Where(kitten=>kitten.fluffiness > 10)
.Select(kitten=>new {
Name=kitten.name,
Url=kitten.imageUrl
}).Take(10);
}
}
}
}
@Winには、私が多かれ少なかれ従うという考えがあります。
Presentation
だけをpresentsにします。
Controllerは単にbridgeとして機能し、実際には何もしません。それは仲介者です。テストが簡単なはずです。
DALは最も難しい部分です。 Webサービスで分離するのが好きな人もいますが、私はプロジェクトで一度分離しました。そうすれば、DALを他の人(内部または外部)が消費するためのAPIとして機能させることもできます。そのため、WCFまたはWebAPIが思い浮かびます。
そうすれば、DALはWebサーバーから完全に独立しています。誰かがサーバーをハッキングした場合でも、DALはおそらく安全です。
それはあなた次第だと思います。
単一責任の原則 。各クラスには、変更する理由が1つだけあるはずです。 @Zirakは、一連のイベントにおいて各人がどのように単一の責任を持っているかを示す良い例です。
あなたが提供した架空のテストケースを見てみましょう。
public ActionResult Kittens() // some parameters might be here
{
using(var db = new KittenEntities()){ // db can also be injected,
var result = db.Kittens // this explicit query is here
.Where(kitten=>kitten.fluffiness > 10)
.Select(kitten=>new {
Name=kitten.name,
Url=kitten.imageUrl
}).Take(10);
return Json(result,JsonRequestBehavior.AllowGet);
}
}
サービスレイヤー を間にすると、次のようになります。
public ActionResult Kittens() // some parameters might be here
{
using(var service = new KittenService())
{
var result = service.GetFluffyKittens();
return Json(result,JsonRequestBehavior.AllowGet);
}
}
public class KittenService : IDisposable
{
public IEnumerable<Kitten> GetFluffyKittens()
{
using(var db = new KittenEntities()){ // db can also be injected,
return db.Kittens // this explicit query is here
.Where(kitten=>kitten.fluffiness > 10)
.Select(kitten=>new {
Name=kitten.name,
Url=kitten.imageUrl
}).Take(10);
}
}
}
さらにいくつかの架空のコントローラークラスを使用すると、これを再利用するのがはるかに簡単になることがわかります。それは素晴らしいことです!コードを再利用できますが、さらに多くのメリットがあります。たとえば、私たちの子猫のWebサイトが狂ったように始まっているとしましょう。誰もがふわふわの子猫を見たいので、データベース(シャード)をパーティション分割する必要があります。すべてのdb呼び出しのコンストラクターには、適切なデータベースへの接続を挿入する必要があります。コントローラベースのEFコードでは、データベースの問題のためにコントローラを変更する必要があります。
明らかに、これは、コントローラーがデータベースの懸念に依存していることを意味します。現在、変更する理由が多すぎるため、コードに偶発的なバグが発生し、その変更に関係のないコードを再テストする必要が生じる可能性があります。
サービスを使用すると、コントローラーをその変更から保護しながら、次のことを実行できます。
public class KittenService : IDisposable
{
public IEnumerable<Kitten> GetFluffyKittens()
{
using(var db = GetDbContextForFuffyKittens()){ // db can also be injected,
return db.Kittens // this explicit query is here
.Where(kitten=>kitten.fluffiness > 10)
.Select(kitten=>new {
Name=kitten.name,
Url=kitten.imageUrl
}).Take(10);
}
}
protected KittenEntities GetDbContextForFuffyKittens(){
// ... code to determine the least used shard and get connection string ...
var connectionString = GetShardThatIsntBusy();
return new KittensEntities(connectionString);
}
}
ここで重要なのは、変更がコードの他の部分に到達しないように分離することです。コードの変更によって影響を受けるものはすべてテストする必要があるため、変更を相互に分離する必要があります。これには、コードをDRYに保つという副作用があるため、より柔軟で再利用可能なクラスとサービスが得られます。
クラスを分離することで、以前は困難または反復的だった動作を一元化することもできます。データアクセスのログエラーについて考えてください。最初の方法では、どこでもログを記録する必要があります。間にレイヤーがあると、ロギングロジックを簡単に挿入できます。
public class KittenService : IDisposable
{
public IEnumerable<Kitten> GetFluffyKittens()
{
Func<IEnumerable<Kitten>> func = () => {
using(var db = GetDbContextForFuffyKittens()){ // db can also be injected,
return db.Kittens // this explicit query is here
.Where(kitten=>kitten.fluffiness > 10)
.Select(kitten=>new {
Name=kitten.name,
Url=kitten.imageUrl
}).Take(10);
}
};
return this.Execute(func);
}
protected KittenEntities GetDbContextForFuffyKittens(){
// ... code to determine the least used shard and get connection string ...
var connectionString = GetShardThatIsntBusy();
return new KittensEntities(connectionString);
}
protected T Execute(Func<T> func){
try
{
return func();
}
catch(Exception ex){
Logging.Log(ex);
throw ex;
}
}
}
どちらの方法もテストにはあまり適していません。依存性注入を使用して、DIコンテナーを取得してdbコンテキストを作成し、それをコントローラーコンストラクターに注入します。
編集:テストについてもう少し
テストできる場合は、公開する前に、アプリケーションが仕様に従って機能するかどうかを確認できます。
簡単にテストできない場合は、テストを作成しません。
そのチャットルームから:
さて、ささいなアプリケーションではそれを書いてあまり変更しませんが、重要なアプリケーションでは依存性と呼ばれるこれらの厄介なものを取得します。これを変更すると多くのたわごとが壊れるので、依存性注入を使用して偽造できるリポジトリを挿入すると、コードが偽造されていないことを確認するために単体テストを作成できます
与えられた2つのオプションから選択する必要がある場合(注:本当に必要でした)、簡単にするために1と言いますが、保守が難しく、コードの重複が多く発生するため、使用することはお勧めしません。コントローラには、できるだけ少ないビジネスロジックを含める必要があります。データアクセスを委任し、それをViewModelにマップして、ビューに渡すだけです。
コントローラーからデータアクセスを抽象化したい場合(これは良いことです)、GetLatestKittens(int fluffinessIndex)
のようなメソッドを含むサービスレイヤーを作成することをお勧めします。
POCOにデータアクセスロジックを配置することもお勧めしません。これにより、別のORM(NHibernateなど)に切り替えて同じPOCOを再利用することはできません。