Java 持久性/表
一个表是关系型数据库的基本持久结构。表包含一个列列表,该列表定义表的结构,以及一个行列表,该列表定义表的数据。每个列都有一个特定的类型,通常还有大小。标准的关系型类型集限于基本类型,包括数字、字符、日期时间和二进制(尽管大多数现代数据库都有其他类型和类型系统)。表也可以具有约束,这些约束定义限制行数据的规则,例如主键、外键和唯一约束。表还具有其他工件,例如索引、分区和触发器。
持久性类的典型映射将该类映射到单个表。在 JPA 中,这通过@Table
注释或<table>
XML 元素来定义。如果没有表注释,JPA 实现将为该类自动分配一个表。JPA 默认表名是类名(不包括包)首字母大写。类的每个属性都将存储在表中的一个列中。
...
@Entity
@Table(name="EMPLOYEE")
public class Employee {
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<table name="EMPLOYEE"/>
</entity>
尽管在理想情况下每个类都应该映射到一个表,但这并不总是可行的。其他情况包括
- 多表 : 一个类映射到 2 个或多个表。
- 共享表 : 2 个或多个类存储在同一个表中。
- 继承 : 一个类参与继承,并具有继承表和本地表。
- 视图 : 一个类映射到一个视图。
- 存储过程 : 一个类映射到一组存储过程。
- 分区 : 一个类的某些实例映射到一个表,其他实例映射到另一个表。
- 复制 : 一个类的数据被复制到多个表。
- 历史 : 一个类具有历史数据。
这些都是高级情况,其中一些由 JPA 规范处理,而许多则没有。以下部分将进一步探讨每种情况,包括 JPA 规范支持的内容,在规范内解决问题的方法,以及如何使用一些 JPA 实现扩展来处理这种情况。
有时一个类会映射到多个表。这通常发生在遗留数据模型或现有数据模型中,其中对象模型和数据模型不匹配。它也可能发生在子类数据存储在其他表中的继承中。多表也可能出于性能、分区或安全原因使用。
JPA 允许将多个表分配给单个类。可以使用@SecondaryTable
和 SecondaryTables 注释或<secondary-table>
元素。默认情况下,假定@Id
列在两个表中,这样次要表的@Id
列就是次要表的主键,也是对第一个表的外部键。如果第一个表的@Id
列没有相同的名称,可以使用@PrimaryKeyJoinColumn
或<primary-key-join-column>
来定义外部键连接条件。
在多表实体中,每个映射必须定义映射的列来自哪个表。这是使用@Column
或@JoinColumn
注释或 XML 元素的table
属性来完成的。默认情况下,使用类的主表,因此您只需要为次要表设置表。对于继承,默认表是正在映射的子类的主表。
...
@Entity
@Table(name="EMPLOYEE")
@SecondaryTable(name="EMP_DATA",
pkJoinColumns = @PrimaryKeyJoinColumn(name="EMP_ID", referencedColumnName="ID")
)
public class Employee {
...
@Column(name="YEAR_OF_SERV", table="EMP_DATA")
private int yearsOfService;
@OneToOne
@JoinColumn(name="MGR_ID", table="EMP_DATA", referencedColumnName="ID")
private Employee manager;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<table name="EMPLOYEE"/>
<secondary-table name="EMP_DATA">
<primary-key-join-column name="EMP_ID" referenced-column-name="ID"/>
</secondary-table>
<attributes>
...
<basic name="yearsOfService">
<column name="YEAR_OF_SERV" table="EMP_DATA"/>
</basic>
<one-to-one name="manager">
<join-column name="MGR_ID" table="EMP_DATA" referenced-column-name="ID"/>
</one-to-one>
</attributes>
</entity>
使用@PrimaryKeyJoinColumn
,名称指的是次要表中的外键列,而 referencedColumnName 指的是第一个表中的主键列。如果您有多个次要表,它们必须始终引用第一个表。在定义表的模式时,通常将次要表中的连接列定义为该表的主键,以及对第一个表的外部键。根据您定义的外部键约束,表的顺序可能很重要,该顺序通常与 JPA 实现插入表顺序匹配,因此请确保表顺序与约束依赖项匹配。
对于具有多个表的类的关系,外键(连接列)始终映射到目标的主表。JPA 不允许外键映射到目标对象主表以外的表。通常情况下,这不是问题,因为外键几乎总是映射到主表的 id/主键,但在一些高级场景中,这可能是一个问题。一些 JPA 产品允许列或连接列使用列的限定名称(例如,@JoinColumn(referenceColumnName="EMP_DATA.EMP_NUM")
),以允许这种类型的关系。一些 JPA 产品也可能通过自己的 API、注释或 XML 支持这一点。
有时,您可能有一个辅助表,它是通过从主表到辅助表的,而不是从辅助表到主表的外键来引用的。您甚至可能在两个辅助表之间有一个外键。假设您有一个EMPLOYEE
表和一个ADDRESS
表,EMPLOYEE
通过ADDRESS_ID
外键引用ADDRESS
,并且(出于某种奇怪的原因),您只想使用单个 Employee 类来存储来自两个表的 data。JPA 规范没有直接涵盖这一点,因此,如果遇到这种情况,首先要考虑的是,如果您有灵活性,请更改数据模型以符合规范。您还可以更改对象模型,为每个表定义一个类,在本例中为 Employee 类和 Address 类,这通常是最好的解决方案。您还应该检查您的 JPA 实现以查看它在该区域支持哪些扩展。
解决问题的一种方法是简单地交换您的主表和辅助表。这将导致辅助表引用主表的主键,并且在规范范围内。但是,这将产生副作用,其中之一是您现在将对象的 primary key 从EMP_ID
更改为ADDRESS_ID
,并且可能还会有其他映射和查询影响。如果您有超过 2 个表,这可能也不起作用。
另一种选择是只在@PrimaryKeyJoinColumn
中使用外键列,这在技术上将是倒退的,并且可能不受规范支持,但对某些 JPA 实现可能有效。但是,这会导致表插入顺序与外键约束不匹配,因此需要删除约束或延迟约束。
还可以通过数据库视图来映射场景。可以定义一个视图来连接两个表,并且类可以映射到视图而不是表。视图在某些数据库上是只读的,但许多数据库也允许写入,或者允许使用触发器来处理写入操作。
一些 JPA 实现提供扩展来处理这些场景。
- TopLink,EclipseLink : 为其映射模型
ClassDescriptor.addForeignKeyFieldNameForMultipleTable()
提供专有 API,该 API 允许在辅助表之间定义任意复杂的外键关系。这可以通过使用@DescriptorCustomizer
注释和DescriptorCustomizer
类来配置。
有时,数据模型和对象模型根本无法很好地协作。数据库可能是遗留模型,与新的应用程序模型不匹配,或者 DBA 或对象架构师可能有点疯狂。在这些情况下,您可能需要高级的多表连接。
这些示例包括有两个表,它们不是通过其主键或外键,而是通过一些常量或计算来关联的。假设您有一个EMPLOYEE
表和一个ADDRESS
表,ADDRESS
表有一个EMP_ID
外键到EMPLOYEE
表,但每个员工都有多个地址,并且只希望TYPE
为"HOME"
的地址。在这种情况下,希望将来自两个表的数据映射到Employee
对象中。需要一个连接表达式,其中外键匹配并且常量匹配。
同样,这种情况可以通过重新设计数据或对象模型,或者通过使用视图来处理。一些 JPA 实现提供扩展来处理这些场景。
- TopLink,EclipseLink : 为其映射模型
DescriptorQueryManager.setMultipleTableJoinExpression()
提供专有 API,该 API 允许定义任意复杂的多表连接。这可以通过使用@DescriptorCustomizer
注释和DescriptorCustomizer
类来配置。
多表映射的另一个变态是希望对辅助表进行外连接。如果辅助表可能或可能没有为对象定义行,则可能需要这样做。通常,如果要尝试这样做,则对象应该是只读的,因为写入可能或可能不存在的行可能很棘手。
JPA 不直接支持这一点,如果遇到这种情况,最好重新考虑数据模型或对象模型设计。同样,可以通过数据库视图来映射这一点,其中使用外连接在视图中连接表。
一些 JPA 实现支持为多个表使用外连接。
- Hibernate : 这可以通过使用 Hibernate
@Table
注释并将其optional
属性设置为true
来实现。这将配置 Hibernate 使用外连接来读取表,并且如果映射到该表的所有属性都为 null,则不会写入该表。
- TopLink,EclipseLink : 如果数据库支持在 where 子句中使用外连接语法(Oracle、Sybase、SQL Server),则可以使用多表连接表达式来配置要用于读取表的外连接。
一些 JPA 提供商可能在包含特殊字符(如空格)的表名和列名方面存在问题。通常,最好使用标准字符、不使用空格,并且所有名称都使用大写字母。只要数据库和 JDBC 驱动程序支持字符集,国际语言应该没问题。
可能需要用引号括住包含特殊字符或在某些情况下包含混合大小写字母的表名和列名。例如,如果表名包含空格,则可以按以下方式定义:
@Table("\"Employee Data\"")
一些数据库支持混合大小写字母的表名和列名,而其他数据库则区分大小写。如果您的数据库不区分大小写,或者您希望您的数据模型可移植,则最好使用全大写字母的名称。这在 JPA 中通常不是什么大问题,因为您很少直接从应用程序中使用表名和列名,但在某些情况下(如果使用本机 SQL 查询)可能会出现问题。
数据库表可能需要以表限定符为前缀,例如表的创建者,或其命名空间、模式或目录。一些数据库还支持将表链接到其他数据库,因此链接名称也可以是表限定符。
在 JPA 中,可以通过schema
或catalog
属性在表上设置表限定符。通常,使用哪个属性并不重要,因为两者都只是在表名前加上前缀。从技术上讲,您甚至可以将完整的名称 "schema.table" 作为表的名称,并且它将起作用。在 schema 或 catalog 中设置前缀的好处是可以为整个持久性单元设置默认表限定符,此外,不设置真实表名可能会影响本机 SQL 查询。
如果所有表都需要相同的表限定符,您可以在 orm.xml 中设置默认值。
...
@Entity
@Table(name="EMPLOYEE", schema="ACME")
public class Employee {
...
}
<entity-mappings>
<persistence-unit-metadata>
<persistence-unit-defaults>
<schema name="ACME"/>
</persistence-unit-defaults>
</persistence-unit-metadata>
....
</entity-mappings>
<entity-mappings>
<schema name="ACME"/>
...
</entity-mappings>