“最后手段”或为什么需要数据库优先设计

在这篇很晚的文章中,我将解释为什么在我看来,在大多数情况下,为应用程序开发数据模型时,必须遵循数据库优先方法。 在项目开始发展后,您无需再经历“ Java(任何其他语言)优先”的方法,而是将您带到一条充满痛苦和磨难的漫长道路。


图片
Alan O'Rourke / Audience Stack 许可的 “太忙了,不能变得更好”。 原始图片


本文受到最近StackOverflow问题的启发


有趣的reddit讨论/ r / java/ r /编程


代码生成


令我惊讶的是,一小群用户似乎对jOOQ与源代码生成紧密相关的事实感到震惊。


尽管您可以随意使用jOOQ,但首选方法(根据文档)是从现有数据库架构开始,然后使用jOOQ生成必要的客户端类(与您的表相对应),然后编写类型安全的代码很容易查询这些表:


for (Record2<String, String> record : DSL.using(configuration) // ^^^^^^^^^^^^^^^^^^^^^^^ Type information derived from the // generated code referenced from the below SELECT clause .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME) // vvvvv ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ Generated names .from(ACTOR) .orderBy(1, 2)) { // ... } 

可以在程序集外部手动生成代码,也可以在每个程序集自动生成代码。 例如, 这种生成可以在安装Flyway迁移后立即发生 ,也可以手动或自动启动。


源代码生成


这些我不想在本文中讨论的代码生成方法有不同的理念,优点和缺点。 但是从本质上讲,生成的代码的含义是它是我们认为是一种“标准”(系统内部和外部)的Java表示形式。 从某种意义上说,编译器从源代码生成字节码,机器代码或其他源代码时会做同样的事情-结果,我们用另一种特定语言了解了我们的“标准”。


有很多这样的代码生成器。 例如, XJC可以从XSD或WSDL文件生成Java代码 。 原理总是相同的:


  • 有一些标准(外部或内部),例如规范,数据模型等。
  • 有必要用我们通常的编程语言来了解这个标准。

为了避免不必要的工作和不必要的错误, 生成此视图几乎总是有意义的。


类型提供者和注释处理


值得注意的是,jOOQ中另一种更现代的代码生成方法是类型提供程序( 如F#中所做的那样 ),其中代码是由编译器在编译时生成的,并且永远不会以原始形式存在。 Java中类似(但不太复杂)的工具是注释处理器,例如Lombok


在这两种情况下,一切都与普通代码生成相同,除了:


  • 您看不到生成的代码(也许对于很多人来说这是一个很大的好处吗?)
  • 您必须确保每次编译都可以使用“参考”。 在Lombok的情况下,这不会引起任何问题,而Lombok会直接注释源代码本身,在这种情况下为“标准”。 依赖于始终在线的实时连接的数据库模型要复杂一些。

代码生成有什么问题?


除了要手动还是自动生成代码这一棘手的问题之外,有些人还认为根本不需要生成代码。 我最常听到的原因是这种生成很难在CI / CD管道中实现。 是的,这是真的,因为 我们在创建和支持其他基础架构方面会产生开销,特别是如果您不熟悉所使用的工具(jOOQ,JAXB,Hibernate等)时。


如果研究代码生成器的开销太大,那么实际上将没有什么好处。 但这是唯一的反对。 在大多数其他情况下,手动编写代码(这是某种事物的模型的通常表示形式)绝对没有意义。


许多人声称他们没有时间这样做,因为 现在,您需要尽快推出另一个MVP。 他们将能够在以后的某个时间完成CI / CD管道的定稿。 在这种情况下,我通常会说:“您太忙了,无法变得更好。”


“但是Hibernate / JPA使Java的首次开发变得容易得多。”


是的,这是真的。 对于Hibernate用户而言,这既是喜悦,也是痛苦。 使用它,您可以简单地编写以下形式的几个对象:


 @Entity class Book { @Id int id; String title; } 

差不多完成了。 接下来,Hibernate将介绍有关如何在DDL和所需的SQL方言中定义此对象的整个例程:


 CREATE TABLE book ( id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, title VARCHAR(50), CONSTRAINT pk_book PRIMARY KEY (id) ); CREATE INDEX i_book_title ON book (title); 

这确实是快速开始开发的好方法-您只需要启动应用程序即可。


但是,并非所有事情都如此乐观。 仍然有很多问题:


  • Hibernate会生成我需要的主键名称吗?
  • 我将在“标题”字段上创建所需的索引吗?
  • 每个记录都会生成唯一的ID值吗?

好像没有。 但是,在开发项目时,您始终可以丢弃当前数据库,并通过向模型添加必要的注释来从头开始生成所有内容。
因此,Book类的最终形式将如下所示:


 @Entity @Table(name = "book", indexes = { @Index(name = "i_book_title", columnList = "title") }) class Book { @Id @GeneratedValue(strategy = IDENTITY) int id; String title; } 

但是稍后您会为此付费


迟早,您的应用程序将投入生产,并且所描述的方案将停止工作:


在一个真实的生活系统中,您不再只能接管和删除数据库,因为 数据在其中使用,可能会花费很多钱。

从现在开始,您需要为数据模型中的每个更改编写迁移脚本, 例如,使用Flyway 。 但是,您的客户班怎么办? 您可以手动修改它们(这将导致双重工作),也可以要求Hibernate生成它们(但是这种生成的结果满足期望的可能性有多大?)。 结果,您可能会遇到大问题。


一旦代码投入生产,几乎立即有必要尽快进行更正。


而且因为 在组装线中未内置数据库迁移的安装;您将必须手动安装此类补丁程序,后果自负。 没有足够的时间返回去做正确的事情。 仅将所有麻烦归咎于休眠。


相反,您可能从一开始就采取了截然不同的行动。 即,使用圆轮而不是方形轮。


首先进入数据库


数据模式参考和控制在您的DBMS办公室中。 数据库是唯一定义架构的地方,所有客户端都具有该架构的副本,反之亦然。 数据在数据库中,而不在客户端中,因此有意义的是提供对方案及其完整性的控制,使其精确地位于数据所在的位置。


这是古老的智慧,没有新事物。 主键和唯一键很好。 外键很漂亮。 在数据库方面检查约束是很棒的。 断言(最终实现时)很棒。


不仅如此。 例如,如果您使用的是Oracle,则可以指定:


  • 您的表在哪个表空间中?
  • 她拥有的PCTFREE是什么意思
  • 序列缓存的大小是多少?

在小型系统上,所有这些可能都不重要,但是在大型系统上,您无需遵循“大数据”的道路,直到从当前存储中挤出所有果汁。 我从未见过一个ORM(包括jOOQ)可以使用DBMS提供的全套DDL参数。 ORM仅提供一些工具来帮助您编写DDL。


最终,设计良好的模式只能使用特定于DBMS的DDL手动编写。 所有自动生成的DDL只是对此的一种近似。


客户模型呢?


如前所述,您将需要在客户端对数据库模式进行某种表示。 不用说,此视图必须与真实模型同步。 怎么做? 当然使用代码生成器。


所有数据库都通过良好的旧SQL提供对其元信息的访问。 因此,例如,您可以获取来自不同数据库的所有表的列表:


 -- H2, HSQLDB, MySQL, PostgreSQL, SQL Server SELECT table_schema, table_name FROM information_schema.tables -- DB2 SELECT tabschema, tabname FROM syscat.tables -- Oracle SELECT owner, table_name FROM all_tables -- SQLite SELECT name FROM sqlite_master -- Teradata SELECT databasename, tablename FROM dbc.tables 

调用特定JDBC驱动程序的DatabaseMetaData.getTables()方法时或在jOOQ-meta模块中执行的正是此类查询(以及对视图,实例化视图和表函数的类似查询)。


根据此类查询的结果,无论使用哪种数据访问技术,创建数据库模型的任何客户端表示都是相对容易的。


  • 如果使用JDBC或Spring,则可以创建一组String常量
  • 如果使用JPA,则可以自己创建对象
  • 如果使用jOOQ,则可以创建jOOQ元模型

根据您的数据访问API提供的功能数量(jOOQ,JPA或其他功能),生成的元模型可以真正丰富和完整。 例如, jOOQ 3.11中的隐式联接函数依赖于有关表之间外键关系的元信息


现在,对数据库模式的任何更改将自动导致客户端代码的更新。


假设您需要重命名表中的列:


 ALTER TABLE book RENAME COLUMN title TO book_title; 

您确定要两次执行此工作吗? 没办法 只需提交此DDL,运行构建并享用更新的对象:


 @Entity @Table(name = "book", indexes = { // Would you have thought of this? @Index(name = "i_book_title", columnList = "book_title") }) class Book { @Id @GeneratedValue(strategy = IDENTITY) int id; @Column("book_title") String **bookTitle**; } 

同样,不需要每次都编译接收到的客户端(至少直到下一次数据库模式更改时才编译),这已经是一大优势了!
大多数DDL更改也是语义更改,而不仅仅是语法更改。 因此,很高兴在生成的客户端代码中看到受影响的数据库中的最新更改到底是什么。


真理永远孤单


无论使用哪种技术,都应该始终只有一个模型,这是子系统的标准。 或者至少,我们应该为此而努力,避免业务混乱,因为“标准”无处不在,而同时又无处不在。 它使一切变得容易得多。 例如,如果要与其他系统共享XML文件,则可能正在使用XSD。 作为XML格式的元模型INFORMATION_SCHEMA jOOQ: https ://www.jooq.org/xsd/jooq-meta-3.10.0.xsd


  • XSD是众所周知的
  • XSD完美描述了XML内容,并允许使用所有客户端语言进行验证
  • XSD使版本控制容易并向后兼容
  • 使用XJC可以将XSD转换为Java代码

我们特别注意最后一点。 通过XML消息与外部系统进行通信时,我们必须确保消息的有效性。 使用JAXB,XJC和XSD之类的东西确实非常容易。 在这种情况下,考虑Java优先方法的适当性将是疯狂的。 基于XML对象生成的XML质量差,文档记录差并且难以扩展。 如果有用于此类交互的SLA,那么您将感到失望。


老实说,这类似于现在各种JSON API所发生的事情,但这是一个完全不同的故事...


是什么使数据库更糟?


使用数据库时,此处的一切相同。 数据库拥有数据,它也必须是数据模式的主数据库。 必须直接通过DDL完成所有架构修改,以更新引用。


更新参考之后,所有客户都应更新关于模型的想法。 可以使用jOOQ和/或Hibernate或JDBC用Java编写某些客户端。 其他客户端可以用Perl(祝他们好运)或C#编写。 没关系 主要模型在数据库中。 尽管使用ORM创建的模型质量较差,但文档记录不充分且难以扩展。


因此,不要从开发的一开始就这样做。 而是从数据库开始。 创建一个自动CI / CD管道。 使用其中的代码生成为每个构建自动为客户端生成数据库模型。 不必担心,一切都会好起来的。 所需要做的只是建立基础结构的一些初步工作,但是结果,您将在未来几年的其余项目中获得开发过程的收益。


不用了


说明


进行整合:本文决不主张将数据库模型应用于整个系统(主题领域,业务逻辑等)。 我的陈述仅在于以下事实:与数据库交互的客户端代码应仅表示数据库模式,而不能以任何方式定义和形成数据库模式。


在仍有两层架构的地方,数据库架构可能是有关系统模型的唯一信息来源。 但是,在大多数系统上,我将数据访问级别看作是封装数据库模型的“子系统”。 这样的东西。


例外情况


与其他任何良好规则一样,我们的规则也有例外(我已经警告过,数据库优先方法和代码生成并非总是正确的选择)。 这些例外情况(也许列表不完整):


  • 当电路未知时,需要进行调查。 例如,您是一个工具的提供者,可以帮助用户导航任何方案。 当然,不会产生任何代码。 但是无论如何,您都必须处理数据库本身及其架构。
  • 对于某些任务,您需要即时创建一个方案。 这可能类似于Entity-attribute-value模式的变体之一,因为 您没有明确定义的模式。 不能确定在这种情况下RDBMS是正确的选择。

这些例外的特殊之处在于它们很少在野生动植物中相遇。 在大多数情况下,使用关系数据库时,该方案是预先知道的,并且是模型的“标准”,并且客户端应使用通过代码生成器生成的该模型的副本。

Source: https://habr.com/ru/post/zh-CN413597/


All Articles