【问题标题】:MVC, ORM, and data access patternsMVC、ORM 和数据访问模式
【发布时间】:2011-07-17 09:48:12
【问题描述】:

我想我已经达到了“分析瘫痪”的状态。 我有一个 MVC 应用程序,使用 EF 作为 ORM。 因此,我正在尝试确定最佳数据访问模式,到目前为止,我认为将所有数据访问逻辑放入控制器中是可行的方法......但这听起来有点不对。 另一种选择是创建一个外部存储库,处理数据交互。 这是我的优点/缺点:

如果嵌入对控制器的数据访问,我将得到如下代码:

using (DbContext db = new DbContext())
{
    User user = db.Users.Where(x=>x.Name == "Bob").Single();
    user.Address.Street = "some st";
    db.SaveChanges();
}

因此,有了这个,我获得了延迟加载的全部好处,我在完成后立即关闭连接,我在 where 子句上很灵活 - 所有细节。 缺点 - 我在一个方法中混合了一堆东西 - 数据检查、数据访问、UI 交互。

使用 Repository,我将数据访问外部化,如果我决定使用 ado.net 或使用不同的数据库,理论上可以替换 repos。 但是,我看不到实现延迟加载以及如何控制 DbContext/连接生命周期的好方法。 说,我有带有 CRUD 方法的 IRepository 接口,我将如何加载属于给定用户的地址列表?使 GetAddressListByUserId 之类的方法看起来丑陋、错误, 并且会让我创建一堆同样丑陋的方法,并且在使用 ORM 时毫无意义。

我确信这个问题已经解决了百万次,希望在某个地方有解决方案..


还有一个关于存储库模式的问题——你如何处理作为属性的对象?例如。用户有一个地址列表,您将如何检索该列表?为地址创建一个存储库?使用 ORM,地址对象不必通过 repo 引用回用户,也不必 Id 字段 - 它必须拥有所有这些。更多代码,更多公开属性..

【问题讨论】:

标签: c# asp.net-mvc design-patterns orm asp.net-mvc-3


【解决方案1】:

您选择的方法在很大程度上取决于您将要使用的项目类型。对于需要 Rapid Application Development (RAD) 方法的小型项目,直接在 MVC 项目中使用您的 EF 模型并在控制器中访问数据几乎可以,但是项目越多,越混乱它会变成这样,你会开始遇到越来越多的问题。如果您想要良好的设计和可维护性,有几种不同的方法,但通常您可以坚持以下做法:

保持你的控制器和视图干净。控制器应该只控制应用程序流而不包含数据访问甚至业务逻辑。视图应该只用于演示 - 给它一个 ViewModel,它将以 Html 的形式呈现(没有业务逻辑或计算)。 ViewModel 每个视图是一种非常简洁的方式。

典型的控制器动作如下所示:

public ActionResult UpdateCompany(CompanyViewModel model)
{
    if (ModelState.IsValid)
    {
        Company company = SomeCompanyViewModelHelper.
                          MapCompanyViewModelToDomainObject(model);
        companyService.UpdateCompany(company);
        return RedirectToRoute(/* Wherever you go after company is updated */);
    }
    // Return the same view with highlighted errors
    return View(model);
}

由于上述原因,最好抽象您的数据访问(可测试性、易于切换数据提供者或 ORM 或其他任何东西等)。 Repository 模式是一个不错的选择,但在这里您还可以获得一些实现选项。关于泛型/非泛型存储库,是否应该返回IQueryables 等问题一直存在很多讨论。但最终还是由您来选择。

顺便说一句,你为什么要延迟加载?通常,您确切地知道特定视图需要哪些数据,那么为什么要选择以延迟方式获取它,从而进行额外的数据库调用,而不是在一次调用中急切地加载您需要的所有数据?就个人而言,我认为有多个 Get 方法来获取有或没有孩子的对象是可以的。例如。

public class CompanyRepository
{
    Get(int Id);
    Get(string name);
    GetWithEmployees(int id);
    ...
}

这似乎有点矫枉过正,您可能会选择不同的方法,但只要您有遵循的模式,维护代码就会容易得多。

