为啥所有 Active Record 都讨厌? [关闭]

Posted

技术标签:

【中文标题】为啥所有 Active Record 都讨厌? [关闭]【英文标题】:Why all the Active Record hate? [closed]为什么所有 Active Record 都讨厌? [关闭] 【发布时间】:2010-09-05 16:35:14 【问题描述】:

随着我对 OOP 的了解越来越多,并开始实施各种设计模式,我不断回想起人们讨厌 Active Record 的案例。

通常,人们说它不能很好地扩展(以 Twitter 为例)——但没有人真正解释 为什么它不能很好地扩展;和/或如何在没有缺点的情况下实现 AR 的优点(通过类似但不同的模式?)

希望这不会变成一场关于设计模式的圣战——我只想知道****具体**** Active Record 出了什么问题。

如果不能很好地扩展,为什么不呢?

还有什么问题?

【问题讨论】:

我想总的来说,很多对设计模式的厌恶和厌恶都与错误的使用有关。人们倾向于在错误的环境中过度使用和使用它们,最终得到比原来更复杂的解决方案 Ruby 的 Active Record 实现更像是一个 ORM。 有一种社会现象是为了获得欣赏,更多的认可,看起来更聪明和最前沿,人们倾向于机械地重复任何对任何现行标准、模型的否定的炒作,广泛采用的技术,将其与下一波的革命性进展相混淆。 【参考方案1】:

有 ActiveRecord the Design Pattern 和 ActiveRecord the Rails ORM Library,还有大量针对 .NET 和其他语言的仿冒品。

这些都是不同的东西。他们大多遵循该设计模式,但以许多不同的方式对其进行扩展和修改,因此在有人说“ActiveRecord 糟透了”之前,需要先说“哪个 ActiveRecord,有堆?”来限定它。

我只熟悉 Rails 的 ActiveRecord,我会尝试解决在使用它时提出的所有投诉。

@BlaM

我在 Active Records 中看到的问题是,它总是只有一张表

代码:

class Person
    belongs_to :company
end
people = Person.find(:all, :include => :company )

这将使用LEFT JOIN companies on companies.id = person.company_id 生成 SQL,并自动生成关联的 Company 对象,因此您可以执行 people.first.company 并且不需要访问数据库,因为数据已经存在。

@pix0r

Active Record 的固有问题是自动生成并执行数据库查询以填充对象和修改数据库记录

代码:

person = Person.find_by_sql("giant complicated sql query")

不鼓励这样做,因为它很丑陋,但对于您只是简单地需要编写原始 SQL 的情况,它很容易完成。

@蒂姆·沙利文

...并且您选择了模型的几个实例,您基本上是在执行“select * from ...”

代码:

people = Person.find(:all, :select=>'name, id')

这只会从数据库中选择名称和 ID 列,映射对象中的所有其他“属性”都将为零,除非您手动重新加载该对象,等等。

【讨论】:

强大!我不知道那个特定的功能。又一个支持 AR 的论点让我加入我的武器库。 加入超越了 Active Record 模式。 "Person.find_by_sql" 根本不是 Active Record 模式。它的“活动记录”让我失望了,所以我需要手动修补它。【参考方案2】:

我一直发现 ActiveRecord 适用于模型相对扁平的基于 CRUD 的快速应用程序(例如,没有很多类层次结构)。但是,对于具有复杂 OO 层次结构的应用程序,DataMapper 可能是更好的解决方案。虽然 ActiveRecord 假设您的表和数据对象之间的比率为 1:1,但这种关系对于更复杂的域变得难以处理。在他的book on patterns 中,Martin Fowler 指出 ActiveRecord 在您的模型相当复杂的情况下容易崩溃,并建议使用DataMapper 作为替代方案。

我发现这在实践中是正确的。在您的域中有很多继承的情况下,将继承映射到 RDBMS 比映射关联或组合更难。

我这样做的方式是让控制器通过这些 DataMapper(或“服务层”)类访问“域”对象。这些并不直接反映数据库,而是充当某些现实世界对象的 OO 表示。假设您的域中有一个 User 类,并且需要在检索该 User 对象时已经加载了对其他对象的引用或其他对象的集合。数据可能来自许多不同的表,而 ActiveRecord 模式可能会使其变得非常困难。

您的控制器代码不是直接加载用户对象并使用 ActiveRecord 样式 API 访问数据,而是通过调用 UserMapper.getUser() 方法的 API 来检索用户对象。正是这个映射器负责从它们各自的表中加载任何关联的对象,并将完成的用户“域”对象返回给调用者。

