避免在Controllers中使用SQL的策略…或者我的模型中应该有多少种方法?


17

因此,我遇到的情况经常是我的模型开始出现以下情况之一:

  • 用无数种方法成长为怪物

要么

  • 允许您将SQL片段传递给它们,以便它们足够灵活,不需要一百万种不同的方法

例如,假设我们有一个“小部件”模型。我们从一些基本方法开始:

  • get($ id)
  • 插入($ record)
  • 更新($ id,$ record)
  • 删除($ id)
  • getList()//获取小部件列表

一切都很好,但是然后我们需要一些报告:

  • listCreatedBetween($ start_date,$ end_date)
  • listPurchasedBetween($ start_date,$ end_date)
  • listOfPending()

然后,报告开始变得复杂:

  • listPendingCreatedBetween($ start_date,$ end_date)
  • listForCustomer($ customer_id)
  • listPendingCreatedBetweenForCustomer($ customer_id,$ start_date,$ end_date)

您可以看到增长的地方...最终,我们有如此多的特定查询要求,我要么需要实现大量的方法,要么可以将某种“查询”对象传递给单个-> query(query $ query)方法...

...或者只是硬着头皮,开始做这样的事情:

  • list = MyModel-> query(“开始日期> X AND结束日期<Y AND待定= 1 AND customer_id = Z”)

仅仅拥有一个这样的方法而不是五千万个其他更具体的方法是有一定吸引力的,但是有时将一堆基本上是SQL的东西填充到控制器中,这是“错误的”。

是否有“正确”的方式来处理这种情况?将这样的查询填充到通用的-> query()方法中似乎可以接受吗?

有更好的策略吗?


我现在正在一个非MVC项目中遇到同样的问题。问题不断浮出水面:数据访问层应该抽象出每个存储过程,并使业务逻辑层数据库不可知,还是数据访问层是通用的,以牺牲业务层对底层数据库的了解为代价?也许一种中间解决方案是使用ExecuteSP(字符串spName,params object []参数)之类的东西,然后将所有SP名称包括在配置文件中,以供业务层读取。我对此没有很好的答案。
格雷格·杰克逊

Answers:


10

Martin Fowler的企业应用程序体系结构模式描述了许多与ORM相关的模式,包括查询对象的使用,这就是我的建议。

通过将每个查询的逻辑分为单独管理和维护的策略对象,查询对象使您可以遵循“单一职责”原则。控制器可以直接管理其使用,也可以将其委派给辅助控制器或辅助对象。

你会很多吗?当然。可以将一些分组为通用查询吗?再来一次

您可以使用依赖注入从元数据创建对象吗?那就是大多数ORM工具所做的。


4

没有正确的方法来执行此操作。许多人使用ORM来消除所有复杂性。一些更高级的ORM将代码表达式转换为复杂的SQL语句。ORM也有其缺点,但是对于许多应用程序而言,好处多于成本。

如果您不使用海量数据集,那么最简单的方法是将整个表选择到内存中并用代码过滤。

//pseudocode
List<Person> people = Sql.GetList<Person>("select * from people");
List<Person> over21 = people.Where(x => x.Age >= 21);

对于内部报告应用程序,此方法可能很好。如果数据集确实很大,那么您将开始需要大量自定义方法以及表上的适当索引。


1
+1(表示“没有正确的方法可以执行此操作”)
ozz 2012年

1
不幸的是,即使使用最小的数据集,也无法对数据集进行过滤,因为它太慢了。:-(很高兴听到其他人遇到了我同样的问题。:-)
Keith Palmer Jr.

@KeithPalmer出于好奇,您的桌子有多大?
2012年

数十万行,如果没有更多的话。在数据库之外,有太多的过滤器无法以可接受的性能进行过滤,尤其是在分布式体系结构中,数据库与应用程序不在同一台计算机上。
基思·帕尔默(Keith Palmer)

-1表示“没有正确的方法来执行此操作”。有几种正确的方法。在OP进行操作时添加功能时将方法数量加倍是不可扩展的方法,此处建议的替代方法同样不可​​扩展,仅就数据库大小而不是查询功能的数量而言。确实存在可扩展的方法,请参见其他答案。
西奥多·默多克

4

一些ORM允许您从基本方法开始构造复杂的查询。例如

old_purchases = (Purchase.objects
    .filter(date__lt=date.today(),type=Purchase.PRESENT).
    .excude(status=Purchase.REJECTED)
    .order_by('customer'))

Django ORM中是一个完全有效的查询。

这个想法是,您有一些查询生成器(在本例中为Purchase.objects),其内部状态表示有关查询的信息。类似的方法getfilterexcludeorder_by是有效的,并与更新的状态返回一个新的查询生成器。这些对象实现了一个可迭代的接口,因此,当您对其进行迭代时,将执行查询并获得到目前为止构建的查询的结果。尽管此示例取自Django,但您将在许多其他ORM中看到相同的结构。


我看不出它比old_purchases = Purchases.query(“ date> date.today()AND type = Purchase.PRESENT AND status!= Purchase.REJECTED”)有什么优势?您不是通过将SQL AND和OR变成方法AND和OR来降低复杂性或抽象化任何东西-您只是在更改AND和OR的表示形式,对吗?
基思·帕尔默(Keith Palmer)2012年

