Java 持久化/继承
继承是面向对象编程和 Java 的基本概念。关系型数据库没有继承的概念,因此在数据库中持久化继承可能很棘手。由于关系型数据库没有继承的概念,因此没有标准方法在数据库中实现继承,因此持久化继承最难的部分是选择如何在数据库中表示继承。
JPA 定义了几种继承机制,主要通过 @Inheritance
注解或 <inheritance>
元素定义。InheritanceType
枚举定义了三种继承策略,SINGLE_TABLE
、TABLE_PER_CLASS
和 JOINED
。
单表 继承是默认值,每类表 是 JPA 规范的可选功能,因此并非所有提供商都支持它。JPA 还定义了通过 @MappedSuperclass
注解或 <mapped-superclass>
元素定义的映射超类概念。映射超类不是持久化类,但允许为其子类定义常见映射。
单表继承是最简单、通常也是性能最佳、最佳的解决方案。在单表继承中,使用单个表来存储整个继承层次结构中所有实例。该表将拥有每个类中每个属性的列。鉴别器列用于确定特定行属于哪个类,层次结构中的每个类都定义了自己的唯一鉴别器值。
PROJECT (表)
ID | PROJ_TYPE | NAME | BUDGET |
1 | L | Accounting | 50000 |
2 | S | Legal | null |
@Entity
@Inheritance
@DiscriminatorColumn(name="PROJ_TYPE")
@Table(name="PROJECT")
public class Project implements Serializable{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
...
}
@Entity
@DiscriminatorValue("L")
public class LargeProject extends Project {
private BigDecimal budget;
}
@Entity
@DiscriminatorValue("S")
public class SmallProject extends Project {
}
<entity name="Project" class="org.acme.Project" access="FIELD">
<table name="PROJECT"/>
<inheritance/>
<discriminator-column name="PROJ_TYPE"/>
<attributes>
<id name="id"/>
...
</attributes>
</entity>
<entity name="LargeProject" class="org.acme.LargeProject" access="FIELD">
<discriminator-value>L</discriminator-value>
...
</entity>
<entity name="SmallProject" class="org.acme.SmallProject" access="FIELD">
<discriminator-value>S</discriminator-value>
</entity>
- 如果您正在映射到现有的数据库模式,您的表可能没有类鉴别器列。一些 JPA 提供商在使用连接 继承策略时不需要类鉴别器,因此这可能是一个解决方案。否则,您需要某种方法来确定行的类。有时继承的值可以从多个列中计算出来,或者存在鉴别器,但值与类之间没有一对一的映射。一些 JPA 提供商为此提供扩展支持。另一种选择是创建一个数据库视图,该视图生成鉴别器列,然后将您的层次结构映射到该视图而不是表。一般来说,最好的解决方案只是在表中添加一个鉴别器列(说实话,ALTER TABLE 是您在 ORM 中最好的朋友)。
- TopLink / EclipseLink : 支持通过 Java 代码计算继承鉴别器。这可以通过使用
DescriptorCustomizer
和ClassDescriptor
的InheritancePolicy
的setClassExtractor()
方法来实现。
- TopLink / EclipseLink : 支持通过 Java 代码计算继承鉴别器。这可以通过使用
- Hibernate : 这可以通过使用 Hibernate
@DiscriminatorFormula
注解来实现。这允许使用数据库特定的 SQL 或函数来计算鉴别器值。
- Hibernate : 这可以通过使用 Hibernate
- 子类不能将属性定义为不允许为空,因为其他子类必须将空值插入这些列中。解决此问题的一种方法是,不要在列上定义非空约束,而是定义一个表约束,该约束检查鉴别器值和非空值。一般来说,最好的解决方案是接受没有约束(很可能您在生活中已经有足够的约束要处理)。
连接继承是最合乎逻辑的继承解决方案,因为它反映了数据模型中的对象模型。在连接继承中,为继承层次结构中的每个类定义一个表,以存储该类仅的本地属性。层次结构中的每个表还必须存储对象的 ID(主键),该 ID仅在根类中定义。层次结构中的所有类必须共享相同的 ID 属性。鉴别器列用于确定特定行属于哪个类,层次结构中的每个类都定义了自己的唯一鉴别器值。
一些 JPA 提供商支持有或没有鉴别器列的连接继承,一些需要鉴别器列,而一些不支持鉴别器列。因此,连接继承似乎尚未完全标准化。
数据库中联接继承表的示例
[edit | edit source]PROJECT (表)
ID | PROJ_TYPE | NAME |
1 | L | Accounting |
2 | S | Legal |
SMALLPROJECT (表)
ID |
2 |
LARGEPROJECT (表)
ID | BUDGET |
1 | 50000 |
联接继承注解示例
[edit | edit source]@Entity
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="PROJ_TYPE")
@Table(name="PROJECT")
public abstract class Project {
@Id
private long id;
private String name;
...
}
@Entity
@DiscriminatorValue("L")
@Table(name="LARGEPROJECT")
public class LargeProject extends Project {
private BigDecimal budget;
}
@Entity
@DiscriminatorValue("S")
@Table(name="SMALLPROJECT")
public class SmallProject extends Project {
}
联接继承 XML 示例
[edit | edit source]<entity name="Project" class="org.acme.Project" access="FIELD">
<table name="PROJECT"/>
<inheritance strategy="JOINED"/>
<discriminator-column name="PROJ_TYPE"/>
<attributes>
<id name="id"/>
...
</attributes>
</entity>
<entity name="LargeProject" class="org.acme.LargeProject" access="FIELD">
<table name="LARGEPROJECT"/>
<discriminator-value>L</discriminator-value>
...
</entity>
<entity name="SmallProject" class="org.acme.SmallProject" access="FIELD">
<table name="SMALLPROJECT"/>
<discriminator-value>S</discriminator-value>
</entity>
常见问题
[edit | edit source]查询性能低下
[edit | edit source]- 联接模型的主要缺点是,查询任何类都需要联接查询。查询根类或分支类甚至更困难,因为需要多个查询,或者需要外联接或联合查询。一种解决方案是使用单表继承,如果类有许多共同点,这很好,但如果它是大型层次结构,并且子类几乎没有共同点,那么这可能不理想。另一种解决方案是删除继承,而是使用
MappedSuperclass
,但这意味着你不能再查询或拥有该类的关系。
- 性能最差的查询将是针对根类或分支类的查询。避免对根类和分支类进行查询和建立关系将有助于减轻这种负担。如果你必须查询根类或分支类,JPA 提供者使用两种方法,一种是外部联接所有子类表,另一种是先查询根表,然后直接查询所需的子类表。第一种方法的优点是只需要一个查询,第二种方法的优点是避免了外联接,外联接通常在数据库中性能很差。你可能希望尝试每种方法,以确定哪种机制在你的应用程序中更有效,并查看你的 JPA 提供者是否支持该机制。通常,多查询机制更有效,但这通常取决于数据库连接的速度。
- TopLink / EclipseLink : 支持两种查询机制。默认情况下使用多查询机制。可以通过使用
DescriptorCustomizer
和ClassDescriptor
的InheritancePolicy
的setShouldOuterJoinSubclasses()
方法来使用外联接。
- TopLink / EclipseLink : 支持两种查询机制。默认情况下使用多查询机制。可以通过使用
没有/不想为每个子类创建一个表
[edit | edit source]- 大多数继承层次结构不符合联接或单表继承策略。通常,所需的策略介于两者之间,在某些子类中具有联接表,而在其他子类中没有。不幸的是,JPA 不直接支持这一点。一种解决方法是将你的继承层次结构映射为单表,然后在子类中添加额外的表,或者通过在每个子类中定义
Table
或SecondaryTable
来实现,具体取决于需要。根据你的 JPA 提供者,这可能有效(不要忘记牺牲鸡)。如果无效,那么你可能需要使用 JPA 提供者特定的解决方案(如果你的提供者存在此类解决方案),否则在仅具有单表或每个子类一个表的限制范围内生存。你也可以更改继承层次结构,使其匹配你的数据模型,因此,如果子类没有表,那么将其类折叠到其超类中。
没有类鉴别器列
[edit | edit source]- 如果你正在映射到现有的数据库模式,你的表可能没有类判别器列。一些 JPA 提供者在使用联接继承策略时不需要类判别器,因此这可能是一种解决方案。否则,你需要某种方法来确定行的类。有时可以从多个列计算继承值,或者存在判别器,但没有从值到类的单一映射。一些 JPA 提供者为此提供了扩展支持。另一种选择是创建一个制造判别器列的数据库视图,然后将你的层次结构映射到该视图而不是表。
- TopLink / EclipseLink : 支持通过 Java 代码计算继承鉴别器。这可以通过使用
DescriptorCustomizer
和ClassDescriptor
的InheritancePolicy
的setClassExtractor()
方法来实现。
- TopLink / EclipseLink : 支持通过 Java 代码计算继承鉴别器。这可以通过使用
- Hibernate : 这可以通过使用 Hibernate
@DiscriminatorFormula
注解来实现。这允许使用数据库特定的 SQL 或函数来计算鉴别器值。
- Hibernate : 这可以通过使用 Hibernate
高级
[edit | edit source]每类表继承
[edit | edit source]每类表继承允许在对象模型中使用继承,而数据模型中不存在继承。在每类表继承中,为继承层次结构中的每个具体类定义一个表,以存储该类所有属性及其所有超类的属性。谨慎使用此策略,因为它在 JPA 规范中是可选的,并且查询根类或分支类可能非常困难且效率低下。
数据库中每类表继承表的示例
[edit | edit source]SMALLPROJECT (表)
ID | NAME |
2 | Legal |
LARGEPROJECT (表)
ID | NAME | BUDGET |
1 | Accounting | 50000 |
每类表继承注解示例
[edit | edit source]@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public abstract class Project {
@Id
private long id;
...
}
@Entity
@Table(name="LARGEPROJECT")
public class LargeProject extends Project {
private BigDecimal budget;
}
@Entity
@Table(name="SMALLPROJECT")
public class SmallProject extends Project {
}
每类表继承 XML 示例
[edit | edit source]<entity name="Project" class="org.acme.Project" access="FIELD">
<inheritance strategy="TABLE_PER_CLASS"/>
<attributes>
<id name="id"/>
...
</attributes>
</entity>
<entity name="LargeProject" class="org.acme.LargeProject" access="FIELD">
<table name="LARGEPROJECT"/>
...
</entity>
<entity name="SmallProject" class="org.acme.SmallProject" access="FIELD">
<table name="SMALLPROJECT"/>
</entity>
常见问题
[edit | edit source]查询性能低下
[edit | edit source]- 每类表模型的主要缺点是,对根类或分支类进行查询或建立关系会变得很昂贵。查询根类或分支类需要多个查询,或联合查询。一种解决方案是使用单表继承,如果类有许多共同点,这很好,但如果它是大型层次结构,并且子类几乎没有共同点,那么这可能不理想。另一种解决方案是删除每类表继承,而是使用
MappedSuperclass
,但这意味着你不能再查询或拥有该类的关系。
排序和联接问题
[edit | edit source]- 由于每类表继承需要多个查询或联合查询,因此你不能在查询中联接、获取联接或遍历它们。此外,当使用排序时,结果将按类排序,然后按排序顺序排序。这些限制取决于你的 JPA 提供者,一些 JPA 提供者可能具有其他限制,或者根本不支持每类表,因为它在 JPA 规范中是可选的。
映射超类
[edit | edit source]映射超类继承允许在对象模型中使用继承,而数据模型中不存在继承。它类似于每类表继承,但不允许查询、持久化或与超类建立关系。它的主要目的是允许映射信息被其子类继承。子类负责定义表、id 和其他信息,并且可以修改任何继承的映射。映射超类的一种常见用法是为你的应用程序定义一个通用的 PersistentObject
,以定义通用行为和映射,例如 id 和版本。映射超类通常应该是一个抽象类。映射超类不是 Entity
,而是通过 @MappedSuperclass
注解或 <mapped-superclass>
元素定义的。
SMALLPROJECT (表)
ID | NAME |
2 | Legal |
LARGEPROJECT (表)
ID | PROJECT_NAME | BUDGET |
1 | Accounting | 50000 |
@MappedSuperclass
public abstract class Project {
@Id
private long id;
@Column(name="NAME")
private String name;
...
}
@Entity
@Table(name="LARGEPROJECT")
@AttributeOverride(name="NAME", column=@Column(name="PROJECT_NAME"))
public class LargeProject extends Project {
private BigDecimal budget;
}
@Entity
@Table("SMALLPROJECT")
public class SmallProject extends Project {
}
<mapped-superclass class="org.acme.Project" access="FIELD">
<attributes>
<id name="id"/>
<basic name="name">
<column name="NAME"/>
</basic>
...
</attributes>
</mapped-superclass>
<entity name="LargeProject" class="org.acme.LargeProject" access="FIELD">
<table name="LARGEPROJECT"/>
<attribute-override name="name">
<column name="PROJECT_NAME"/>
</attribute-override>
...
</entity>
<entity name="SmallProject" class="org.acme.SmallProject" access="FIELD">
<table name="SMALLPROJECT"/>
</entity>
- 映射的超类主要缺点是它们无法被查询或持久化。您也不能与映射的超类建立关系。如果需要执行这些操作,则必须使用其他继承模型,例如每类表,该模型实际上与映射的超类相同,只是可能没有这些限制。另一种选择是更改您的模型,以便您的类不与超类建立关系,例如将关系更改为子类,或删除关系,并通过查询每个可能的子类并在 Java 中收集结果来查询其值。
- 有时,您会遇到需要与父类以不同方式映射的子类,或者与父类相似,但缺少某个字段,或使用方式截然不同。不幸的是,在 JPA 中很难不从父类继承所有内容,您可以覆盖映射,但不能删除映射,也不能更改映射类型或目标类。如果将映射定义为属性(get 方法)或通过 XML,您可能会尝试覆盖继承的映射或将其标记为
Transient
,这可能在 JPA 提供程序中起作用(别忘了祭奠一只鸡)。
- 另一种解决方案是实际修复对象模型中的继承关系。如果您从
Bar
继承foo
,但不想继承它,那么将其从Bar
中删除,如果其他子类需要它,则将它添加到每个子类中,或者创建一个具有foo
的Bar
的FooBar
子类,让其他子类扩展它。
- 一些 JPA 提供程序可能提供更宽松的继承方式。
- TopLink / EclipseLink : 允许子类删除映射、重新定义映射或完全独立于其超类。这可以通过使用
DescriptorCustomizer
并删除ClassDescriptor
的映射、添加具有相同属性名称的映射或删除InheritancePolicy
来实现。
- TopLink / EclipseLink : 允许子类删除映射、重新定义映射或完全独立于其超类。这可以通过使用