本质上,您只是添加了另一层抽象来使代码更易于管理。无论您的 DataMapper 类是否包含原始的自定义 SQL,还是对数据抽象层 API 的调用,甚至是自己访问 ActiveRecord 模式,对于接收漂亮的填充用户对象的控制器代码来说都无关紧要。

不管怎样,我就是这样做的。

【讨论】:

@JoãoBragança - 也许不是讽刺的评论,你实际上可以解释当一个人的数据被分片时发生的困难 - 所以我们其他人可以学到一些东西:)【参考方案3】:

我认为人们“讨厌” ActiveRecord 的原因和它的“错误”之间可能有一组非常不同的原因。

在讨厌的问题上,对任何与 Rails 相关的东西都有很多毒液。至于它有什么问题,它很可能就像所有技术一样,在某些情况下它是一个不错的选择,也存在有更好选择的情况。在我的经验中,如果你不能利用 Rails ActiveRecord 的大部分功能,那就是数据库的结构很糟糕。如果您在没有主键的情况下访问数据,并且违反了第一范式,其中需要大量存储过程来访问数据,那么您最好使用更多只是 SQL 包装器的东西。如果您的数据库结构相对较好,ActiveRecord 可以让您利用这一点。

使用代码 sn-p rejoinder 来添加回复评论者说 ActiveRecord 中的事情很困难的主题

@Sam McAfee 假设您的域中有一个 User 类,并且需要在检索该 User 对象时已经加载了对其他对象的引用或集合。数据可能来自许多不同的表,而 ActiveRecord 模式可能会使其变得非常困难。

user = User.find(id, :include => ["posts", "comments"])
first_post = user.posts.first
first_comment = user.comments.first

通过使用包含选项,ActiveRecord 允许您覆盖默认的延迟加载行为。

【讨论】:

【参考方案4】:

我的回答又长又迟,甚至不完整,但很好地解释了为什么我讨厌这种模式、观点甚至一些情绪:

1) 短版:Active Record 在数据库和应用程序代码之间创建一个“强绑定”的“thin layer”。这没有解决任何逻辑问题,没有任何问题,根本没有问题。恕我直言,它不提供任何值,除了一些 语法糖程序员使用(然后可能使用“对象语法”来访问关系数据库中存在的一些数据)。为程序员创造一些舒适感的努力应该(恕我直言......)更好地投资于低级数据库访问工具,例如simple,easy,plain hash_map get_record( string id_value, string table_name, string id_column_name="id" ) 和类似方法的一些变体(当然,概念和优雅因使用的语言而有很大差异)。

2) 长版本:在我对事物进行“概念控制”的任何数据库驱动项目中,我避免使用 AR,这很好。我通常构建一个分层架构(你迟早会把你的软件分层,至少在大中型项目中是这样):

A1) 数据库本身、表、关系,如果 DBMS 允许的话,甚至是一些逻辑(mysql 现在也成熟了)

A2) 很多时候,不仅仅是一个数据存储:文件系统(数据库中的 blob 并不总是一个好的决定......),遗留系统(想象自己“如何”访问它们,可能有很多种类。 . 但这不是重点...)

B) 数据库访问层(在这个级别,工具方法,帮助轻松访问数据库中的数据非常受欢迎,但 AR 在这里没有提供任何价值,除了一些语法糖)

C) 应用对象层:“应用对象”有时是数据库中表格的简单行,但大多数时候它们无论如何都是复合对象,并且附加了一些更高的逻辑,所以要花费时间在这个级别的 AR 对象中显然是无用的,浪费了宝贵的编码人员时间,因为这些对象的“真正价值”,“更高的逻辑”需要在 AR 对象之上实现,无论如何 - 有和没有 AR !而且,例如,为什么要抽象“日志条目对象”?应用程序逻辑代码编写它们,但它应该具有更新或删除它们的能力吗?听起来很傻,App::Log("I am a log message")le=new LogEntry(); le.time=now(); le.text="I am a log message"; le.Insert(); 更容易使用。例如:在您的应用程序的日志视图中使用“日志条目对象”将适用于 100、1000 甚至 10000 条日志行,但迟早您将不得不优化 - 我敢打赌,在大多数情况下,您只会在你的应用程序逻辑中使用那个漂亮的小 SQL SELECT 语句(这完全打破了 AR 的想法......),而不是将那个小语句包装在刚性固定的 AR 想法框架中,并使用大量代码包装和隐藏它。您浪费在编写和/或构建 AR 代码上的时间本可以投入到一个更智能的界面上,用于阅读日志条目列表(很多很多方面,没有限制)。编码人员应该敢于发明新的抽象以实现适合预期应用程序的应用程序逻辑,并且不要愚蠢地重新实现愚蠢的模式,这听起来不错! p>

