The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:
TL;DR ORM是一个糟糕的反模式,违反了面向对象编程的所有原则,将对象分解并转化为愚笨和被动的数据包。在任何应用程序中都没有理由存在ORM,无论是小型Web应用程序还是具有数千个表和对它们进行CRUD操作的企业级系统。替代方案是能够使用SQL的对象。
对象关系映射(ORM)是一种从面向对象语言(例如Java)访问关系数据库的技术(也称为设计模式)。几乎每种语言都有多种ORM的实现,例如:Java的Hibernate,Ruby on Rails的ActiveRecord,PHP的Doctrine,以及Python的SQLAlchemy。在Java中,ORM设计甚至被标准化为JPA。
首先,让我们通过示例来看看ORM是如何工作的。我们使用Java,PostgreSQL和Hibernate。假设我们在数据库中有一个名为post
的单个表:
现在我们希望从我们的Java应用程序中CRUD操作这个表格(CRUD代表创建、读取、更新和删除)。首先,我们应该创建一个Post
类(对不起,它很长,但这是我能做的最好的)。
在使用Hibernate之前,我们必须创建一个会话工厂。
这个工厂将在我们每次想要操作Post
对象时提供一个”会话”。每次与会话有关的操作都应该放在这个代码块中:
当会话准备好时,我们可以通过以下方式从数据库表中获取所有帖子的列表:
我认为这里的情况很清楚。Hibernate是一个庞大而强大的引擎,它与数据库建立连接,执行必要的SQL SELECT
请求,并检索数据。然后它创建Post
类的实例,并将其填充到数据中。当对象传递给我们时,它已经填充了数据,我们应该使用getter方法来提取它们,就像我们在上面使用getTitle()
一样。
当我们想要执行反向操作并将对象发送到数据库时,我们做的一切都是相同的,只是顺序相反。我们创建Post
类的实例,将其填充数据,并要求Hibernate保存它:
这就是几乎每个ORM的工作原理。基本原则始终相同—ORM对象是带有数据的贫血封装。我们与ORM框架进行交互,而框架与数据库进行交互。对象只帮助我们将请求发送给ORM框架并理解其响应。除了getter和setter之外,对象没有其他方法。它们甚至不知道它们来自哪个数据库。
这就是对象关系映射的工作原理。
你可能会问,它有什么问题?一切都有问题!
严肃地说,出了什么问题?Hibernate已经是Java中最受欢迎的库之一超过10年了。全球几乎每个SQL密集型应用程序都在使用它。每个Java教程都会提到Hibernate(或者其他ORM如TopLink或OpenJPA)用于与数据库连接的应用程序。它是一个标准的事实上,但我还是说它是错的?是的。
我声称ORM背后的整个理念是错误的。它的发明也许是空引用之后面向对象编程的第二个重大错误。
实际上,我并不是唯一一个这样说的人,肯定也不是第一个。关于这个主题已经由非常受人尊敬的作者发表了很多文章,包括Martin Fowler的OrmHate(并不反对ORM,但还是值得一提),Jeff Atwood的Object-Relational Mapping Is the Vietnam of Computer Science,Ted Neward的The Vietnam of Computer Science,Laurie Voss的ORM Is an Anti-Pattern,以及其他许多人的观点。
然而,我的论点与他们说的不同。尽管他们的理由是实际和有效的,比如“ORM很慢”或者“数据库升级很困难”,但他们忽略了主要观点。你可以在Bozhidar Bozhanov的ORM Haters Don’t Get It博文中看到对这些实际论点的很好的回答。
主要观点是,ORM不是将数据库交互封装在对象内部,而是将其抽离开来,从而破坏了一个完整和内聚的生物体。对象的一部分保留数据,而另一部分在ORM引擎(会话工厂)内部实现,知道如何处理这些数据并将其传输到关系数据库中。看看这张图片,它说明了ORM在做什么。
作为一名帖子的读者,我必须处理两个组件:1)ORM和2)返回给我的“截断”的对象。我要交互的行为应该通过面向对象的单个入口点提供。在ORM的情况下,我通过两个入口点获得这个行为-ORM引擎和我们甚至不能称之为对象的“东西”。
由于这种对面向对象范式的可怕和冒犯性违反,我们已经有了许多在受人尊敬的出版物中提到过的实际问题。我只能再添加一些。
SQL并没有被隐藏。ORM的用户必须使用SQL(或其方言,如HQL)。看看上面的例子,我们调用session.createQuery("FROM Post")
来获取所有的帖子。尽管它不是SQL,但它非常类似。因此,关系模型并没有封装在对象内部,而是暴露给整个应用程序。每个人,每个对象都不可避免地必须处理关系模型以获取或保存东西。因此,ORM并没有隐藏和包装SQL,而是把整个应用程序污染了。
难以测试。当某个对象正在处理帖子列表时,它需要处理一个SessionFactory
实例。我们如何模拟这个依赖关系?我们必须创建一个它的模拟对象吗?这个任务有多复杂?看看上面的代码,你就会意识到那个单元测试将会是多么冗长和繁琐。相反,我们可以编写集成测试,并将整个应用程序连接到测试版本的PostgreSQL。在这种情况下,不需要模拟SessionFactory
,但这样的测试将会相当慢,更重要的是,我们与数据库无关的对象将会被测试针对数据库实例。这是一个糟糕的设计。
让我再重申一遍。ORM的实际问题只是后果。根本的缺点是ORM将对象分离开来,严重和冒犯性地违反了对象的本质思想。
什么是替代方案?让我以例子来展示给你。我们试着用我的方式设计那个类,Post
。我们需要将它拆分为两个类:Post
和Posts
,单数和复数形式。我在之前的一篇文章中已经提到过,一个好的对象总是对现实生活实体的抽象。以下是这个原则在实践中的运用。我们有两个实体:数据库表和表行。因此,我们将创建两个类;Posts
将代表表,而Post
将代表行。
正如我在那篇文章中也提到的,每个对象都应该按照合同工作并实现一个接口。让我们从两个接口开始设计。当然,我们的对象将是不可变的。以下是Posts
的样子:
这是一个Post
的样子:
以下是我们如何列出数据库表中的所有帖子:
以下是我们创建新帖子的步骤:
如你所见,我们现在有真正的对象了。它们负责所有操作,完美地隐藏了它们的实现细节。没有事务、会话或工厂。我们甚至不知道这些对象是真的在与PostgreSQL通信还是它们将所有数据保存在文本文件中。我们只需要从“Posts”中获得列出所有帖子和创建新帖子的能力。实现细节完全被隐藏了起来。现在让我们看看如何实现这两个类。
我将使用jcabi-jdbc作为JDBC包装器,但你可以使用其他类似的工具,比如jOOQ,或者如果你喜欢的话,直接使用纯JDBC也可以。这并不重要。重要的是你的数据库交互被封装在对象内部。让我们从“Posts”开始,在类“PgPosts”中实现它(“pg”代表PostgreSQL)。
接下来,让我们在PgPost
类中实现Post
接口。
这是使用我们刚刚创建的类进行完整数据库交互场景的样子。
你可以在这里看到一个完整的实际示例。这是一个开源的Web应用程序,使用与上面解释的相同方法与PostgreSQL一起工作,即使用SQL语言进行操作的对象。
我能听到你尖叫:“性能呢?”在上面的那个脚本中,我们对数据库进行了许多多余的往返。首先,我们使用SELECT id
检索帖子的ID,然后为了获取它们的标题,我们为每个帖子额外调用了SELECT title
。这是低效的,或者简单地说,太慢了。
不用担心;这是面向对象编程,意味着它是灵活的!让我们创建一个PgPost
的装饰器,在其构造函数中接受所有数据并将其内部缓存,永久保存下来。
请注意:这个装饰器对于PostgreSQL或JDBC一无所知。它只是装饰一个类型为Post
的对象,并预先缓存日期和标题。与往常一样,这个装饰器也是不可变的。
现在让我们创建另一个Posts
的实现,它将返回“常量”对象:
现在,这个新类的iterate()
方法返回的所有帖子都预先加载了日期和标题,只需进行一次数据库请求。
通过使用装饰器和多个实现相同接口的类,您可以组合任何所需的功能。最重要的是,在扩展功能的同时,设计的复杂性没有增加,因为类的大小没有增加。相反,我们引入了保持内聚性和稳定性的新类,因为它们很小。
每个对象应该处理自己的事务,并以与SELECT
或INSERT
查询相同的方式封装它们。这将导致嵌套事务,只要数据库服务器支持它们,这是完全可以的。如果没有这样的支持,可以创建一个会话范围的事务对象,该对象将接受一个“可调用”类。例如:
然后,当您想要将几个对象操作包装在一个事务中时,请按照以下方式进行:
这段代码将创建一个新帖子并对其发表评论。如果其中一个调用失败,整个事务将被回滚。
这种方法对我来说看起来很面向对象。我称之为“会说SQL的对象”,因为它们知道如何与数据库服务器进行SQL通信。这是它们的技能,完美地封装在它们的边界内。
Translated by ChatGPT gpt-3.5-turbo/42 on 2024-01-09 at 18:03