在一个好的设计中。应该在单独的业务逻辑层(在asp.net MVC模型中)处理访问数据库,还是可以将IQueryable
或DbContext
对象传递给控制器?
为什么呢?各自的优点和缺点是什么?
我在C#中构建ASP.NET MVC应用程序。它使用EntityFramework作为ORM。
让我们稍微简化一下这个场景。
我有一个可爱的蓬松小猫的数据库表。每只小猫都有小猫图像链接,小猫蓬松指数,小猫名称和小猫ID。这些映射到EF生成的名为Kitten
的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的类。基本上它应该是数据库实体上面的层,因此控制器不必知道实际的数据库,映射器或实体框架。
注意:当然,替代方法非常也被视为答案。
澄清1:在实践中, 这是一个简单的应用程序。这是一个具有数十个控制器和数千行代码的应用程序,这些实体不仅在这里使用,而且在数十个其他C#项目中使用。这里的示例是简化测试用例。
答案 0 :(得分:25)
第二种方法更优越。让我们尝试一个蹩脚的比喻:
你进入一家披萨店,然后走到柜台前。 “欢迎来到McPizza Maestro Double Deluxe,我可以接受您的订单吗?”那个疙瘩的收银员问你,他眼中的虚空有可能引诱你。“是的,我会买一个带橄榄的大披萨”。 “好的”,收银员回答,他的声音在“o”声中间嘶叫。他向厨房大喊“One Jimmy Carter!”
然后,等了一会儿,你会得到一个带橄榄的大披萨。你注意到什么特别的东西吗?收银员没有说“拿一些面团,像圣诞节那样旋转它,倒一些奶酪和番茄酱,撒上橄榄,放入烤箱约8分钟!”想一想,这根本不是特别的。收银员只是两个世界之间的门户:想要披萨的顾客和制作披萨的厨师。对于所有收银员都知道,厨师从外星人那里得到他的披萨,或者把他们从吉米卡特那里切下来(他的资源越来越少,人们也不知所措。)
这是你的情况。你的收银员不是傻瓜。他知道如何制作披萨。这并不意味着他应该制作披萨,或者告诉某人如何制作披萨。这是厨师的工作。正如其他答案(特别是Florian Margaine和Madara Uchiha)所说明的那样,责任分离。该模型可能不会做太多,它可能只是一个函数调用,它甚至可能是一行 - 但这并不重要,因为控制器不关心。
现在,让我们说主人认为比萨饼只是一种时尚(亵渎神灵!)然后你转向更现代的东西,一种奇特的汉堡包。让我们来看看会发生什么:
你进入一个花式汉堡关节,走到柜台。 “欢迎来到Le Burger Maestro Double Deluxe,我可以接受您的订单吗?” “是的,我会吃一个带橄榄的大汉堡”。 “好的”,他转向厨房,“一个吉米卡特!”
然后,你会得到一个带橄榄的大汉堡包(ew)。
答案 1 :(得分:12)
选项1和2有点极端,就像魔鬼和深蓝色海洋之间的选择一样,但如果我必须在两者之间做出选择,我宁愿选择1。
首先,选项2将抛出运行时异常,因为实体框架不支持投影到实体(Select(e => new Kitten(...))
中,并且它不允许在投影中使用带参数的构造函数。现在,此注释在这种情况下似乎有点迂腐,但通过投射到实体并返回Kitten
(或Kitten
的枚举),你隐藏了这种方法的真正问题。
显然,您的方法会返回您要在视图中使用的实体的两个属性 - 小猫的name
和imageUrl
。因为这些只是所有Kitten
属性的选择,返回(半填充)Kitten
实体是不合适的。那么,从这个方法实际返回什么类型?
object
(或IEnumerable<object>
)(这就是我如何理解您对&#34; 对象方法&#34的评论;)如果您将结果传递到Json(...)
以便稍后在Javascript中处理,那么这很好。但是你会丢失所有编译时类型信息,我怀疑object
结果类型对其他任何东西都有用。现在,这只是一个视图的一种方法 - 列出小猫的视图。然后你有一个详细信息视图来显示一只小猫,然后是编辑视图,然后是删除确认视图。现有Kitten
实体的四个视图,每个视图都需要不同的属性,每个属性都需要一个单独的方法和投影以及不同的DTO类型。 Dog
实体和项目中的100个实体相同,您可能获得400种方法和400种返回类型。
除了这个特定的观点之外,很可能不会在任何其他地方重复使用。为什么你想要Take
只有name
只有imageUrl
和Age
的小猫?你有第二只小猫列表视图吗?如果是这样,它将有一个原因,并且查询只是偶然和现在相同,如果一个更改另一个不一定,否则列表视图不正确&#34;重复使用&#34;并且不应该存在两次。或者Excel导出使用的列表可能相同吗?但也许Excel用户希望明天有1000只小猫,而视图应该仍然只显示10.或者视图应该明天显示小猫的GetLatestKittensForListView
,但Excel用户不想要有这个,因为他们的Excel宏不会再次正确运行该更改。仅仅因为两段代码是相同的,如果它们处于不同的上下文或具有不同的语义,则不必将它们分解为公共可重用组件。最好留一个GetLatestKittensForExcelExport
和Where
。或者你最好不要在服务层中使用这些方法。
根据这些考虑因素,前往比萨店的游览作为第一种方法优越的比喻:)
&#34;欢迎来到BigPizza,定制披萨店,我可以接受您的订单吗?&#34; &#34;嗯,我想要一个披萨加橄榄,但是顶部是番茄酱,底部是奶酪,然后在烤箱里烘烤90分钟,直到它变成黑色,像硬盘一样坚硬花岗岩。&#34; &#34;好的,先生,定制的比萨饼是我们的专业,我们会做到。&#34;
收银员去了厨房。 &#34;在柜台有一个心理学家,他想要一个披萨......它是一块花岗岩的岩石......等等......我们需要先说出一个名字&#34;,他告诉厨师。
&#34;不!&#34;,厨师尖叫,&#34;不再!你知道我们已经尝试过了。&#34;他拿了一叠400页的纸,&#34;这里我们从2005年开始有花岗岩,但是......它没有橄榄,但是有了paprica ......这里是顶级番茄 ...但客户希望它只需半分钟即可烘烤。&#34; &#34;也许我们应该把它称为 TopTomatoGraniteRockSpecial ?&#34; &#34;但它并没有考虑底部的奶酪......&#34;收银员:&#34; Special 应该表达的内容。&#34; &#34;但是比萨饼岩石形成的金字塔也很特别,#34;厨师回答。 &#34;嗯......这很难......&#34;,绝望的收银员说。
&#34;我的比萨饼已经在烤箱里了吗?&#34;突然它在厨房门口喊叫。 &#34;让我们停止讨论,告诉我如何制作这种比萨饼,我们不会第二次吃这样的比萨饼,厨师决定。 &#34;好吧,它是用橄榄比萨饼,但是顶部是番茄酱,底部是奶酪,然后在烤箱里烘烤90分钟,直到它变成黑色,像硬花岗岩一样坚硬。 &#34;
如果选项1通过在视图层中使用数据库上下文违反了关注点分离原则,则选项2通过在服务或业务层中使用以表示为中心的查询逻辑来违反相同的原则。从技术的角度来看,它不会,但它最终会得到一个服务层,而不是“可重复使用的”服务层。在表示层之外。它具有更高的开发和维护成本,因为对于控制器操作中的每个必需数据,您必须创建服务,方法和返回类型。
现在,实际上可能是查询或查询经常重复使用的部分,这就是为什么我认为选项1几乎与选项2一样极端 - 例如{{1密钥的子句(可能会在细节中使用,编辑和删除确认视图),过滤掉&#34;软删除&#34;实体,由多租户架构中的租户过滤或禁用变更跟踪等。对于这种真正的重复查询逻辑,我可以想象将其提取到服务或存储库层(但可能只是可重用的扩展方法)可能有意义,如
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。
这样的架构是如此不同 - 在我看来 - 你根本不能说&#34;也许&#34;或者&#34;不是现在,但可能在将来可能是一个要求,或者可能它不会被赢得#34;。这是项目的利益相关者在进行架构决策之前必须定义的东西,因为它会大大增加开发成本,如果&#34;也许&#34;可能会浪费资金用于开发和维护。结果永远不会成为现实。
我只是在谈论一个网络应用中的查询或GET请求,这些请求很少我会称之为&#34;业务逻辑&#34;一点都不POST请求和修改数据是一个完全不同的故事。如果禁止订单在开票后可以更改,例如这是一般的商业规则&#34;通常适用于任何视图或Web服务或后台进程或任何尝试更改订单的内容。我肯定会将订单状态检查到业务服务或任何常见组件中,而不会进入控制器。
可能存在反对在控制器操作中使用IQueryable<T>
的争论,因为它与LINQ-to-Entities耦合,这将使单元测试变得困难。但是什么是单元测试将在一个控制器动作中进行测试,该动作不包含任何业务逻辑,这些参数传递的参数通常来自视图,通过模型绑定或路由 - 单元测试未涵盖 - 使用模拟的存储库/服务返回IEnumerable<T>
- 未测试数据库查询和访问 - 并且返回View
- 未测试视图的正确呈现?
答案 2 :(得分:9)
这是关键词:
我可能会在其他项目中使用此类,而不仅仅是asp.net MVC项目。
控制器以HTTP为中心。只能处理HTTP请求。如果您想在任何其他项目中使用您的模型,即您的业务逻辑,那么不能在控制器中有任何逻辑。您必须能够脱掉模型,将其放在其他地方,并且所有业务逻辑仍然有效。
所以,不,不要从你的控制器访问你的数据库。它会杀死你可能获得的任何可能的重用。
当您可以使用可以重复使用的简单方法时,是否真的想要在所有项目中重写所有db / linq请求?
另一件事:选项1中的函数有两个职责:它从映射器对象获取结果,显示它。这是太多的责任。责任清单中有一个“和”。您的选项2只有一个责任:作为模型和视图之间的链接。
答案 3 :(得分:4)
我不确定ASP.NET或C#是如何做的。但我确实知道MVC。
在MVC中,您将应用程序分为两个主要层:演示层(包含Controller和View),以及 Model 层(包含.. 。模型)。
关键在于分离申请中的3个主要职责:
如您所见,在Model上可以找到数据库处理,它有几个优点:
有关详细信息,请参阅此处的优秀答案: How should a model be structured in MVC?
答案 4 :(得分: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);
}
}
}
}
答案 5 :(得分:2)
让Presentation
只是呈现。
控制器只是充当网桥,它什么都不做,它是中间人。应该很容易测试。
DAL是最难的部分。有些人喜欢在Web服务上将其分开,我曾经为一个项目做过一次。这样你就可以让DAL充当其他人(内部或外部)消费的API - 所以我想到了WCF或WebAPI。
这样您的DAL完全独立于您的Web服务器。如果某人攻击您的服务器,DAL可能仍然是安全的。
这取决于你我猜。
答案 6 :(得分:2)
Single Responsibility Principle。您的每个课程都应该只有一个改变的理由。 @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);
}
}
介于两者之间service layer,它可能看起来像这样。
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);
}
}
}
使用更多虚构的控制器类,您可以看到如何更容易重用。那很棒!我们有代码重用,但还有更多的好处。让我们举个例子说,我们的小猫网站就像疯了一样起飞,每个人都想看看蓬松的小猫,所以我们需要对我们的数据库进行分区(分片)。我们所有db调用的构造函数都需要注入与正确数据库的连接。使用基于控制器的EF代码,由于DATABASE问题,我们必须更改控制器。
显然,这意味着我们的控制器现在依赖于数据库问题。他们现在有太多的理由要改变,这可能会导致代码中的意外错误,并且需要重新测试与该更改无关的代码。
通过服务,我们可以执行以下操作,同时保护控制器免受此更改。
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);
}
}
这里的关键是将更改与代码的其他部分隔离开来。您应该测试任何受代码更改影响的内容,因此您希望将更改彼此隔离。这会产生保持代码干燥的副作用,因此您最终会获得更灵活,可重用的类和服务。
分离类还允许您集中以前难以或重复的行为。考虑记录数据访问中的错误。在第一种方法中,您需要在任何地方进行记录。如果介于两者之间,您可以轻松插入一些日志记录逻辑。
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;
}
}
}
答案 7 :(得分:1)
无论哪种方式都不适合测试。使用依赖注入来获取DI容器以创建db上下文并将其注入控制器构造函数。
编辑:关于测试的更多信息
如果您可以测试,您可以在发布之前查看您的应用程序是否按照规范运行 如果你不能轻易测试,你就不会写测试。
来自聊天室:
好的,所以在一个简单的应用程序上你写它并且它没有太大变化, 但是在一个非常简单的应用程序中,你会得到这些令人讨厌的东西,称为依赖项,当你更改它时会打破很多东西,所以你使用依赖注入注入一个你可以伪造的repo,然后你可以编写单元测试来制作确保你的代码没有
答案 8 :(得分:1)
如果我有(注意:真的有)在两个给定的选项之间进行选择,为简单起见,我会说1,但我不建议使用它,因为它很难维护并导致很多重复的代码。 控制器应包含尽可能少的业务逻辑。它应该只委托数据访问,将其映射到ViewModel并将其传递给View。
如果要从控制器中抽象数据访问(这是一件好事),您可能需要创建一个包含GetLatestKittens(int fluffinessIndex)
等方法的服务层。
我不建议在POCO中放置数据访问逻辑,这不允许您切换到另一个ORM(例如NHibernate)并重用相同的POCO。