D) 应用程序逻辑 - 实现交互对象以及创建、删除和列出(!)应用程序逻辑对象的逻辑(不,这些任务很少应该锚定在应用程序逻辑对象本身:纸上是否你的办公桌会告诉你办公室里所有其他工作表的名称和位置?忘记列出对象的“静态”方法,那是愚蠢的,这是为了让人类的思维方式适应 [some-not-all-AR-框架式-]AR 思维)

E) 用户界面 - 好吧,我将在以下几行中写下非常非常主观的内容,但根据我的经验,基于 AR 构建的项目通常会忽略应用程序的 UI 部分 - 时间被浪费在了创建模糊的抽象。最后,这样的应用程序浪费了编码人员的大量时间,感觉就像是编码人员为编码人员提供的应用程序,内部和外部都倾向于技术。编码员感觉很好(努力工作终于完成,一切都完成并正确,根据纸上的概念......),客户“只需要了解它需要那样”,因为那是“专业”......好吧,对不起,我离题了;-)

好吧,诚然,这一切都是主观的,但这是我的经验(Ruby on Rails 除外,它可能会有所不同,而且我对这种方法的实践经验为零)。

在付费项目中,我经常听到需要从创建一些“活动记录”对象作为更高级别应用程序逻辑的构建块开始。根据我的经验,这明显经常是客户(在大多数情况下是软件开发公司)没有好的概念、大视野、对产品应该做什么的概述的某种借口终于可以了。那些客户在僵化的框架中思考(“十年前的项目运行良好..”),他们可能会充实实体,他们可能会定义实体关系,他们可能会分解数据关系并定义基本的应用程序逻辑,但随后他们就停止了把它交给你,认为这就是你所需要的......他们往往缺乏完整的应用程序逻辑,用户界面,可用性等概念......他们缺乏大视野,他们缺乏对应用程序的热爱细节,他们希望你遵循 AR 的方式,因为.. 好吧,为什么,它在几年前的那个项目中起作用,它让人们忙碌而沉默?我不知道。但是“细节”将男人与男孩分开,或者..最初的广告口号是怎样的? ;-)

经过多年(十年积极开发经验),每当客户提到“活跃记录模式”时,我的警钟就会响起。我学会了尝试让他们回到那个重要的概念阶段,让他们三思而后行,尝试让他们展示他们的概念弱点,或者如果他们没有辨别力就完全避免他们(最后,你知道,一个还不知道自己想要什么的客户,甚至可能认为自己知道但不知道,或者试图免费将概念工作外化给我,这花费了我许多宝贵的时间、几天、几周和几个月的时间,生活是太短了……)。

所以,最后:这就是为什么我讨厌这种愚蠢的“主动记录模式”,我会并且会尽可能避免它。

编辑:我什至会称之为无模式。它不能解决任何问题(模式并不意味着创建语法糖)。它产生了许多问题:所有问题的根源(在此处的许多答案中都提到了......)是,它只是隐藏在模式接口后面的良好的旧的、开发良好且功能强大的 SQL定义极其有限。

这种模式用语法糖代替了灵活性!

想一想,AR 为你解决了哪些问题?

【讨论】:

它是一种数据源架构模式。也许您应该阅读 Fowler 的企业应用程序架构模式?在实际使用模式/ORM 并发现它在多大程度上简化了事情之前,我的想法与您类似。 我分享你的感受。当一个框架不支持复合键时,我闻到了一些不对劲......我在 SQLAlchemy 之前避免了任何类型的 ORM,我们经常在较低级别使用它,作为 SQL 生成器。它实现了Data Mapper,非常灵活。 自从两天以来我参与了一个使用“最先进”ORM 的项目,也许现在实现已经成熟(与几年前我使用的相比)。也许,我的想法会改变,我们会在三个月内看到:-) 项目完成了,你知道吗? ORM 仍然很糟糕,我在映射问题上浪费了很多时间,这些问题很容易以关系方式表达为一堆“面向对象的代码”。好吧,当然,ORM 提供了用一种 OOP+SQL-Mix 来表达查询的方法——当然是一种类似 OOP 的语法——但这比简单地编写 SQL 查询要花更多的时间。抽象泄露,OOP 之上的“OOPSQLExperiment”——允许用户以 OOP 语法编写 SQL 是有史以来最糟糕的想法。不,再也不会了。 多年来,我为所有内容编写了原始 SQL。 Rails AR 有时让我感到沮丧,对于被动查询,我几乎同意你的看法,但这就是它解决的问题:1) 使保存未通过验证的数据变得相当困难。 2)跟踪自上次持续以来内存中的变化。 3) 使用第 2 点编写合理的before_save 回调以保持记录内的一致性 4) 外部服务触发器的after_commit 挂钩。 5) 用于将 DDL 更改组织成变更集(迁移)的良好 DSL。 (那里仍然很痛苦,但是当 > 1 个开发人员时没有模式会更糟。)【参考方案5】:

