真相第一,或者说为什么系统需要基于数据库结构来设计

嘿哈布尔!

我们继续探讨这个话题 爪哇岛 и 春季包括数据库级别。 今天,我们将介绍为什么在设计大型应用程序时,数据库结构而不是 Java 代码具有决定性的重要性,这是如何实现的,以及这条规则有哪些例外情况。

在这篇相当迟来的文章中,我将解释为什么我认为在几乎所有情况下,应用程序中的数据模型应该“从数据库”设计,而不是“从 Java 的功能”(或您使用的任何客户端语言)与)一起工作。 如果选择第二种方法,一旦你的项目开始发展,你就会进入一条漫长的痛苦之路。

这篇文章是根据 一个问题,在 Stack Overflow 上给出。

Reddit 上有趣的讨论 /r/java и / r /编程.

代码生成

让我感到多么惊讶的是,有这么一小部分用户在熟悉了 jOOQ 后,却对 jOOQ 严重依赖于源代码生成来运行这一事实感到不满。 没有人会阻止您按照您认为合适的方式使用 jOOQ,也没有人强迫您使用代码生成。 但默认情况下(如手册中所述),jOOQ 的工作方式如下:您从(遗留)数据库模式开始,使用 jOOQ 代码生成器对其进行反向工程,以获得一组代表您的表的类,然后编写类型 -针对这些表的安全查询:

	for (Record2<String, String> record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ Информация о типах выведена на 
//   основании сгенерированного кода, на который ссылается приведенное
// ниже условие SELECT 
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ сгенерированные имена
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

代码可以在构建之外手动生成,也可以在每次构建时手动生成。 例如,这种再生可以紧接在 Flyway数据库迁移,也可以手动或自动完成.

源代码生成

这些代码生成方法(手动和自动)有各种原理、优点和缺点,我不会在本文中详细讨论。 但是,一般来说,生成的代码的全部意义在于,它允许您在 Java 中重现我们认为理所当然的“真理”,无论是在我们的系统内还是在系统外。 从某种意义上说,从源代码生成字节码、机器代码或其他类型代码的编译器做同样的事情——我们用另一种语言得到“真相”的表示,无论具体原因如何。

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

  • 有一些事实(内部或外部)——例如规范、数据模型等。
  • 我们需要用我们的编程语言来本地表达这一事实。

此外,几乎总是建议生成这样的表示 - 以避免冗余。

类型提供者和注释处理

注意:另一种更现代、更具体的 jOOQ 代码生成方法涉及使用类型提供程序, 因为它们是在 F# 中实现的。 在这种情况下,代码是由编译器生成的,实际上是在编译阶段。 原则上,这样的代码不以源代码的形式存在。 在 Java 中,有类似的工具,尽管不是那么优雅 - 这些是注释处理器,例如, 龙目岛.

从某种意义上说,这里发生的事情与第一种情况相同,除了:

  • 您没有看到生成的代码(也许这种情况对某人来说似乎并不那么令人排斥?)
  • 您必须确保可以提供类型,即“true”必须始终可用。 对于 Lombok 来说这很容易,它注释了“真相”。 对于依赖于始终可用的实时连接的数据库模型来说,这有点困难。

代码生成有什么问题?

除了如何更好地开始代码生成(手动或自动)这一棘手问题之外,我还必须提到,有些人认为根本不需要代码生成。 我最常遇到的这种观点的理由是,很难设置构建管道。 是的,这确实很难。 还有额外的基础设施成本。 如果您刚刚开始使用特定产品(无论是 jOOQ、JAXB 还是 Hibernate 等),那么需要时间来设置一个工作台,您希望花时间学习 API 本身以从中获取价值。

如果理解生成器设备相关的成本太高,那么,事实上,API 在代码生成器的可用性方面做得很差(而且将来证明它的定制也很困难)。 对于任何此类 API,可用性应该是最高优先级。 但这只是反对代码生成的理由之一。 否则,完全手写内部或外部事实的本地表示。

许多人会说他们没有时间做这一切。 他们的超级产品即将在最后期限前完成。 有一天我们会梳理装配输送机,我们会有时间的。 我会回答他们:

真相第一,或者说为什么系统需要基于数据库结构来设计
, 艾伦·奥罗克,观众堆栈

但在 Hibernate / JPA 中,“用 Java”编写代码非常容易。

真的。 对于 Hibernate 及其用户来说,这既是福音也是诅咒。 在 Hibernate 中,您可以简单地编写几个实体,如下所示:

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

并且几乎一切都准备好了。 现在 Hibernate 的大部分工作是生成复杂的“细节”,说明如何在 SQL“方言”的 DDL 中准确定义该实体:

	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 真的会强制执行这个命名主键的定义吗?
  • Hibernate 会在 TITLE 上创建索引吗? 我确信我们需要它。
  • Hibernate 会将这个密钥作为身份规范中的身份密钥吗?

可能不会。 如果您从头开始开发项目,那么在添加必要的注释后,只需丢弃旧数据库并生成新数据库总是很方便的。 因此,Book 实体最终将采用以下形式:

	@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 值是多少
  • 您的序列中的缓存大小是多少(在 id 后面)

所有这些在小型系统中可能并不重要,但不必等到过渡到“大数据”领域 - 您可以更早地开始从供应商提供的存储优化中受益,例如上面提到的那些。 我见过的 ORM(包括 jOOQ)都没有提供对您可能想要在数据库中使用的全套 DDL 选项的访问。 ORM 提供了一些工具来帮助您编写 DDL。

但最终,设计良好的模式是用 DDL 手写的。 任何生成的 DDL 只是它的近似值。

那么客户模型呢?

如上所述,在客户端上,您将需要数据库架构的副本,即客户端视图。 不用说,这个客户端视图必须与真实模型同步。 实现这一目标的最佳方法是什么? 带有代码生成器。

所有数据库都通过 SQL 提供元信息。 以下是如何从数据库中获取不同 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

这些查询(或类似的查询,取决于您是否还必须考虑视图、物化视图、表值函数)也通过调用来执行 DatabaseMetaData.getTables() 来自 JDBC,或使用 jOOQ 元模块。

根据此类查询的结果,无论您在客户端上使用什么技术,生成数据库模型的任何客户端表示都相对容易。

  • 如果您使用 JDBC 或 Spring,您可以创建一组字符串常量
  • 如果您使用 JPA,那么您可以自己生成实体
  • 如果您使用jOOQ,您可以生成jOOQ元模型

根据您的客户端 API 提供的功能(例如 jOOQ 或 JPA),生成的元模型可能非常丰富和完整。 以隐式连接的可能性为例, jOOQ 3.11中引入,它依赖于生成的有关表之间外键关系的元信息。

现在任何数据库增量都会自动更新客户端代码。 想象一下例如:

ALTER TABLE book RENAME COLUMN title TO book_title;

你真的愿意做两次这项工作吗? 不可能。 我们只需提交 DDL,通过构建管道运行它,并获取更新的实体:

@Entity
@Table(name = "book", indexes = {
 
  // Вы об этом задумывались?
  @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。 看一下 XML 形式的 jOOQ 的 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 对象,它们可以以某种方式以可理解的方式呈现为 XML 并发送到另一个系统以供使用,那真是太疯狂了。 以这种方式生成的 XML 质量非常差,没有文档记录,并且难以开发。 如果在这样的接口上就服务质量(SLA)水平达成一致,我们会立即搞砸。

老实说,这正是 JSON API 经常发生的情况,但那是另一个故事了,下次我会争论......

数据库:它们是相同的

使用数据库,您会明白它们基本上都是相同的。 数据库拥有其数据并且必须管理架构。 对模式所做的任何修改都必须直接在 DDL 中实现,以便更新单一事实来源。

当源更新发生时,所有客户端也必须更新其模型副本。 某些客户端可能是使用 jOOQ 和 Hibernate 或 JDBC(或两者)用 Java 编写的。 其他客户端可能用 Perl 编写(祝他们好运),其他客户端则用 C# 编写。 没关系。 主要模型在数据库中。 ORM 生成的模型通常质量较差、文档缺乏且难以开发。

所以不要犯错误。 从一开始就不要犯错误。 从数据库工作。 构建可以自动化的部署管道。 启用代码生成器可以方便地复制数据库模型并将其转储到客户端。 不再担心代码生成器。 他们很好。 有了他们,您将变得更有生产力。 您所需要做的就是花一点时间从一开始就设置它们,您将获得多年的性能改进来构建您的项目故事。

以后先别谢我。

澄清

需要明确的是:本文并不以任何方式主张整个系统(即域、业务逻辑等)需要进行调整以适应您的数据库模型。 我在本文中讨论的是与数据库交互的客户端代码应该在数据库模型的基础上运行,这样它就不会以“一流”状态重现数据库模型。 此类逻辑通常位于客户端的数据访问层。

在一些地方仍然保留的两级体系结构中,这样的系统模型可能是唯一可能的。 然而,在大多数系统中,数据访问层在我看来是封装数据库模型的“子系统”。

Исключения

每条规则都有例外,我之前说过,数据库优先和源代码生成方法有时可能不合适。 以下是一些此类例外情况(可能还有其他例外情况):

  • 当模式未知并且需要打开时。 例如,您提供一个工具来帮助用户导航任何图表。 唷。 这里没有代码生成。 但仍然是——首先是数据库。
  • 当需要动态生成电路来解决某些问题时。 这个例子似乎是该模式的稍微褶边版本 实体属性值,即,您实际上没有明确定义的模式。 在这种情况下,您通常甚至根本无法确定 RDBMS 是否适合您。

例外本质上是例外。 在大多数涉及使用 RDBMS 的情况下,模式是预先已知的,它位于 RDBMS 内部,并且是“真相”的唯一来源,并且所有客户端都必须获取从中派生的副本。 理想情况下,这应该涉及代码生成器。

来源: habr.com

添加评论