4
其实并不是。您正在抽象SQL,这为您带来了很多优势。首先,避免注射。然后,您可以更改基础数据库,而不必担心SQL方言的版本稍有不同,因为ORM会为您处理此问题。在许多情况下,您也可以不加注意地放置NoSQL后端。第三,这些查询生成器是可以像其他任何东西一样传递的对象。这意味着,你的模型可以构造一半的查询(例如,你可以有最常见的情况的一些方法),然后它可以在控制器细化到处理..
安德烈

2
...最具体的情况 一个典型的示例是在Django中定义模型的默认顺序。除非您另外指定,否则所有查询结果都将遵循该顺序。第四,如果出于性能原因需要对数据进行非规范化,则只需调整ORM,而不必重写所有查询。
安德里亚(Andrea)2012年

+1适用于上述动态查询语言和LINQ。
埃文·普赖斯

2

还有第三种方法。

您的特定示例展示了所需的方法数量随所需功能的数量呈指数增长:我们希望能够提供高级查询,将每个查询功能组合在一起...如果我们通过添加方法来做到这一点,那么对于基本查询,如果添加一个可选功能,则为两个,如果添加两个可选功能,则为四个,如果添加三个,则为八个,如果添加n个功能,则为2 ^ n。

除了三个或四个功能外,这显然是无法维持的,而且很多紧密相关的代码几乎都在方法之间进行了复制粘贴,从而散发出难闻的气味。

您可以通过添加一个数据对象来保存参数来避免这种情况,并且可以使用一个方法根据提供(或不提供)的参数集构建查询。在这种情况下,添加日期范围之类的新功能就像将日期范围的设置器和获取器添加到数据对象一样简单,然后在构建参数化查询的地方添加一些代码:

if (dataObject.getStartDate() != null) {
    query += " AND (date BETWEEN ? AND ?) "
}

...以及将参数添加到查询的位置:

if (dataObject.getStartDate() != null) {
    preparedStatement.setTime(dataObject.getStartDate());
    preparedStatement.setTime(dataObject.getEndDate());
}

这种方法允许随着功能的增加线性代码的增长,而不必允许任意的,无参数的查询。


0

我认为一般的共识是在MVC中的模型中保持尽可能多的数据访问。其他设计原则之一是将一些更通用的查询(那些与您的模型没有直接关系的查询)移到更高,更抽象的级别,在此您还可以允许其他模型使用它。(在RoR中,我们有一个称为框架的东西)。还有另一件事要考虑,那就是代码的可维护性。随着项目的发展,如果您具有控制器中的数据访问权限,则追踪它将变得越来越困难(我们目前在一个大型项目中正面临着这个问题)模型,尽管杂乱的方法为任何需要控制的控制器提供了单点联系可能最终从表中查询。(这也可能导致代码的重用,这反过来是有益的)


1
您在说什么的例子...?
基思·帕尔默(Keith Palmer)2012年

0

您的服务层接口可能有很多方法,但是对数据库的调用只能有一个。

数据库有4个主要操作

  • 插入
  • 更新资料
  • 删除
  • 询问

另一种可选方法可能是执行一些不属于基本DB操作的数据库操作。让我们称之为执行。

插入和更新可以合并为一项操作,称为保存。

您的方法很多都是查询的。因此,您可以创建一个通用接口来满足大多数即时需求。这是一个示例通用接口:

 public interface IDALService
    {
        DataTransferObject<T> Save<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Search<T>(DataTransferObject<T> Dto) where T: IPOCO;
        DataTransferObject<T> Delete<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Execute<T>(DataTransferObject<T> Dto) where T : IPOCO;
    }

数据传输对象是通用的,并且其中包含所有的过滤器,参数,排序等。数据层将负责解析和提取数据,并通过存储过程,参数化的sql,linq等对数据库进行操作。因此,SQL不在层之间传递。这通常是ORM所做的,但是您可以自己滚动并拥有自己的映射。

因此,在您的情况下,您有小部件。小部件将实现IPOCO接口。

因此,在您的服务层模型中 getList().

需要一个映射层来处理转换getList

Search<Widget>(DataTransferObject<Widget> Dto)

反之亦然。就像其他人提到的那样,有时这是通过ORM完成的,但是最终您会得到很多样板代码,尤其是当您有100个表时。ORM神奇地创建了参数化的SQL,并针对数据库运行它。如果要滚动自己的数据(另外在数据层本身中),则需要映射程序来设置SP,linq等。(基本上,进入数据库的sql)。

如前所述,DTO是由合成组成的对象。其中包含的对象之一可能是一个称为QueryParameters的对象。这些将是查询所要设置和使用的所有查询参数。另一个对象将是查询,更新等的返回对象列表。这就是有效载荷。如果是这种情况,则有效负载将是小部件列表列表。

因此,基本策略是:

  • 服务层呼叫
  • 使用某种存储库/映射将服务层调用转换为数据库
  • 数据库调用

在您的情况下,我认为该模型可以有很多方法,但是最佳情况下,您希望数据库调用是通用的。您仍然会得到大量的样板映射代码(尤其是SP)或神奇的ORM代码,它们可以为您动态创建参数化的SQL。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.