有些消息让我感到困惑。 有些答案是“ORM”与“SQL”之类的。

事实上,AR 只是一种简化的编程模式,您可以在其中利用域对象编写数据库访问代码。

这些对象通常具有业务属性(bean 的属性)和一些行为(通常对这些属性起作用的方法)。

AR 只是对与数据库相关的任务说“向这些域对象添加一些方法”。

我不得不说,从我的观点和经验来看,我不喜欢这种模式。

乍一看,它听起来不错。一些现代 Java 工具(如 Spring Roo)使用这种模式。

对我来说,真正的问题在于 OOP 问题。 AR 模式迫使您以某种方式将对象的依赖项添加到基础设施对象。这些基础设施对象让领域对象通过 AR 建议的方法查询数据库。

我一直说,两个层次是项目成功的关键。服务层(业务逻辑驻留或可以通过某种远程技术导出,例如 Web 服务)和领域层。在我看来,如果我们为解析 AR 模式的领域层对象添加一些依赖项(并非真正需要),我们的领域对象将更难与其他层或(罕见的)外部应用程序共享。

AR 的 Spring Roo 实现很有趣,因为它不依赖于对象本身,而是依赖于一些 AspectJ 文件。但如果以后您不想使用 Roo 并且必须重构项目,AR 方法将直接在您的域对象中实现。

另一种观点。想象一下,我们不使用关系数据库来存储我们的对象。例如,假设应用程序将我们的域对象存储在 NoSQL 数据库中或仅存储在 XML 文件中。我们会在我们的领域对象中实现执行这些任务的方法吗?我不这么认为(例如,在 XM 的情况下,我们会将与 XML 相关的依赖项添加到我们的域对象中......我认为真的很难过)。那么为什么我们必须在领域对象中实现关系数据库方法,就像 Ar 模式所说的那样?

总而言之,AR 模式听起来更简单,适合小型和简单的应用程序。但是,当我们有复杂的大型应用程序时,我认为经典的分层架构是更好的方法。

【讨论】:

欢迎来到 SO。感谢您的评论,但 NullUserException 在 2011 年 12 月 17 日 1:17 关闭了这个问题,因为它没有建设性【参考方案6】:

问题是关于 Active 记录设计模式。不是一个形式 工具。

最初的问题是用 Rails 标记的,是指使用 Ruby on Rails 构建的 Twitter。 Rails 中的 ActiveRecord 框架是 Fowler 的 Active Record 设计模式的实现。

【讨论】:

【参考方案7】:

关于 Active Record 的投诉,我看到的主要内容是,当您围绕表格创建模型并选择模型的多个实例时,您基本上是在执行“select * from .. ."。这对于编辑记录或显示记录很好,但是如果您想显示数据库中所有联系人的城市列表,您可以执行“从...中选择城市”并且只获取城市.使用 Active Record 执行此操作需要您选择所有列,但只能使用 City。

当然,不同的实现会以不同的方式处理这个问题。不过,这是一个问题。

现在,您可以通过为您正在尝试做的特定事情创建一个新模型来解决这个问题,但有些人会争辩说这比收益更多。

我,我在挖掘 Active Record。 :-)

HTH

【讨论】:

“使用 Active Record 执行此操作需要您选择所有列,但仅使用 City。”指定一个 select 子句实际上非常容易。【参考方案8】:

虽然所有其他关于 SQL 优化的 cmets 肯定是有效的,但我对活动记录模式的主要抱怨是它通常会导致 impedance mismatch。我喜欢保持我的域干净并正确封装,而活动记录模式通常会破坏所有希望。

【讨论】:

