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 : 允許子類刪除對映、重新定義對映或完全獨立於其超類。這可以透過使用