在这篇过期的文章中,我将解释为什么我认为在几乎所有情况下,都应该在应用程序的数据模型中实现“数据库优先”的设计,而不是“ Java首先”的设计(或任何客户端语言),一旦您的项目发展起来,后一种方法将导致漫长的痛苦之路。
关于/ r / java和/ r / programming的有趣的reddit讨论 。
代码生成
令我惊讶的是,一小群初次使用jOOQ的用户似乎对jOOQ严重依赖于源代码生成感到震惊。 没有人阻止你使用jOOQ你想要的方式,你不必使用代码生成,而是使用默认的方式,根据本手册jOOQ是开始一个(传统)数据库模式,反向工程,使用jOOQ的代码生成器,以获得一堆代表您的表的类,然后针对这些表编写类型安全的查询:
1个
2
3
4
5
6
7
8
9
10
|
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代码 。 原理始终相同:
- 有一些事实(内部或外部),例如规范,数据模型等。
- 我们需要在编程语言中以本地方式表示该事实
为了避免冗余,生成后者几乎总是有意义的。
还请参见: 为什么SQL绑定变量对性能很重要
类型提供者和注释处理
值得注意的是:jOOQ的特定代码生成用例的另一种更现代的方法是由F#实现的类型提供程序 ,在这种情况下,代码是由编译器在编译时生成的。 它从未真正以其源代码形式存在。 Java中类似(但不太复杂)的工具是注释处理器,例如Lombok 。
从某种意义上讲,它除了执行以下操作之外,还具有相同的作用:
- 您看不到生成的代码(也许这对某些人来说不那么令人震惊?)
- 您必须确保可以提供类型,即“真相”必须始终可用。 对于Lombok来说,这很容易,它注释了“真相”。 对于依赖始终可用的实时连接的数据库模型,要困难一些。
代码生成有什么问题?
除了棘手的问题是手动还是自动触发代码生成,有些人似乎认为根本不应该生成代码。 我听到最多的原因是,很难在构建管道中进行设置。 是的,是的。 有额外的基础架构开销。 尤其是如果您对某种产品(例如jOOQ,JAXB或Hibernate等)不熟悉,设置环境需要花费时间,您宁愿花时间学习API本身并从中获取价值。
如果学习代码生成器的工作方式的开销太大,那么,实际上API不能使代码生成器易于使用(以后再进行自定义)。 对于任何此类API来说,这应该是一个高优先级。 但这是反对代码生成的唯一论点。 除此之外,手写内部或外部真理的本地表示完全没有任何意义。
许多人认为他们没有时间去做这些东西。 他们需要运送自己的MVP。 他们可以稍后确定其构建管道。 我说:
由Alan O'Rourke / Audience Stack 授权的CC “忙于改进”。 原始图片
“但是Hibernate / JPA使Java编程首先变得容易”
是的,这是真的。 对于Hibernate及其用户而言,这既是一种幸福,也是一种诅咒。 在Hibernate中,您可以只编写几个实体,例如:
1个
2
3
4
5
6
|
@Entity
class Book {
@Id
int id;
String title;
}
|
而且您快要设置好了。 让Hibernate生成如何在SQL方言的DDL中定义此实体的无聊的“细节”:
1个
2
3
4
5
6
7
8
|
CREATE TABLE book (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
title VARCHAR (50),
CONSTRAINT pk_book PRIMARY KEY (id)
);
ON book (title); CREATE INDEX i_book_title book (title);
|
…并开始运行该应用程序。 快速入门并尝试一些东西真的很酷。
还请参见: 如何确保您的代码与旧版JDK兼容
但是,等等。 我作弊了。
- Hibernate真的会应用命名的主键定义吗?
- 它会在TITLE上创建索引,我知道我们需要它吗?
- 会添加身份规范吗?
可能不是。 在开发未开发项目时,添加所有批注后,始终丢弃整个数据库并从头开始重新生成它很方便。 因此,Book实体最终将看起来像这样:
1个
2
3
4
5
6
7
8
9
10
|
@Entity
@Table (name = "book" , indexes = {
@Index (name = "i_book_title" , columnList = "title" )
})
class Book {
@Id
@GeneratedValue (strategy = IDENTITY)
int id;
String title;
}
|
凉。 再生。 同样,这使得上手非常容易。
但是你以后要付出代价
在某个时候,您开始生产。 这就是这种模型不再起作用的时候。 因为
上线之后,您将无法再丢弃数据库,因为数据库已成为旧版。
从现在开始,您必须编写DDL迁移脚本,例如使用Flyway 。 然后,您的实体发生了什么? 您可以手动调整它们(以便您加倍工作),也可以让Hibernate为您重新生成它们(与您的期望相符的一代机会有多大?),您只能输掉。
因为一旦投入生产,就需要修复程序。 而且那些必须快速上线。 而且由于您没有准备好顺利地将迁移流水线化到生产环境,因此您会疯狂地打补丁。 然后您就没时间做对了™。 而且您会责备Hibernate,因为这总是别人的错...
取而代之的是,您所做的事情可能与一开始完全不同。 就像使用那些轮毂一样。
首先进入数据库
数据库模式的真正“真相”及其上的“主权”与数据库一起存在。 数据库是定义架构的唯一位置,并且所有客户端都有数据库架构的副本,反之亦然。 数据在数据库中,而不是在客户端中,因此完全有必要在数据库中的数据所在的位置强制实施架构及其完整性。
这是古老的智慧,没有新事物。 主键和唯一键很好。 外键很好。 检查约束是好的。 断言(最终实现时)是好的。
那不是结局。 例如,如果您使用的是Oracle,则可能需要指定:
- 您的表驻留在哪个表空间中
- 它具有什么PCTFREE值
- 您序列的缓存大小(在身份之后)是多少
也许,在小型系统中,所有这些都无关紧要,但您不必先去“大数据”,即可从上述特定于供应商的存储优化中获利。 我见过的所有ORM(包括jOOQ)都不会允许您使用数据库中可能要使用的全套DDL选项。 ORM提供了一些工具来帮助您编写DDL。
但是最终,一个精心设计的架构是用DDL手写的。 所有生成的DDL只是其中的一个近似值。
客户端模型呢?
如前所述,您将需要客户端中的数据库模式副本,即客户端表示形式。 不用说,此客户表示形式必须与真实模型同步。 如何做到最好? 通过使用代码生成器。
所有数据库都通过SQL公开其元信息。 以下是使用各种SQL方言从数据库中获取所有表的方法:
1个
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
-- 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的隐式联接功能,该功能依赖于有关表之间外键关系的生成的元信息 。
现在,任何数据库增量都会自动导致更新客户端代码。 例如,假设:
1个
|
ALTER TABLE book RENAME COLUMN title TO book_title;
|
您真的要做两次这项工作吗? 没门。 只需提交DDL,在构建管道中运行它,并拥有一个更新的实体:
1个
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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;
}
|
或更新的jOOQ类。 另外:您的客户端代码可能不再编译,这可能是一件好事! 大多数DDL更改也是语义更改,而不仅仅是语法更改。 因此,很高兴能够在编译的客户端源代码中看到数据库增量会影响(或可能影响)哪些代码。
一个真理
无论您使用哪种技术,总有一个模型包含子系统的单个事实-至少,我们应该为此目标而定,避免“真相”无处不在的企业混乱。 它只是使一切变得简单得多。 如果您与其他系统交换XML文件,则将使用XSD。 就像jOOQ的XML形式的INFORMATION_SCHEMA元模型一样:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd
- XSD是众所周知的
- XSD很好地指定了XML内容,并允许使用所有客户端语言进行验证
- XSD可以轻松地进行版本控制,并向后兼容
- 可以使用XJC将XSD转换为Java代码
最后一点很重要。 通过XML消息与外部系统通信时,我们要确保我们的消息有效。 使用JAXB,XJC和XSD确实非常容易。 认为将Java设计为Java对象的Java优先方法可以以某种方式合理地映射到XML以供其他人使用,这简直是胡说八道。 生成的XML的质量很差,没有文档记录,并且很难发展。 如果在这样的接口上有一个SLA,我们将被搞砸。
坦率地说,这就是JSON API一直发生的情况,但这是另一回事,另一回事……
还请参见: 关于如何成为一名优秀程序员的10个技巧
数据库:一样
当您使用数据库时,这是同一件事。 数据库拥有其数据,它应该是模式的主数据库。 对模式的所有修改都应直接使用DDL实施,以更新单个事实。
一旦更新了真相,所有客户也需要更新其模型副本。 某些客户端可能使用jOOQ和Hibernate(或两者)或JDBC用Java编写。 其他客户可能用Perl编写(祝他们好运)。 甚至其他客户端也可以用C#编写。 没关系 主要模型在数据库中。 ORM生成的模型质量很差,没有很好的文档记录,并且很难发展。
所以,不要这样做。 而且,不要从一开始就这样做。 相反,请先进入数据库。 建立可以自动化的部署管道。 包括代码生成器,以将数据库模型复制回客户端。 不必再担心代码生成了。 那是一件好事。 您将变得富有成效。 设置它只需要一点点的初步努力,您就可以在项目的其余部分中获得多年提高的生产率。
晚点再谢我。
澄清度
只是为了确保:本文决不主张将数据库模型强加于整个系统(例如,域,业务逻辑等)。 我在这里提出的主张是,与数据库交互的客户端代码应作用于数据库模型,而不应具有自己的数据库一流模型。 此逻辑通常驻留在客户端的数据访问层中。
在有时仍然存在的两层体系结构中,这可能是系统的唯一模型。 但是,在大多数系统中,我认为数据访问层是封装数据库模型的“子系统”。 所以,那里。
例外情况
总是有例外,我向您保证,数据库优先和代码生成方法不一定总是正确的选择。 这些异常是(可能不详尽):
- 当架构未知并且必须被发现时。 例如,您是帮助用户浏览任何架构的工具供应商。 ………没有代码生成。 但还是数据库优先。
- 需要为某些任务即时生成模式时。 这听起来很像实体属性值模式的某种或多或少复杂的版本,即您实际上并没有定义明确的架构。 在这种情况下,通常甚至不能确定RDBMS是否是正确的选择。
异常的性质是它们是例外。 在大多数 RDBMS用法中,该模式是预先已知的,作为“真相”的唯一来源放置在RDBMS内,并且客户端将从其派生出副本-理想情况下是使用代码生成器生成的。