ActiveRecord 实际上解决了阻抗不匹配问题,它允许您针对关系模式以面向对象的方式进行编码。 想详细说明一下?一般的共识是,根据定义,以关系数据库建模的对象不是面向对象的(因为关系数据库不围绕 OO 概念,例如继承和多态性)。 已知有三种方法可以将继承映射到关系模式。参考:castleproject.org/ActiveRecord/documentation/trunk/usersguide/… 我认为您将 Castle Active Record OSS 项目误认为是 Active Record 设计模式。最初的问题(和我的回答)是指设计模式。 Castle Active Record 项目包含一些东西来帮助 OO 开发,但模式本身没有。 我只是引用 Castle 作为参考。 RoR 的 ActiveRecord 仅实现单表继承 (martinfowler.com/eaaCatalog/singleTableInheritance.html),但正在考虑其他策略 (blog.zerosum.org/2007/2/16/…)【参考方案9】:

我喜欢 SubSonic 只做一栏的方式。 要么

DataBaseTable.GetList(DataBaseTable.Columns.ColumnYouWant)

,或者:

Query q = DataBaseTable.CreateQuery()
               .WHERE(DataBaseTable.Columns.ColumnToFilterOn,value);
q.SelectList = DataBaseTable.Columns.ColumnYouWant;
q.Load();

但在延迟加载方面,Linq 仍然是王者。

【讨论】:

【参考方案10】:

@布拉姆: 有时我刚刚为连接的结果实现了一个活动记录。不一定总是关系表 Active Record。为什么不是“Join 语句的结果” Active Record?

【讨论】:

【参考方案11】:

我要说的是 Active Record 作为一种设计模式,我没见过 ROR。

一些开发人员讨厌 Active Record,因为他们阅读了有关编写干净整洁代码的智能书籍,这些书籍指出 Active Record 违反了单一责任原则,违反了 DDD 规则,即域对象应该是持久无知的,以及这些书中的许多其他规则那种书。

Active Record 中的第二件事域对象往往与数据库是一对一的,这可能被认为是某些系统(主要是 n 层)的限制。

这只是抽象的东西,我还没有看到 ruby​​ on rails 实际实现这种模式。

【讨论】:

【参考方案12】:

我在 Active Records 中看到的问题是,它总是只有 一个 表。没关系,只要您真的只使用那一张表,但在大多数情况下,当您使用数据时,您会在某处进行某种联接。

是的,就性能而言,加入通常比根本不加入差,但加入 通常 优于“fake”join,先读取整个表A,然后使用获得的信息读取和过滤表B。

【讨论】:

@BlaM:你说得对。尽管我从未使用过 Active Record,但我使用过其他固定式 ORM 系统(尤其是 NHibernate),并且我有两个大抱怨:创建对象的愚蠢方法(即 .hbm.xml 文件,每个都得到编译到他们自己的程序集中)并且仅加载对象就会导致性能损失(当等效的 SQL 查询几乎不需要处理时,NHibernate 可以使单核 proc 执行一个根本不加载任何内容的查询几秒钟)。当然不是特定于 Active Record,但我发现大多数 ORM 系统(和类似 ORM 的系统)似乎 使用 hbm.xml 文件有很多替代方法。参见例如 NHibernate.Mapping.Attributes 和 fluent-nhibernate。 关于对象创建性能,我从来没有遇到过这样的性能问题,你可能想用探查器检查。 @mausch:不需要分析器。这是一个相当有名的问题。不知道它是否适用于最新版本(我还没有在我的工作中使用)。 ayende.com/Blog/archive/2007/10/26/… 在查找中使用 :joins 或 :includes IE Customer.find(:all, :include => :contacts, :conditions => "active = 1") 将执行 SQL 连接,而不是全表扫描。【参考方案13】:

ActiveRecord 的问题在于它自动为您生成的查询可能会导致性能问题。

您最终会采取一些不直观的技巧来优化查询,这让您想知道首先手动编写查询是否会更有效。

【讨论】:

【参考方案14】:

尝试做多对多的多态关系。没那么容易。尤其是当您不使用 STI 时。

【讨论】:

以上是关于为啥所有 Active Record 都讨厌? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

CodeIgniter:如果 Active Record 找不到任何数据,则返回 FALSE

如何通过Active Record计算数据库中的所有对象

为啥都是autoload,load_all!并要求所有在active_support.rb 中使用?

在 Ruby on Rails 4 中使用 Active Record 或 Squeel Gem 重写 SQL 查询

Active Record:如何读取具有许多其他对象的db对象?

Active Record 包括 where