Orms教会我的:只需学习SQL(2014)

2020-10-21 14:54:08

我得出的结论是,对我来说,ORM弊大于利。简而言之,它们可以用来很好地增强与SQLIN程序的合作,但它们不应该取代它。

一些背景知识:在过去的30个月里,我一直在使用必须与Postgres接口的代码,在某种程度上,还必须与SQLite接口。其中大部分是SQLAlChemy(我很喜欢)和Hibernate(我不喜欢)。我使用过现有的代码和数据模型,也设计过自己的代码和数据模型。大多数数据是基于事件的存储(“时间线”),重点放在创建报告上。

关于对象/关系ImpedanceMismatch的文章已经很多了。除非你亲身经历过,否则很难欣赏它。纽瓦德在他著名的文章中提出了许多令人信服的理由,解释了奥姆斯为什么会陷入泥潭。根据我的经验,我必须直接处理很多问题:实体身份问题、双重模式问题、数据检索机制问题和部分对象问题。我想简单谈谈我在这些问题上的经验,并加上我自己的一个。

也许我遇到的ORMS中最具颠覆性的问题是“属性记录”或“宽表”,即不断累积属性的表。尽管我很想避免它,但有时它是必要的(尽管像Postgres的hstore这样的东西可以帮助我)。例如,客户可能会向您提供大量数据,他们希望将这些数据附加到基于各种业务逻辑的报告中。此外,您对这些数据没有太多的洞察力;您只是在胡乱摆弄它。

在数据库中,这本身并不是一件可怕的事情。有了ORM,它就成了真正的痛点。具体地说,问题开始出现在任何直接使用实体创建查询的查询中。在项目的早期,您可能有一个类似于Hibernate的查询。

当foo有五个属性时,这可能很好,但是当它有一百个属性时,它就变成了一个数据消防软管。这等同于使用SELECT*,它通常表达的信息比预期的要多。然而,ORM鼓励这种使用,并且经常使编写精确的预测变得非常容易,就像它们在SQL中一样。(我通过添加适当的投影优化了这类查询,并将运行时间从几分钟减少到几秒;所有时间都花在了将数据库行转换为Java对象上。)。

这导致了另一个糟糕的经历:有害地使用外键。在我使用过的ORM中,类之间的链接在数据模型中表示为外键,如果没有仔细配置,在检索对象时会导致大量连接。(在我的工作中,最近对一个这样的表进行了计数,结果产生了600多个属性和14个连接来访问单个对象,使用首选查询方法。)。

属性爬行和过度使用外键告诉我,为了有效地使用ORM,您仍然需要了解SQL。与ORMS的争用之处在于,如果您需要了解SQL,只需使用SQL,因为它不需要知道如何将非SQL转换为SQL。

当您试图使用ORM实际编写查询时,了解如何编写SQL变得更加重要。当效率令人担忧时,这一点尤为重要。

在我看来,除非您有一个非常简单的数据模型(也就是说,您从来不做连接),否则您将竭尽全力计算出如何让ORM生成高效运行的SQL。大多数情况下,它比实际的SQL更加模糊。

如果您选择保持查询简单,那么您最终将在代码中完成大量工作,而这些工作可以在数据库中更快地完成。窗口函数是相对高级的SQL,用ORM编写起来很痛苦。不将它们写入查询可能意味着您会将大量额外数据从数据库传输到您的应用程序。

在这些情况下,我选择使用模板系统编写查询,并使用ORM描述表。通过直接使用SQL,我获得了表的应用层描述的便利性。这比我到目前为止用过的任何东西麻烦都要少得多。

这似乎是那些不可避免的裁员之一。如果你试图摆脱它,你只会制造更多的问题或增加太多的复杂性。

问题在于,您最终在两个位置拥有数据定义:数据库和应用程序。如果将定义完全保留在应用程序中,则最终必须使用ORM代码编写SQL DataDefinition Language(DDL),这与在ORM中编写高级查询是相同的。如果您将其保存在数据库中,则为了方便和防止过多的“字符串键入”,您可能需要在应用程序中使用表示形式。

我更喜欢将数据定义保存在数据库中,然后将其读取到应用程序中。它不能解决问题,但它使问题更易于管理。我发现用反射技术来获取数据定义是不值得的,我不得不在两个地方管理数据定义的不稳定性。

但是,该死的迁移问题是真正的刺痛:更改模型在应用程序中没什么大不了的,但在数据库中却是真正的痛苦。毕竟,数据库是持久的,而应用程序数据不是。ORM在这里只是个障碍,因为它们根本不能帮助管理数据迁移。我的工作原则是数据库的数据定义不应该在应用程序中操作。相反,应该操作查询结果。也就是说,查询是您对数据库的API。因此,我不再考虑对象,而是考虑具有返回类型的函数。

因此,人们不得不问,除了方便进行查询之外,您是否应该使用ORM呢?

在使用ORM时,处理实体标识是您必须始终牢记的事情之一,它迫使您为两个系统编写代码,而只有一个系统的可表达性。

当您有外键时,您使用标识符引用相关标识。在您的应用程序中,“标识符”有多种含义,但通常是内存位置(指针)。在数据库中,它是对象本身的状态。这两件事并不能很好地相处,因为您实际上只能在数据库(您正在处理的数据的最终目的地)中使用数据库标识符。

这导致必须通过手动刷新缓存或执行部分提交来操作ORM来获取数据库标识符,以获取实际的数据库标识符。

我甚至不能将其称为泄漏抽象,因为“泄漏”意味着相对于源代码有少量内容泄漏。

纽瓦德提到的一些事情是开发人员需要处理事务。事务是动态限定作用域的,这在编程语言中是一个强大但通常被忽略的概念,因为如果过度使用它们会导致融合。这导致了大量带有异常处理程序的boilerplatecode,并仔细考虑了事务边界应该发生在哪里。它还使您可以将会话对象传递给可能必须与数据库通信的任何函数/方法。

由于应用程序对基于时间的上下文的依赖,事务的概念很难转化为应用程序。正如前面提到的,动态作用域是在程序中使用它的一种方式,但它与主导范式词汇化作用域不一致。因此,在编写使用数据库的代码时,您必须非常小心地了解事务的“何时”,这可能会使模块化变得棘手(“这里有一个仅在某些上下文中有效的有用函数”)。

在这一点上,我开始质疑直接拒绝存储过程背后的智慧。这听起来很异端,但它可能适用于我的用例。(嘿,随着“devops”的出现,开发人员和数据库管理员之间的分歧基本上是不存在的。)

我发现自己将数据库看作是另一种具有API的数据类型:查询。查询返回某种类型的值,这些值在程序中表示为某个对象。通过不再将应用程序中的对象视为要存储在数据库中的对象(ORMS的存在理由),而将数据库视为(大型且复杂的)数据类型,我发现在应用程序中使用数据库要简单得多。想知道为什么我没有早点看到。

(应该明确的是,我并不是说所有的应用程序都应该这样处理数据库。我要说的是,根据我正在处理的数据,这符合我的用例。)。

不管我是否发现存储过程实际上并不那么邪恶,或者我是否继续使用模板化的SQL,我都知道一件事:我不会落入“ORMS使事情变得容易”的陷阱。它们是表示数据定义的一种可以接受的方式,但是编写查询的方式很差,存储对象状态的方式也很糟糕。如果您使用的是RDBMS,请咬紧牙关学习SQL。