译自:An Introduction to Hibernate 6
文中相关链接需要科学上网方可访问,后续有时间再逐个翻译。文章中如果存在任何不准确的地方,欢迎指正。
尚未完成,不断更新中....
系列文章:
目录
4.对象/关系映射
在给定了一个领域模型(即一组带有我们在前一章中介绍的所有注解的实体类)之后,Hibernate 将愉快地根据它推断出完整的关系型模式,甚至在你礼貌地询问时,还可以将它导出到你的数据库中。
生成的模式将会非常合理和可行,尽管如果你仔细观察,你可能会发现一些问题。例如,每个 VARCHAR 列的长度都是相同的,VARCHAR(255)。
但是,我刚刚描述的这个过程,我们称之为自上而下的映射,实际上并不适用于最常见的对象/关系映射的使用场景。通常,Java 类通常不会先于关系模式存在。通常情况下,我们已经有了一个关系型模式,并且我们正在围绕这个模式构建我们的领域模型。这被称为自下而上的映射。
开发人员通常将现有的关系数据库称为“遗留”数据。这常常让人联想到那些糟糕的“遗留应用程序”,可能是用 COBOL 或其他什么语言写的。但是,遗留数据具有很大的价值,学会如何处理它们非常重要。
特别是在自下而上的映射过程中,我们经常需要自定义推断出的对象/关系映射。这是一个有点乏味的主题,所以我们不打算在这方面花太多篇幅。相反,我们将快速浏览一下最重要的映射注解。
Hibernate 的 SQL 命名约定 计算机上的小写字母已经有很长一段时间了。大多数开发人员早就学会了在 MixedCase、camelCase,甚至 snake_case 中编写的文本比在 SHOUTYCASE 中编写的文本更容易阅读。这一点不仅适用于 SQL,也适用于任何其他语言。
因此,20 多年来,Hibernate 项目上的约定是:
- 查询语言标识符使用小写编写,
- 表名使用 MixedCase 编写,
- 列名使用 camelCase 编写。
也就是说,我们简单地采用了 Java 的优秀约定,并将其应用到了 SQL 中。
当然,我们无法强制你遵循这个约定,即使我们愿意。实际上,你可以很容易地编写一个 PhysicalNamingStrategy
,如果你愿意的话,可以将表名和列名设置得像这样 ALL UGLY AND SHOUTY。但是,默认情况下,这是 Hibernate 遵循的约定,而且实际上是一个相当合理的约定。
4.1. 映射实体继承层次
在实体类继承中,我们看到实体类可以存在于继承层次结构中。有三种基本策略可以将实体层次结构映射到关系数据库表中。我们将它们放入一个表格中,以便更容易比较它们之间的差异点。
表格 20. 实体继承映射策略
策略 | 映射 | 多态查询 | 约束 | 规范化 | 何时使用 |
---|---|---|---|---|---|
SINGLE_TABLE | 将层次结构中的每个类映射到同一张表,并使用鉴别器列的值确定每行代表哪个具体类。 | 只需查询一张表即可检索到给定类的实例。 | 由子类声明的属性映射到没有 NOT NULL 约束的列。 | 任何关联都可以有外键约束。 | 子类数据是非规范化的。 |
JOINED | 将层次结构中的每个类映射到一个单独的表,但每个表只映射该类自身声明的属性。 | 必须通过以下方式联接该类的表:与其超类映射的所有表以及其子类映射的所有表。 | 任何属性都可以映射到带有 NOT NULL 约束的列。 | 任何关联都可以有外键约束。 | 表格是规范化的。 |
TABLE_PER_CLASS | 将层次结构中的每个具体类映射到一个单独的表,但将所有继承属性都非规范化到表中。 | 必须在该类的表和其子类映射的所有表之间执行 UNION 操作。 | 针对超类的关联在数据库中不能有相应的外键约束。 | 任何属性都可以映射到带有 NOT NULL 约束的列。 | 超类数据是非规范化的。 |
这三种映射策略由 InheritanceType
枚举定义。我们使用 @Inheritance
注解来指定继承映射策略。
对于带有鉴别器列的映射,我们应该:
- 通过在根实体上使用
@DiscriminatorColumn
注解来指定鉴别器列的名称和类型,并且 - 通过在层次结构中的每个实体上使用
@DiscriminatorValue
注解来指定鉴别器的值。
对于单表继承,我们总是需要一个鉴别器:
@Entity
@DiscriminatorColumn(discriminatorType=CHAR, name="kind")
@DiscriminatorValue('P')
class Person { ... }
@Entity
@DiscriminatorValue('A')
class Author { ... }
我们不需要明确指定 @Inheritance(strategy=SINGLE_TABLE)
,因为那是默认值。
对于 JOINED
继承,我们不需要鉴别器:
@Entity
@Inheritance(strategy=JOINED)
class Person { ... }
@Entity
class Author { ... }
然而,如果我们愿意的话,可以添加一个鉴别器列,在这种情况下,多态查询的生成 SQL 将会稍微简单一些。
类似地,对于 TABLE_PER_CLASS
继承,我们有:
@Entity
@Inheritance(strategy=TABLE_PER_CLASS)
class Person { ... }
@Entity
class Author { ... }
Hibernate 不允许 TABLE_PER_CLASS
继承映射使用鉴别器列,因为它们没有意义,也没有优势。
请注意,在这种情况下,像这样的多态关联:
@ManyToOne Person person;
是一个不好的主意,因为无法创建同时针对两个映射表的外键约束。
4.2. 映射到数据库表格
下面的注解精确指定了领域模型元素如何映射到关系模型的表格:
表格 21. 映射表格的注解
注解 | 用途 |
---|---|
@Table | 将实体类映射到其主表格。 |
@SecondaryTable | 为实体类定义一个附加表格。 |
@JoinTable | 将多对多或多对一关联映射到其关联表格。 |
@CollectionTable | 将 @ElementCollection 映射到其表格。 |
前两个注解用于将实体映射到其主表格,可选择性地,还可以映射到一个或多个附加表格。
4.3. 实体映射到表格
默认情况下,一个实体映射到一个单独的表格,可以使用 @Table
注解指定表格:
@Entity
@Table(name="People")
class Person { ... }
然而,@SecondaryTable
注解允许我们将其属性分散到多个附加表格中:
@Entity
@Table(name="Books")
@SecondaryTable(name="Editions")
class Book { ... }
@Table
注解不仅可以指定一个名称:
表格 22. @Table 注解成员
注解成员 | 用途 |
---|---|
name | 映射表格的名称 |
schema 💀 | 表格所属的模式(schema) |
catalog 💀 | 表格所属的目录(catalog) |
uniqueConstraints | 一个或多个 @UniqueConstraint 注解,声明多列的唯一约束 |
indexes | 一个或多个 @Index 注解,每个声明一个索引 |
只有当领域模型分布在多个模式中时,通过注解显式指定模式才有意义。
否则,在 @Table
注解中硬编码模式(或目录)是个坏主意。相反地:
- 可以设置配置属性
hibernate.default_schema
(或hibernate.default_catalog
),或者 - 在 JDBC 连接 URL 中简单地指定模式。
@SecondaryTable
注解更有趣:
表格 23. @SecondaryTable 注解成员
注解成员 | 用途 |
---|---|
name | 映射表格的名称 |
schema 💀 | 表格所属的模式(schema) |
catalog 💀 | 表格所属的目录(catalog) |
uniqueConstraints | 一个或多个 @UniqueConstraint 注解,声明多列的唯一约束 |
indexes | 一个或多个 @Index 注解,每个声明一个索引 |
pkJoinColumns | 一个或多个 @PrimaryKeyJoinColumn 注解,指定主键列映射 |
foreignKey | 一个 @ForeignKey 注解,指定 @PrimaryKeyJoinColumns 的外键约束 |
在 SINGLE_TABLE
实体继承层次结构中,在子类上使用 @SecondaryTable
注解可以得到一种 SINGLE_TABLE 和 JOINED 继承的混合形式。
4.4. 关联映射到表格
@JoinTable
注解指定了一个关联表格,即一个保存两个关联实体的外键的表格。通常,这个注解与 @ManyToMany
关联一起使用:
@Entity
class Book {
...
@ManyToMany
@JoinTable(name="BooksAuthors")
Set<Author> authors;
...
}
但是,它甚至也可以用于将 @ManyToOne
或 @OneToOne
关联映射到关联表格。
@Entity
class Book {
...
@ManyToOne(fetch=LAZY)
@JoinTable(name="BookPublisher")
Publisher publisher;
...
}
在这里,关联表格的一个列上应该有一个唯一约束。
@Entity
class Author {
...
@OneToOne(optional=false, fetch=LAZY)
@JoinTable(name="AuthorPerson")
Person author;
...
}
在这里,关联表格的两个列上应该有唯一约束。
表格 24. @JoinTable 注解成员
注解成员 | 用途 |
---|---|
name | 映射的关联表格的名称 |
schema 💀 | 表格所属的模式(schema) |
catalog 💀 | 表格所属的目录(catalog) |
uniqueConstraints | 一个或多个 @UniqueConstraint 注解,声明多列的唯一约束 |
indexes | 一个或多个 @Index 注解,每个声明一个索引 |
joinColumns | 一个或多个 @JoinColumn 注解,指定关联方表格中的外键列映射 |
inverseJoinColumns | 一个或多个 @JoinColumn 注解,指定非关联方表格中的外键列映射 |
foreignKey | 一个 @ForeignKey 注解,指定 joinColumns 上的外键约束名称 |
inverseForeignKey | 一个 @ForeignKey 注解,指定 inverseJoinColumns 上的外键约束名称 |
为了更好地理解这些注解,我们首先必须讨论一般的列映射。
4.5. 映射到列
以下注解指定了领域模型元素如何映射到关系模型表格的列:
表格 25. 映射列的注解
注解 | 用途 |
---|---|
@Column | 将属性映射到一个列。 |
@JoinColumn | 将关联映射到一个外键列。 |
@PrimaryKeyJoinColumn | 将用于将次表格与其主表格、或在 JOINED 继承中将子类表格与其根类表格连接的主键进行映射。 |
@OrderColumn | 指定用于维护列表顺序的列。 |
@MapKeyColumn | 指定用于持久化映射键的列。 |
我们使用 @Column
注解来映射基本属性。
4.6. 将基本属性映射到列
@Column
注解不仅仅用于指定列名。
表格 26. @Column 注解成员
注解成员 | 用途 |
---|---|
name | 映射的列的名称 |
table | 此列所属的表格的名称 |
length | VARCHAR、CHAR 或 VARBINARY 列类型的长度 |
precision | FLOAT、DECIMAL、NUMERIC、TIME 或 TIMESTAMP 列类型的小数位数 |
scale | DECIMAL 或 NUMERIC 列类型的小数位数,即小数点右边的精度 |
unique | 列是否具有 UNIQUE 约束 |
nullable | 列是否具有 NOT NULL 约束 |
insertable | 列是否应该出现在生成的 SQL INSERT 语句中 |
updatable | 列是否应该出现在生成的 SQL UPDATE 语句中 |
columnDefinition 💀 | 应该用于声明列的 DDL 片段 |
我们不再建议使用 columnDefinition
,因为它会导致不可移植的 DDL。Hibernate 有更好的方法来定制生成的 DDL,使用这些方法可以在不同的数据库中实现可移植性。
在这里,我们看到了使用 @Column
注解的四种不同方式:
@Entity
@Table(name="Books")
@SecondaryTable(name="Editions")
class Book {
@Id @GeneratedValue
@Column(name="bookId") // 自定义列名
Long id;
@Column(length=100, nullable=false) // 将列声明为 VARCHAR(100) NOT NULL
String title;
@Column(length=17, unique=true, nullable=false) // 将列声明为 VARCHAR(17) NOT NULL UNIQUE
String isbn;
@Column(table="Editions", updatable=false) // 列属于次表格,并且不会被更新
int edition;
}
我们不使用 `@Column` 来映射关联。
4.7. 将关联映射到外键列
@JoinColumn
注解用于自定义外键列。
表格 27. @JoinColumn 注解成员
注解成员 | 用途 |
---|---|
name | 映射的外键列的名称 |
table | 此列所属的表格的名称 |
referencedColumnName | 映射的外键列引用的列的名称 |
unique | 列是否具有 UNIQUE 约束 |
nullable | 列是否具有 NOT NULL 约束 |
insertable | 列是否应该出现在生成的 SQL INSERT 语句中 |
updatable | 列是否应该出现在生成的 SQL UPDATE 语句中 |
columnDefinition 💀 | 应该用于声明列的 DDL 片段 |
foreignKey | 一个 @ForeignKey 注解,指定 FOREIGN KEY 约束的名称 |
一个外键列不一定要引用被关联表格的主键。它可以引用被关联实体的任何其他唯一键,甚至是次表格的唯一键。
在这里,我们看到了如何使用 @JoinColumn
定义一个 @ManyToOne
关联,将外键列映射到 Book 的 @NaturalId
:
@Entity
@Table(name="Items")
class Item {
...
@ManyToOne(optional=false)
@JoinColumn(name = "bookIsbn", referencedColumnName = "isbn",
foreignKey = @ForeignKey(name="ItemsToBooksBySsn"))
Book book;
...
}
如果这让你感到困惑:
bookIsbn
是Items
表格中外键列的名称,- 它引用了
Books
表格的唯一键isbn
,并且 - 它有一个名为
ItemsToBooksBySsn
的外键约束。
请注意,foreignKey
成员是完全可选的,只影响 DDL 生成。
如果不使用 @ForeignKey
显式指定名称,Hibernate 将生成一个相当丑陋的名称。这是因为某些数据库上外键名称的最大长度受到极大限制,我们需要避免冲突。公平地说,如果你只是用生成的 DDL 进行测试,那么这是完全可以接受的。
对于复合外键,我们可能有多个 @JoinColumn
注解:
@Entity
@Table(name="Items")
class Item {
...
@ManyToOne(optional=false)
@JoinColumn(name = "bookIsbn", referencedColumnName = "isbn")
@JoinColumn(name = "bookPrinting", referencedColumnName = "printing")
Book book;
...
}
如果我们需要指定 @ForeignKey
,这会变得有点混乱:
@Entity
@Table(name="Items")
class Item {
...
@ManyToOne(optional=false)
@JoinColumns(value = {@JoinColumn(name = "bookIsbn", referencedColumnName = "isbn"),
@JoinColumn(name = "bookPrinting", referencedColumnName = "printing")},
foreignKey = @ForeignKey(name="ItemsToBooksBySsn"))
Book book;
...
}
对于映射到 @JoinTable
的关联,获取关联需要两个连接,所以我们必须在 @JoinTable
注解内声明 @JoinColumns
:
@Entity
class Book {
@Id @GeneratedValue
Long id;
@ManyToMany
@JoinTable(joinColumns=@JoinColumn(name="bookId"),
inverseJoinColumns=@joinColumn(name="authorId"),
foreignKey=@ForeignKey(name="BooksToAuthors"))
Set<Author> authors;
...
}
同样,foreignKey
成员是可选的。
4.8. 映射表格之主键连接
@PrimaryKeyJoinColumn
是一种专用注解,用于映射:
@SecondaryTable
的主键列,它同时也是一个外键,引用主表格;- 在
JOINED
继承层次结构中,映射子类表格的主键列,它同时也是一个外键,引用由根实体映射的主表格。
表格 28. @PrimaryKeyJoinColumn 注解成员
注解成员 | 用途 |
---|---|
name | 映射的外键列的名称 |
referencedColumnName | 映射的外键列引用的列的名称 |
columnDefinition 💀 | 应该用于声明列的 DDL 片段 |
foreignKey | 一个 @ForeignKey 注解,指定 FOREIGN KEY 约束的名称 |
在映射子类表格的主键时,我们将 @PrimaryKeyJoinColumn
注解放在实体类上:
@Entity
@Table(name="People")
@Inheritance(strategy=JOINED)
class Person { ... }
@Entity
@Table(name="Authors")
@PrimaryKeyJoinColumn(name="personId") // Authors 表格的主键
class Author { ... }
但是,在映射次表格的主键时,@PrimaryKeyJoinColumn
注解必须位于 @SecondaryTable
注解内部:
@Entity
@Table(name="Books")
@SecondaryTable(name="Editions",
pkJoinColumns = @PrimaryKeyJoinColumn(name="bookId")) // Editions 表格的主键
class Book {
@Id @GeneratedValue
@Column(name="bookId") // Books 表格的主键名称
Long id;
...
}
4.9. 列长度和自适应列类型
Hibernate 根据 @Column
注解指定的列长度自动调整生成的 DDL 中的列类型。所以,通常情况下,我们不需要显式指定一个列应该是 TEXT 或 CLOB 类型,也不用担心在 MySQL 上会出现 TINYTEXT、MEDIUMTEXT、TEXT、LONGTEXT 这样的类型,因为Hibernate会根据需要选择其中一个类型,以适应我们指定的字符串长度。
在这里,Length
类中定义的常量值非常有用:
表格 29. 预定义的列长度
常量 | 值 | 描述 |
---|---|---|
DEFAULT | 255 | 当没有显式指定长度时,VARCHAR 或 VARBINARY 列的默认长度 |
LONG | 32600 | VARCHAR 或 VARBINARY 列上允许的最大列长度,在 Hibernate 支持的每个数据库上都允许 |
LONG16 | 32767 | 使用 16 位表示的最大长度(但是对于某些数据库的 VARCHAR 或 VARBINARY 列来说,这个长度太大了) |
LONG32 | 2147483647 | Java 字符串的最大长度 |
我们可以在 @Column
注解中使用这些常量:
@Column(length=LONG)
String text;
@Column(length=LONG32)
byte[] binaryData;
通常,这就足够了,可以在Hibernate中使用大对象类型。
4.10. 大对象(LOBs)
JPA 提供了 @Lob
注解,用于指定字段应该被持久化为 BLOB 或 CLOB。
@Lob
注解的语义 规范实际上说,该字段应该被持久化为
…作为一个数据库支持的大对象类型。
这是相当不清晰的,而且规范还继续说
…Lob 注解的处理是提供者相关的…
这并没有太大帮助。
Hibernate 对这个注解的解释是我们认为最合理的方式。在 Hibernate 中,一个被 @Lob
注解的属性将使用 PreparedStatement 的 setClob()
或 setBlob()
方法写入到 JDBC,并且将使用 ResultSet 的 getClob()
或 getBlob()
方法从 JDBC 中读取。
但是,通常情况下使用这些 JDBC 方法是不必要的!JDBC 驱动完全有能力在 String 和 CLOB 之间,或者在 byte[] 和 BLOB 之间进行转换。所以,除非你明确需要使用这些 JDBC LOB API,否则你不需要 @Lob
注解。
相反,正如我们在“列长度和自适应列类型”中看到的那样,你只需要指定足够大的列长度来容纳你计划写入该列的数据。
不幸的是,PostgreSQL 的驱动程序不允许通过 JDBC LOB API 读取 BYTEA 或 TEXT 类型的列。
这个 Postgres 驱动程序的限制导致了一个整个博客界和 stackoverflow 上的问题回答者推荐使用复杂的方式来修改用于 Postgres 的 Hibernate 方言,以允许使用 setString()
写入属性,然后使用 getString()
读取。
但是,简单地移除 @Lob
注解会达到完全相同的效果。
总结:
- 在 PostgreSQL 中,
@Lob
总是表示 OID 类型, @Lob
永远不应该用于映射 BYTEA 或 TEXT 类型的列,而- 请不要相信你在 stackoverflow 上看到的一切。
最后,作为一种替代方法,Hibernate 允许你声明一个类型为 java.sql.Blob
或 java.sql.Clob
的属性。
@Entity
class Book {
...
Clob text;
Blob coverArt;
....
}
优点是 java.sql.Clob
或 java.sql.Blob
在原则上可以索引多达 2^63 个字符或字节的数据,比你可以放入 Java 字符串或 byte[] 数组(或你的计算机)中的数据要多得多。
要给这些字段分配一个值,我们需要使用 LobHelper
。我们可以从 Session 中获取它:
LobHelper helper = session.getLobHelper();
book.text = helper.createClob(text);
book.coverArt = helper.createBlob(image);
原则上,Blob 和 Clob 对象提供了从服务器读取或流式传输 LOB 数据的有效方法。
Book book = session.find(Book.class, bookId);
String text = book.text.getSubString(1, textLength);
InputStream bytes = book.images.getBinaryStream();
当然,这里的行为非常依赖于 JDBC 驱动程序,所以我们不能保证在你的数据库上这样做是明智的。
4.11. 映射可嵌入类型到 UDTs 或 JSON
有几种可用的方式可以在数据库端表示可嵌入类型。
可嵌入类型作为 UDTs
首先,一个非常好的选择,至少在 Java 记录类型的情况下,并且对于支持用户定义类型(UDTs)的数据库来说,是定义一个代表记录类型的 UDT。Hibernate 6 使得这个过程非常简单。只需使用新的 @Struct
注解为记录类型或持有对它的引用的属性进行注解:
@Embeddable
@Struct(name="PersonName")
record Name(String firstName, String middleName, String lastName) {}
@Entity
class Person {
...
Name name;
...
}
这将得到以下的 UDT:
create type PersonName as (firstName varchar(255), middleName varchar(255), lastName varchar(255))
并且 Author 表的 name 列将具有类型 PersonName。
将可嵌入类型映射到 JSON
第二个可用的选项是将可嵌入类型映射到 JSON(或 JSONB)列。现在,如果你是从零开始定义数据模型,我们不会完全推荐这样做,但如果你需要映射具有预定义 JSON 类型列的现有表格,这是一个有用的方式。由于可嵌入类型是可以嵌套的,我们可以使用这种方式映射某些 JSON 格式,甚至可以使用 HQL 查询 JSON 属性。
目前,不支持 JSON 数组!
要将可嵌入类型的属性映射到 JSON,我们必须为属性注解 @JdbcTypeCode(SqlTypes.JSON)
,而不是为可嵌入类型进行注解。但是,如果我们想要使用 HQL 查询它的属性,可嵌入类型 Name 仍然应该被注解为 @Embeddable
。
@Embeddable
record Name(String firstName, String middleName, String lastName) {}
@Entity
class Person {
...
@JdbcTypeCode(SqlTypes.JSON)
Name name;
...
}
我们还需要将 Jackson 或 JSONB 的实现(例如 Yasson)添加到运行时类路径中。如果我们想使用 Jackson,我们可以在 Gradle 构建中添加以下行:
runtimeOnly 'com.fasterxml.jackson.core:jackson-databind:{jacksonVersion}'
现在 Author 表的 name 列将具有类型 jsonb,Hibernate 将自动使用 Jackson 将 Name 序列化为 JSON 格式,并从 JSON 格式反序列化为 Name。
4.12. SQL列类型映射总结
正如我们所见,有很多注解会影响Java类型在DDL中映射到SQL列类型。在这里,我们总结了在本章的后半部分中我们刚刚看到的这些注解,以及在之前章节中已经提到的一些注解。
映射SQL列类型的注解
注解 | 解释 |
---|---|
@Enumerated | 指定枚举类型应该如何持久化。 |
@Nationalized | 使用国际化字符类型:NCHAR、NVARCHAR 或 NCLOB。 |
@Lob | 使用 JDBC LOB APIs 来读取和写入被注解的属性。 |
@Array | 将集合映射到具有指定长度的 SQL ARRAY 类型。 |
@Struct | 将可嵌入类型映射到具有给定名称的 SQL UDT。 |
@TimeZoneStorage | 指定时区信息应该如何持久化。 |
@JdbcType 或 @JdbcTypeCode | 使用 JdbcType 的实现来映射任意的 SQL 类型。 |
此外,还有一些配置属性对基本类型如何映射到SQL列类型具有全局影响:
类型映射设置
配置属性名 | 目的 |
---|---|
hibernate.use_nationalized_character_data | 启用默认情况下使用国际化字符类型。 |
hibernate.type.preferred_boolean_jdbc_type | 指定映射布尔类型的默认SQL列类型。 |
hibernate.type.preferred_uuid_jdbc_type | 指定映射UUID的默认SQL列类型。 |
hibernate.type.preferred_duration_jdbc_type | 指定映射Duration的默认SQL列类型。 |
hibernate.type.preferred_instant_jdbc_type | 指定映射Instant的默认SQL列类型。 |
hibernate.timezone.default_storage | 指定存储时区信息的默认策略。 |
这些是全局设置,因此相当笨拙。我们建议除非你有一个非常充分的理由,否则不要去修改这些设置。
在这个章节中,我们还有一个主题想要讨论。
4.13. 映射到公式
Hibernate 允许我们将实体的属性映射到涉及到映射表的列的 SQL 公式。因此,该属性是一种"派生"值。
映射到公式的注解
注解 | 目的 |
---|---|
@Formula | 将属性映射到 SQL 公式。 |
@JoinFormula | 将关联映射到 SQL 公式。 |
@DiscriminatorFormula | 在单表继承中使用 SQL 公式作为鉴别器。 |
例如:
@Entity
class Order {
...
@Column(name = "sub_total", scale=2, precision=8)
BigDecimal subTotal;
@Column(name = "tax", scale=4, precision=4)
BigDecimal taxRate;
@Formula("sub_total * (1.0 + tax)")
BigDecimal totalWithTax;
...
}
在这个例子中,totalWithTax
属性通过 SQL 公式 sub_total * (1.0 + tax)
计算得出。
4.14. 派生标识
当一个实体的部分主键从关联的“父”实体继承而来时,该实体具有派生标识。我们在讨论具有共享主键的一对一关联时已经遇到了一种派生标识的特殊情况。
但是,@ManyToOne 关联也可以是派生标识的一部分。也就是说,可以将外键列或多个外键列包含为组合主键的一部分。在Java方面,有三种不同的方式来表示这种情况:
- 使用带有@IdClass但不带有@MapsId的方式。
- 使用带有@IdClass和@MapsId的方式。
- 使用带有@EmbeddedId和@MapsId的方式。
假设我们有一个如下定义的Parent实体类:
@Entity
class Parent {
@Id
Long parentId;
...
}
parentId字段保存了Parent表的主键,该主键也将成为每个Child实体的复合主键的一部分。
第一种方式
在第一种稍微简单的方法中,我们定义了一个@IdClass来表示Child的主键:
class DerivedId {
Long parent;
String childId;
// 构造函数,equals,hashcode等
...
}
并且Child实体类上使用了@Id注解的@ManyToOne关联:
@Entity
@IdClass(DerivedId.class)
class Child {
@Id
String childId;
@Id @ManyToOne
@JoinColumn(name="parentId")
Parent parent;
...
}
然后,Child表的主键由列(childId,parentId)组成。
第二种方式
这种方法是可以的,但有时最好为主键的每个元素拥有一个字段。我们可以使用我们之前遇到的@MapsId注解:
@Entity
@IdClass(DerivedId.class)
class Child {
@Id
Long parentId;
@Id
String childId;
@ManyToOne
@MapsId(Child_.PARENT_ID) // 对Child.parentId进行类型安全引用
@JoinColumn(name="parentId")
Parent parent;
...
}
我们使用了之前看到的方法来以类型安全的方式引用Child的parentId属性。
请注意,我们必须将列映射信息放在@MapsId注解上,而不是@Id字段上。
我们必须稍微修改我们的@IdClass,以便字段名称对齐:
class DerivedId {
Long parentId;
String childId;
// 构造函数,equals,hashcode等
...
}
第三种方式
第三种选择是将我们的@IdClass重新定义为@Embeddable。实际上,我们不需要更改DerivedId类,但是我们需要添加该注解。
@Embeddable
class DerivedId {
Long parentId;
String childId;
// 构造函数,equals,hashcode等
...
}
然后我们可以在Child中使用@EmbeddedId:
@Entity
class Child {
@EmbeddedId
DerivedId id;
@ManyToOne
@MapsId(DerivedId_.PARENT_ID) // 对DerivedId.parentId进行类型安全引用
@JoinColumn(name="parentId")
Parent parent;
...
}
在@IdClass和@EmbeddedId之间的选择取决于个人喜好。@EmbeddedId或许更加DRY(Don't Repeat Yourself)。
4.15. 添加约束
数据库约束非常重要。即使你确信你的程序没有bug 🧐,它可能并不是唯一一个能够访问数据库的程序。约束有助于确保不同的程序(以及人类管理员)可以友好地相互配合。
Hibernate会自动将某些约束添加到生成的DDL中:主键约束、外键约束和一些唯一约束。但通常需要:
- 添加额外的唯一约束,
- 添加检查约束,或者
- 自定义外键约束的名称。
我们已经学过如何使用 @ForeignKey 注解来指定外键约束的名称。
有两种方法可以向表中添加唯一约束:
- 使用
@Column(unique=true)
指定单列唯一键,或者 - 使用
@UniqueConstraint
注解在多列上定义唯一性约束。
@Entity
@Table(uniqueConstraints=@UniqueConstraint(columnNames={"title", "year", "publisher_id"}))
class Book { ... }
这个注解可能看起来有点丑,但实际上它作为文档是非常有用的。
@Check
注解可以向表中添加检查约束。
@Entity
@Check(name="ValidISBN", constraints="length(isbn)=13")
class Book { ... }
@Check
注解通常用在字段级别上:
@Id @Check(constraints="length(isbn)=13")
String isbn;