【讨论】:

  • “易于切换 ORMS”是提倡存储库模式的糟糕理由。 1. 不常发生。 2. 它们之间有足够的差异,您可能正在考虑进行大型重构。
  • @Yakimych - 为什么不呢?抽象适用于实现可能发生变化或您想要隐藏实现细节的情况。很有可能,为与关系数据库交互而编写的系统永远不会改变为不使用关系数据库。一层也不是一个好的理由。如果我所有的数据库访问都是通过控制器完成的,那仍然需要更改一层。-还想补充一点,如果你有 GetWithEmployees(),你的“存储库”根本就不是存储库模式。您所做的只是编写一个具有更高级名称的 DAL。 ;)
  • 对于初学者,我没有说"The Repository pattern increases maintainability"。我说过,如果您想要大型项目的长期可维护性(参见以前的 cmets 之一),您可能不希望在控制器中访问数据。在这种情况下,存储库模式仅被称为拆分数据访问代码的方法之一。在任何情况下,可维护性都会提高,因为您的数据访问代码、业务逻辑和表示逻辑没有融合到一段代码中。我们通常尝试使其干净并具有 Web、Services、Repositories 层。 (而且没有 1500 行回购 ;))
  • 此外,在控制器中进行数据访问会严重限制您的数据访问代码重用。不是最罕见的情况是当您拥有一个 Web 应用程序,然后想要创建一个本质上相同的 Windows 应用程序。在这种情况下,您将从 web 应用程序中复制粘贴大部分数据访问代码,而不是重用服务或 repo 程序集中的现有方法。
  • @Yakimych 这就是我想将数据库调用外部化的原因之一——我们很有可能必须提供一个做同样事情的 Web 服务。我猜我们仍然可以使用 MVC,返回 JSON,但不确定这是否是最好的方法。
【解决方案2】:

我个人是这样做的:

我有一个抽象的领域层,它不仅有 CRUD 方法,还有专门的方法,例如 UsersManager.Authenticate() 等。它内部使用数据访问逻辑,或数据访问层抽象(取决于级别我需要的抽象)。

至少有一个抽象的依赖关系总是更好的。以下是它的一些优点:

  • 您可以在以后将一种实现替换为另一种实现。
  • 您可以在需要时对控制器进行单元测试。

就控制器本身而言,让它有 2 个构造函数:一个具有抽象域访问类(例如域的外观),另一个(空)构造函数选择默认实现。这样,您的控制器在 Web 应用程序运行时(调用空构造函数)和单元测试期间(注入模拟域层)期间运行良好。

此外,为了能够在以后轻松切换到另一个域,请务必注入域创建者,而不是域本身。这样,将域层构造本地化到域创建者,您可以随时切换到另一个实现,只需重建域创建者(创建者是指某种工厂)。

我希望这会有所帮助。

加法

  • 我不建议在域层中使用 CRUD 方法,因为当您丰富单元测试阶段时,这将成为一场噩梦,甚至当您需要稍后将实现更改为新的时,这将成为一场噩梦。

【讨论】:

  • +1 - 另见数据访问对象。它使用存储库来执行您需要的查询。
  • 那么您为每个数据驱动的类都有一个抽象存储库?
  • 数据驱动的类是什么意思? - 控制器?
  • 他可能是指域类。
  • 谢谢亚基米奇!实际上,我为每个域数据类创建了一个抽象存储库——是的。但是为了简单起见,在域数据类中使用直接数据访问代码同样有效。一切都取决于项目规模和可扩展性要求。主要的是 - 将所有东西都注入控制器 - 这会让你的生活变得更好......好吧,至少是存储库:-)
【解决方案3】:

这真的取决于您想要代码的位置。如果您需要对某个对象进行数据访问,您可以将它放在 IRepository 对象后面或放在控制器中,这无关紧要:您仍然会得到一系列 GetByXXX 调用或等效代码。无论哪种方式,您都可以延迟加载并控制连接的生命周期。所以现在你需要问自己:我希望我的代码放在哪里?

就个人而言,我会主张将其从控制器中取出。我的意思是把它移到另一层。可能使用 IRespository 类型的模式,其中您有一系列 GetByXXX 调用。当然,他们很丑。错误的?否则我会争辩。至少它们都包含在同一个逻辑层中,而不是分散在与验证代码等混合在一起的控制器中。

【讨论】:

  • -1 '对于'或在控制器中没有关系。'这确实很重要。一个类应该只有一个改变的理由。如果控制器做两件事,那么它有两个改变的理由,那是糟糕的设计。
  • 乔治说得好。肯,另请参阅en.wikipedia.org/wiki/Single_responsibility_principle
  • @George Stocker - 控制器本质上违反了 SRP。大型存储库/服务也违反了 SRP。您可以更改存储库/服务层,因为 GetCustomer 需要其他字段,因为 GetCustomerBalanceByDateRange 也需要更改。当依赖于它的每个类只使用它的一小部分功能时,存储库/服务就会成为违反 SRP 的巨大塔。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-16
  • 1970-01-01
  • 2015-10-11
  • 1970-01-01
  • 2019-05-27
  • 1970-01-01
相关资源
最近更新 更多