【问题标题】:Why is ORM considered good but "select *" considered bad?为什么 ORM 被认为是好的但“select *”被认为是坏的?
【发布时间】:2008-11-15 00:21:03
【问题描述】:

ORM 通常不涉及执行 select * 之类的操作吗?

如果我有一个表 MyThing,其中包含 A、B、C、D 等列,那么通常会有一个对象 MyThing,其属性为 A、B、C、D。

如果该对象没有被如下所示的 select 语句完全实例化,那将是邪恶的,只获取 A、B,而不是 C、D:

select A, B from MyThing /* 不要得到 C 和 D,因为我们不需要它们 */

但总是这样做也是邪恶的:

select A, B, C, D /* 获取所有列以便我们可以完全实例化 MyThing 对象 */

ORM 是否假设数据库访问如此之快,现在您不必担心它,因此您总是可以获取所有列?

或者,您是否有不同的 MyThing 对象,每个对象对应一个可能恰好位于 select 语句中的列组合?

编辑:在回答问题之前,请阅读 Nicholas Piasecki 和 Bill Karwin 的回答。我想我问的问题很糟糕,因为很多人误解了它,但尼古拉斯 100% 理解它。和他一样,我对其他答案很感兴趣。


编辑#2:与此问题相关的链接:

Why do we need entity objects?

http://blogs.tedneward.com/2006/06/26/The+Vietnam+Of+Computer+Science.aspx,尤其是“部分对象问题和加载时间悖论”部分

http://groups.google.com/group/comp.object/browse_thread/thread/853fca22ded31c00/99f41d57f195f48b?

http://www.martinfowler.com/bliki/AnemicDomainModel.html

http://database-programmer.blogspot.com/2008/06/why-i-do-not-use-orm.html

【问题讨论】:

  • 也许问题出在标题上——“select *”而不是“select x,y,z”?
  • 您使用的是哪个 ORM,哪个执行 SELECT *?我用过 llblgen 和 nhibernate,都拼出要获取的每一列

标签: orm


【解决方案1】:

根据我有限的经验,事情正如你所描述的那样 - 这是一个混乱的情况,通常的逃避“取决于”的答案适用。

我工作的在线商店就是一个很好的例子。它有一个Brand 对象,在网站的主页上,商店销售的所有品牌都列在左侧。要显示这个品牌菜单,网站需要的只是整数BrandId 和字符串BrandName。但是Brand 对象包含一大堆其他属性,最值得注意的是Description 属性,它可以包含大量关于Brand 的文本。没有两种方法,加载所有关于品牌的额外信息只是为了在无序列表中吐出它的名字是(1)可测量且显着缓慢,通常是因为大文本字段和(2)当它出现时效率很低内存使用,构建大字符串,甚至在扔掉它们之前都没有看它们。

许多 ORM 提供的一个选项是延迟加载属性。所以我们可以有一个Brand 对象返回给我们,但是那个耗时且浪费内存的Description 字段直到我们尝试调用它的get 访问器。此时,代理对象将拦截我们的调用并及时从数据库中提取描述。这有时已经足够好,但已经让我感到很痛苦,我个人不推荐它:

  • 很容易忘记该属性是延迟加载的,只需编写一个 foreach 循环就会引入 SELECT N+1 问题。谁知道当 LINQ 参与其中时会发生什么。
  • 如果由于传输混乱或网络中断而导致即时数据库调用失败怎么办?我几乎可以保证,任何像string desc = brand.Description 一样无害的代码都不会期望这样简单的调用会抛出DataAccessException。现在你刚刚以一种令人讨厌和意想不到的方式坠毁。 (是的,我已经看到我的应用因此而严重失败。学到了很多东西!)

所以我最终所做的是,在需要性能或容易出现数据库死锁的场景中,我创建了一个单独的接口,网站或任何其他程序都可以调用该接口来访问具有仔细检查了他们的查询计划。架构最终看起来有点像这样(原谅 ASCII 艺术):

网站:控制器类 | |----------------------------------+ | | 应用服务器:IDocumentService IOrderService、IInventoryService 等 (数组、数据集)(常规 OO 对象,如 Brand) | | | | | | 数据层:(原始 ADO.NET 返回数组,(像 NHibernate 一样的“全奶油” ORM) 数据集,简单的类)

