跳轉到內容

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來實現。
華夏公益教科書