跳转到内容

Java 持久化/继承

来自 Wikibooks,开放世界中的开放书籍
继承的示例。SmallProject 和 LargeProject 继承了它们共同父类 Project 的属性。

继承是面向对象编程和 Java 的基本概念。关系型数据库没有继承的概念,因此在数据库中持久化继承可能很棘手。由于关系型数据库没有继承的概念,因此没有标准方法在数据库中实现继承,因此持久化继承最难的部分是选择如何在数据库中表示继承。

JPA 定义了几种继承机制,主要通过 @Inheritance 注解或 <inheritance> 元素定义。InheritanceType 枚举定义了三种继承策略,SINGLE_TABLETABLE_PER_CLASSJOINED

单表 继承是默认值,每类表 是 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 {
}

单表继承 XML 示例

[编辑 | 编辑源代码]
<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 代码计算继承鉴别器。这可以通过使用 DescriptorCustomizerClassDescriptorInheritancePolicysetClassExtractor() 方法来实现。
Hibernate : 这可以通过使用 Hibernate @DiscriminatorFormula 注解来实现。这允许使用数据库特定的 SQL 或函数来计算鉴别器值。

非空属性

[编辑 | 编辑源代码]
子类不能将属性定义为不允许为空,因为其他子类必须将空值插入这些列中。解决此问题的一种方法是,不要在列上定义非空约束,而是定义一个表约束,该约束检查鉴别器值和非空值。一般来说,最好的解决方案是接受没有约束(很可能您在生活中已经有足够的约束要处理)。

连接、多表继承

[编辑 | 编辑源代码]

连接继承是最合乎逻辑的继承解决方案,因为它反映了数据模型中的对象模型。在连接继承中,为继承层次结构中的每个类定义一个表,以存储该类的本地属性。层次结构中的每个表还必须存储对象的 ID(主键),该 ID在根类中定义。层次结构中的所有类必须共享相同的 ID 属性。鉴别器列用于确定特定行属于哪个类,层次结构中的每个类都定义了自己的唯一鉴别器值。

一些 JPA 提供商支持有或没有鉴别器列的连接继承,一些需要鉴别器列,而一些不支持鉴别器列。因此,连接继承似乎尚未完全标准化。

Hibernate: 在联接继承中,判别器列受支持,但不是必需的。[1]

数据库中联接继承表的示例

[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 : 支持两种查询机制。默认情况下使用多查询机制。可以通过使用 DescriptorCustomizerClassDescriptorInheritancePolicysetShouldOuterJoinSubclasses() 方法来使用外联接。

没有/不想为每个子类创建一个表

[edit | edit source]
大多数继承层次结构不符合联接单表继承策略。通常,所需的策略介于两者之间,在某些子类中具有联接表,而在其他子类中没有。不幸的是,JPA 不直接支持这一点。一种解决方法是将你的继承层次结构映射为单表,然后在子类中添加额外的表,或者通过在每个子类中定义 TableSecondaryTable 来实现,具体取决于需要。根据你的 JPA 提供者,这可能有效(不要忘记牺牲鸡)。如果无效,那么你可能需要使用 JPA 提供者特定的解决方案(如果你的提供者存在此类解决方案),否则在仅具有单表或每个子类一个表的限制范围内生存。你也可以更改继承层次结构,使其匹配你的数据模型,因此,如果子类没有表,那么将其类折叠到其超类中。

没有类鉴别器列

[edit | edit source]
如果你正在映射到现有的数据库模式,你的表可能没有类判别器列。一些 JPA 提供者在使用联接继承策略时不需要类判别器,因此这可能是一种解决方案。否则,你需要某种方法来确定行的类。有时可以从多个列计算继承值,或者存在判别器,但没有从值到类的单一映射。一些 JPA 提供者为此提供了扩展支持。另一种选择是创建一个制造判别器列的数据库视图,然后将你的层次结构映射到该视图而不是表。
TopLink / EclipseLink : 支持通过 Java 代码计算继承鉴别器。这可以通过使用 DescriptorCustomizerClassDescriptorInheritancePolicysetClassExtractor() 方法来实现。
Hibernate : 这可以通过使用 Hibernate @DiscriminatorFormula 注解来实现。这允许使用数据库特定的 SQL 或函数来计算鉴别器值。

高级

[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 {
}

示例映射的超类 XML

[编辑 | 编辑源代码]
<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中删除,如果其他子类需要它,则将它添加到每个子类中,或者创建一个具有fooBarFooBar子类,让其他子类扩展它。
一些 JPA 提供程序可能提供更宽松的继承方式。
TopLink / EclipseLink : 允许子类删除映射、重新定义映射或完全独立于其超类。这可以通过使用DescriptorCustomizer并删除ClassDescriptor的映射、添加具有相同属性名称的映射或删除InheritancePolicy来实现。
华夏公益教科书