我曾经认为这是作弊,颠覆了OO对象模型。但在实际意义上,只要你做这个显示数据的快捷方式,我认为就可以了。更新/插入以及您仍然通过完全水合、ORM 填充的域模型进行的操作,与显示特定数据子集相比,这种情况发生的频率要低得多(在我的大多数情况下)。像 NHibernate 这样的 ORM 可以让你进行预测,但到那时我还没有看到 ORM 的意义。无论如何,这可能是一个存储过程,编写 ADO.NET 需要两秒钟。

这只是我的两分钱。我期待阅读其他一些回复。

【讨论】:

    【解决方案2】:

    人们使用 ORM 是为了提高开发效率,而不是为了优化运行时性能。这取决于项目是最大化开发效率还是运行时效率更重要。

    在实践中,可以使用 ORM 来获得最大的生产力,然后在完成后分析应用程序以识别瓶颈。仅在您获得最大收益的情况下才将 ORM 代码替换为自定义 SQL 查询。

    SELECT * 如果您通常需要表中的所有列,这还不错。我们不能一概而论,通配符总是好的或坏的。

    编辑: 回复:doofledorfer 的评论...就个人而言,我总是明确命名查询中的列;我从不在生产代码中使用通配符(尽管我在进行临时查询时使用它)。最初的问题是关于 ORM 的——事实上,ORM 框架统一发出SELECT * 来填充相应对象模型中的所有字段并不罕见。

    执行SELECT * 查询不一定表示您需要所有这些列,也不一定意味着您忽略了代码。可能是 ORM 框架正在生成 SQL 查询以确保所有字段都可用以防万一您需要它们。

    【讨论】:

    • 您完全理解我的问题的意图。谢谢。
    • 我认为使用“SELECT *”表示您的决定,实际上,您需要表中的所有列。如果这不是真的,那就是误导。它还表明您可能没有认真考虑它(除非您发表评论断言。)
    【解决方案3】:

    Linq to SqlIQueryable 的任何实现都使用最终使您能够控制所选数据的语法。查询的定义也是其结果集的定义。

    这通过从 ORM 中删除数据形状职责巧妙地避免了 select * 问题。

    例如,选择所有列:

    from c in data.Customers
    select c
    

    要选择一个子集:

    from c in data.Customers
    select new
    {
      c.FirstName,
      c.LastName,
      c.Email
    }
    

    选择组合:

    from c in data.Customers
    join o in data.Orders on c.CustomerId equals o.CustomerId
    select new
    {
      Name = c.FirstName + " " + c.LastName,
      Email = c.Email,
      Date = o.DateSubmitted
    }
    

    【讨论】:

    • 但是....您创建的对象到底是什么?即使它们具有不同的属性,它们都是“客户”域对象吗?
    • 新对象是一个匿名类型,由编译器使用定义的模式生成。就好像您自己定义了它,只是您不必管理类工件。请参阅msdn.microsoft.com/en-us/library/bb397696.aspx 了解更多信息。
    • 对象代表一个投影,根据定义,它不是域对象,不能与域对象混淆。如果您需要一个完整的客户,请选择该客户,否则请选择其他客户。 “人口稀少的对象”似乎是一种反模式,会因技术问题而损害领域。
    • 您最终会遇到技术问题而不是领域问题,并且应用程序的设计是从技术回馈给用户,而不是在另一个方向正确地设计。但也许程序员更快乐一些。
    • 这些是范式转变的成长之痛。灵活的 IQueryable 抽象需要强大的开发人员。关键是查询定义可以是域的一部分,而不仅仅是“请在您的应用程序中实现它”的挂钩点。
    【解决方案4】:

    有两个不同的问题需要考虑。

    首先,当使用 ORM 表和对象具有完全不同的“形状”时,这是很常见的,这也是许多 ORM 工具支持相当复杂的映射的原因之一。

    一个很好的例子是当一个表被部分非规范化时,其中的列包含冗余信息(通常,这样做是为了提高查询或报告性能)。发生这种情况时,ORM 只请求它需要的列比将所有额外的列带回并忽略更有效。

    为什么“选择*”是邪恶的问题是分开的,答案分为两半。

    当执行“select *”时,数据库服务器没有义务以任何特定的顺序返回列,事实上每次都可以合理地以不同的顺序返回列,尽管几乎没有数据库这样做。

    问题是,当典型的开发人员观察到返回的列似乎是一致的顺序时,假设列将始终按照该顺序,然后您就可以编写代码了无根据的假设,只是等待失败。更糟糕的是,这种失败可能不是致命的,但可能只涉及使用出生年份代替帐户余额

    “Select *”的另一个问题与表所有权有关——在许多大公司中,DBA 控制架构,并根据主要系统的要求进行更改。如果您的工具正在执行“select *”,那么您只会获得当前列 - 如果 DBA 删除了您需要的冗余列,您不会收到任何错误,并且您的代码可能会出现错误,导致各种损坏。通过明确请求您需要的字段,您可以确保您的系统会中断而不是处理错误的信息。

    【讨论】:

    • Bevan - 具体来说,“对象”具有不同的“形状”是什么意思?比如,如果语言是 Java/C#/C++,是否只有一个“MyThing 类”但有时它的 GetSomeStringAttr() 返回 null?
    • 在我看来,对象只是最终对特定上下文有明确定义;并且应用程序中的不同上下文可能需要针对不同上下文的不同形状(属性和方法集)。
    • 我指的是好的设计最终会导致对象和表格的结果完全不同。一个简单的例子:一个对象可能以属性 Date 结尾,其中表包含 Date、day、month、year、dayOfWeek、dayOfCalendarYear 和 dayOfFinancialYear 用于报告目的。
    【解决方案5】:

    我不知道你为什么想要一个部分水合的物体。给定具有名称、地址、ID 属性的客户类。我希望他们都创建一个完全填充的 Customer 对象。

    通过大多数 ORM 访问时,可以延迟加载名为 Orders 的客户列表。无论如何,NHibernate 都允许您对其他对象进行投影。因此,如果您说的是一个显示 ID 和名称的简单客户列表,您可以创建一个 CustomerListDisplay 类型的对象并将您的 HQL 查询投影到该对象集中,并且只从数据库中获取您需要的列。

    朋友不要让朋友过早优化。充分水合您的对象,延迟加载它的关联。然后分析您的应用程序以查找问题并优化问题区域。

    【讨论】:

    • 在您只对列的子集感兴趣并且不想支付获取所有列的性能损失的情况下,一个部分水合的对象。
    • 通常获取所有列的“性能影响”是微不足道的。重复往返数据库对性能的影响非常显着。延迟加载只会在某些情况下有所帮助,它不是灵丹妙药。
    • @gbjbaanb :这取决于关系层次结构的深度。它可能不仅仅是单个表中的列,而是与其他表的一系列连接。
    • 是的,但是当您开始谈论“关系层次结构”时,您已经超越了“完全水合您的对象”并进入了“延迟加载其关联”,除非您专门谈论深表-每个子类的继承方案,但我见过的大多数 ORM 似乎都不鼓励使用继承......
    【解决方案6】:

    甚至 ORM 也需要通过使用延迟加载等来避免 SELECT * 有效。

    是的,如果您不使用所有数据,SELECT * 通常是个坏主意。

    那么,您是否有不同种类的 MyThing 对象,每列组合一个? – Corey Trager(11 月 15 日 0:37)

    不,我有只读摘要对象(仅包含重要信息),用于查找和大量集合等内容,并根据需要将这些对象转换为完全水合的对象。 – Cade Roux(11 月 15 日 1:22)

    【讨论】:

    • 那么,您是否有不同种类的 MyThing 对象,每列组合一个?
    • 不,我有只读的摘要对象(仅包含重要信息),用于查找和大量集合之类的内容,并根据需要将它们转换为完全水合的对象。
    • +1 回复评论 Cade,关于摘要对象的这一点应该在答案中。 :)
    【解决方案7】:

    您描述的案例很好地说明了 ORM 不是灵丹妙药。数据库主要通过 SQL 提供对其数据的灵活、基于需求的访问。作为开发人员,我可以根据需要轻松简单地获取所有数据(SELECT *)或部分数据(SELECT COL1、COL2)。接手该项目的任何其他开发人员都将很容易理解我执行此操作的机制。

    为了从 ORM 中获得同样的灵活性,需要做更多的工作(无论是您还是 ORM 开发人员),才能让您回到可以获取全部或部分内容的底层根据需要从数据库中提取列(请参阅上面的优秀答案以了解一些问题)。所有这些额外的东西只是更多可能失败的东西,使得 ORM 系统本质上不如直接的 SQL 调用可靠。

    这并不是说您不应该使用 ORM(我的标准免责声明是所有设计选择都有成本和收益,而选择其中一个或另一个取决于)- 如果它适合您,请自行淘汰。我会说我真的不理解 ORM 的流行,因为它似乎为它的用户创造了很多额外的不有趣的工作。当(等待它)我需要从表中获取每一列时,我会坚持使用 SELECT *。

    【讨论】:

      【解决方案8】:

      ORM 通常不依赖 SELECT *,而是依赖更好的方法来查找列,如定义的数据映射文件(Hibernate、Hibernate 的变体和 Apache iBATIS 执行此操作)。通过查询数据库模式以获取表的列及其数据类型的列表,可以设置一些更自动化的东西。数据的填充方式是特定于您使用的特定 ORM 的,并且应该在那里进行详细记录。

      选择根本不使用的数据绝不是一个好主意,因为它会产生不必要的代码依赖,以后维护起来会很麻烦。对于处理类内部的数据,事情有点复杂。

      一个简短的规则是始终获取该类默认存储的所有数据。在大多数情况下,少量的开销不会产生太大的影响,因此您的主要目标是减少维护开销。稍后,当您对代码进行性能分析,并且有理由相信它可能会从调整行为中受益时,就是时候进行了。

      如果我看到一个 ORM 做出 SELECT * 语句,无论是明显的还是隐藏的,那么我会寻找其他地方来满足我的数据库集成需求。

      【讨论】:

      • 我没有发现您的回答解决了这个问题。即便如此 ORM 可能会使用方法来了解从数据库请求的数据,但您如何在没有 SELECT 语句的情况下最终填充您的 ORM 对象模型,无论它是准备好的语句还是其他什么?
      • 如何将数据加载到类中是 ORM 的特定于实现的细节。例如,在 iBATIS 中,它具有 XML 文件,其中包含 SQL 语句或过程、列数据类型以及列到类的映射,以指定哪一列与类中的哪个特定变量匹配。
      • @Florin,我理解你。 @doktaru,我不明白你。忘记问题的“选择*”部分。相反,请解释 MyThing 有时是完全水合,有时是部分水合时的行为。
      • 对我来说,本质区别在于是类驱动接口,还是表驱动接口。如果该类需要该表还没有的列,或者该表可能需要调整(或连接或..)该类不应该处理它不需要或不知道的列。
      【解决方案9】:

      SELECT * 还不错。你有没有问过谁认为它不好“为什么?”。

      【讨论】:

      • 我会问自己:select * 不好吗?是的,如果您只需要 A 列和 B 列,也不要支付获取 C 和 D 列的费用。只获取您需要的内容。
      • 是的,但这并不是普遍的“坏”。在某些情况下它是“坏的”!如果你以后想要 C 和 D,你不应该得到它们并缓存它们吗?没有一个案例是经过切割和干燥的,所以我们当然不能说“Select * is bad”。我们可以说“选择 * 有时不好”
      • 获取 C 和 D 的最坏后果是,追随你的人不知道你打算如何处理它们。换句话说,这也违反了 YAGNI 原则。
      • select * 在存储过程中不好,因为它会阻止服务器预编译执行计划。
      【解决方案10】:

      SELECT * 强烈表明您对应用程序及其模块的范围没有设计控制权。清理他人工作的主要困难之一是,其中有一些无用的东西,但没有说明需要和使用什么,什么不是。

      应用程序中的每条数据和代码都应该有一个目的,并且应该指定目的,或者容易检测到。

      我们都知道并且鄙视那些不太担心事情为什么会起作用的程序员,他们只是喜欢尝试一些东西,直到预期的事情发生并为下一个人关闭它。 SELECT * 是一个非常好的方法。

      【讨论】:

        【解决方案11】:

        如果您觉得需要将所有内容封装在一个对象中,但需要包含表中内容的一小部分,请定义您自己的类。编写直接 sql(在 ORM 内或不使用 - 大多数允许直接 sql 规避限制)并用结果填充您的对象。

        但是,在大多数情况下,我只会使用表的 ORM 表示,除非分析告诉我不要这样做。

        【讨论】:

          【解决方案12】:

          如果您使用查询缓存选择 * 可能会很好。如果您每次点击表时都选择不同的列分类,则可能只是为所有这些查询获取缓存的 select *。

          我认为您混淆了 ORM 的目的。 ORM 旨在将域模型或类似的映射到数据库中的表或某些数据存储约定。这并不是为了让您的应用程序的计算效率更高,甚至不是预期的。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-05-18
            相关资源
            最近更新 更多