Java 持久化/關係
關係是從一個物件到另一個物件的引用。在 Java 中,關係透過從源物件到目標物件的 物件引用(指標)來定義。從技術上講,在 Java 中,對另一個物件的關係與對 String 或 Date 等資料屬性的“關係”之間沒有區別(基本型別有所不同),因為兩者都是指標;但是,在邏輯上和出於持久化的目的,資料屬性被認為是物件的一部分,而對其他持久物件的引用被認為是關係。
在關係資料庫中,關係透過外部索引鍵來定義。源行包含目標行的主鍵以定義關係(有時還有反向關係)。必須執行查詢以使用外部索引鍵和主鍵資訊讀取關係的目標物件。
在 Java 中,如果關係是到其他物件的集合,則 Collection 或陣列型別用於在 Java 中儲存關係的內容。在關係資料庫中,集合關係要麼透過目標物件具有指向源物件主鍵的反向外部索引鍵來定義,要麼透過具有中間聯接表來儲存關係(兩個物件的主鍵)。
Java 和 JPA 中的所有關係都是單向的,這意味著如果源物件引用目標物件,則不能保證目標物件也與源物件有關係。這與關係資料庫不同,在關係資料庫中,關係透過外部索引鍵和查詢來定義,這樣反向查詢總是存在的。
- 一對一 - 從一個物件到另一個物件的唯一引用,是
OneToOne的反向關係。 - 多對一 - 從一個物件到另一個物件的引用,是
OneToMany的反向關係。 - 一對多 - 物件的
Collection或Map,是ManyToOne的反向關係。 - 多對多 - 物件的
Collection或Map,是ManyToMany的反向關係。 - 嵌入式 - 對共享父級相同表的物件的引用。
- 元素集合 - JPA 2.0,
Basic或Embeddable物件的Collection或Map,儲存在單獨的表中。
這涵蓋了大多數物件模型中存在的絕大多數型別的關係。每種關係型別也涵蓋多種不同的實現,例如 OneToMany 允許聯接表或目標中的外部索引鍵,而集合對映也允許 Collection 型別和 Map 型別。還有一些其他可能的複雜關係型別,請參閱 高階關係。
檢索和構建物件關係的成本遠遠超過選擇物件的成本。對於像 manager 或 managedEmployees 這樣的關係來說尤其如此,如果選擇任何員工,它將透過關係層次結構觸發所有員工的載入。顯然,這是一件壞事,但是物件中擁有關係是非常可取的。
解決這個問題的辦法是延遲載入(延遲載入)。延遲載入允許延遲載入關係,直到訪問它為止。這不僅對於避免資料庫訪問很重要,而且對於避免在不需要時構建物件的成本也很重要。
在 JPA 中,可以使用 `fetch` 屬性在任何關係上設定延遲載入。`fetch` 可以設定為 `LAZY` 或 `EAGER`,如 FetchType 列舉中定義的那樣。除了 `OneToOne` 和 `ManyToOne` 之外,所有關係的預設獲取型別都是 `LAZY`,但通常最好將所有關係都設定為 `LAZY`。`OneToOne` 和 `ManyToOne` 的 `EAGER` 預設值是出於實現原因(更難實現),而不是因為它是一個好主意。從技術上講,在 JPA 中 `LAZY` 只是一個提示,JPA 提供者不需要支援它,但在現實中,所有主要的 JPA 提供者都支援它,如果它們不支援它,它們將毫無用處。
延遲一對一關係註解示例
[edit | edit source]@Entity
public class Employee {
@Id
private long id;
...
@OneToOne(fetch=FetchType.LAZY)
@JoinColumn(name="ADDR_ID")
private Address address;
...
}
延遲一對一關係 XML 示例
[edit | edit source]<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-one name="address" fetch="LAZY">
<join-column name="ADDR_ID"/>
</one-to-one>
</attributes>
</entity>
魔法
[edit | edit source]延遲載入通常涉及 JPA 提供者中的某種“魔法”,以透明地將關係載入到記憶體中,當它們被訪問時。對於集合關係,典型的魔法是 JPA 提供者將其關係設定為自己的 `Collection`、`List`、`Set` 或 `Map` 實現。當在這個集合代理上訪問任何(或大多數)方法時,它將載入真實的集合並轉發該方法。這就是為什麼 JPA 要求所有集合關係使用其中一個集合介面(儘管一些 JPA 提供者也支援集合實現)。
對於 `OneToOne` 和 `ManyToOne` 關係,魔法通常涉及對實體類的某種位元組碼操作,或建立子類。這允許訪問欄位或 get/set 方法被攔截,並在允許訪問值之前先檢索關係。一些 JPA 提供者使用不同的方法,例如將引用包裝在代理物件中,儘管這可能導致 `null` 值和原始方法的問題。為了執行位元組碼魔法,通常需要一個代理或後處理器。確保您正確使用提供者的代理或後處理器,否則延遲可能無法正常工作。您還可能在偵錯程式中注意到額外的變數,但總的來說,除錯仍然可以正常工作。
基礎
[edit | edit source]`Basic` 屬性也可以設定為 `LAZY`,但這通常與延遲關係不同,應該避免,除非該屬性很少被訪問。
參見 基本屬性:延遲載入。
序列化和分離
[edit | edit source]延遲關係的一個主要問題是確保物件被分離或序列化後,關係仍然可用。對於大多數 JPA 提供者,在序列化之後,任何沒有例項化的延遲關係都會被破壞,並且在被訪問時要麼丟擲錯誤,要麼返回 null。
一個簡單的解決方案是將所有關係都設定為 eager。序列化與持久化面臨著相同的問題,如果您沒有延遲關係,很容易序列化整個資料庫。因此,延遲關係對於序列化來說和對於資料庫訪問一樣必要;但是,您需要確保您在序列化之前例項化了您在序列化之後需要的所有內容。您可以只將您認為在序列化之後需要的關係標記為 `EAGER`;這將起作用,但可能存在許多情況下,您不需要這些關係。
第二個解決方案是在返回物件以進行序列化之前,訪問您需要的任何關係。這具有使用案例特定的優點,因此不同的使用案例可以例項化不同的關係。對於集合關係,傳送 `size()` 通常是確保延遲關係被例項化的最佳方法。對於 `OneToOne` 和 `ManyToOne` 關係,通常只要訪問關係就足夠了(例如 `employee.getAddress()`),儘管對於使用代理的一些 JPA 提供者,您可能需要向物件傳送一條訊息(例如 `employee.getAddress().hashCode()`)。
第三個解決方案是使用 JPQL `JOIN FETCH` 在查詢物件時查詢關係。`JOIN FETCH` 通常可以確保關係已被例項化。但是,在使用 `JOIN FETCH` 時應該謹慎,因為它在集合關係上使用時會變得效率低下,尤其是在多個集合關係上使用時,因為它需要在資料庫上進行 n^2 連線。
一些 JPA 提供者還可能提供某些查詢提示或其他序列化選項。
在沒有序列化的情況下,也會出現相同的問題,如果在事務結束之後訪問一個分離的物件。一些 JPA 提供者允許在事務結束之後或在 `EntityManager` 關閉之後訪問延遲關係,但有些提供者不允許。如果您的 JPA 提供者不允許這樣做,那麼您可能需要確保在結束事務之前,您已經例項化了您將需要的所有延遲關係。
急切連接獲取
[edit | edit source]一個常見的誤解是 `EAGER` 意味著應該連接獲取關係,即在與源物件相同的 SQL `SELECT` 語句中檢索關係。一些 JPA 提供者確實以這種方式實現了 eager。但是,僅僅因為某件事需要被載入,並不意味著它應該被連接獲取。考慮 `Employee` - `Phone`,`Phone` 的員工引用被設定為 `EAGER`,因為員工幾乎總是先於電話被載入。但是,在載入電話時,您不想連線員工,員工已經被讀取並且已經存在於快取或持久化上下文中。同樣,僅僅因為您想要載入兩個集合關係,並不意味著您想要連接獲取它們,這將導致一個非常低效的連線,它將返回 n^2 資料。
連接獲取是 JPA 目前只通過 JPQL 提供的功能,這通常是正確的地方,因為每個使用案例都有不同的關係要求。一些 JPA 提供者還在對映級別提供了一個連接獲取選項,以便始終連接獲取關係,但這通常與 `EAGER` 不一樣。連接獲取通常不是載入關係最有效的方式,通常批處理讀取關係在您的 JPA 提供者支援的情況下效率更高。
參見 連接獲取
參見 批處理讀取
級聯
[edit | edit source]關係對映有一個 `cascade` 選項,允許關係級聯到常見的操作。`cascade` 通常用於模擬依賴關係,例如 `Order` -> `OrderLine`。級聯 `orderLines` 關係允許 `Order` -> `OrderLine` 與它們的父級一起被持久化、刪除、合併。
以下操作可以級聯,如 CascadeType 列舉中定義的那樣
PERSIST- 級聯 `EntityManager.persist()` 操作。如果對父級呼叫 `persist()`,並且子級也是新的,它也將被持久化。如果它已經存在,將不會發生任何事情,儘管對現有物件呼叫 `persist()` 仍然會將持久化操作級聯到其依賴項。如果您持久化一個物件,並且它與一個新物件相關,並且關係沒有級聯持久化,那麼將發生異常。這可能需要您先對相關物件呼叫持久化,然後再將其與父級相關聯。總的來說,它可能看起來很奇怪,或者希望始終級聯持久化操作,如果一個新物件與另一個物件相關,那麼它可能應該被持久化。在每個關係上始終級聯持久化可能沒有重大問題,儘管它可能會影響效能。不需要在相關物件上呼叫持久化,在提交時,任何其關係是級聯持久化的相關物件都將自動持久化。預先呼叫持久化的優點是,任何生成的 id 都將(除非使用標識)被分配,並且將引發 `prePersist` 事件。REMOVE- 級聯EntityManager.remove()操作。如果在父級上呼叫remove(),則子級也將被刪除。這應該只用於依賴關係。請注意,只有remove()操作是級聯的,如果您從OneToMany集合中刪除一個依賴物件,它不會被刪除,JPA 要求您顯式呼叫remove()。一些 JPA 提供者可能支援一個選項,即從依賴集合中刪除的物件將被刪除,JPA 2.0 也為此定義了一個選項。MERGE- 級聯EntityManager.merge()操作。如果在父級上呼叫merge(),則子級也將被合併。這通常應該用於依賴關係。請注意,這隻會影響合併的級聯,關係引用本身始終會被合併。如果使用transient變數來限制序列化,這可能會成為一個主要問題,在這種情況下,您可能需要手動合併或重置transient關係。一些 JPA 提供者提供額外的merge操作。REFRESH- 級聯EntityManager.refresh()操作。如果在父級上呼叫refresh(),則子級也將被重新整理。這通常應該用於依賴關係。在為所有關係啟用此功能時要小心,因為它會導致對其他物件的更改被重置。ALL- 級聯所有上述操作。
@Entity
public class Employee {
@Id
private long id;
...
@OneToOne(cascade={CascadeType.ALL})
@JoinColumn(name="ADDR_ID")
private Address address;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-one name="address">
<join-column name="ADDR_ID"/>
<cascade>
<cascade-all/>
</cascade>
</one-to-one>
</attributes>
</entity>
remove 操作的級聯僅在對物件呼叫 remove 時發生。這通常不是依賴關係中想要的。如果相關物件不能在沒有源物件的情況下存在,那麼通常希望在源物件被刪除時刪除它們,而且也希望在它們不再被源物件引用時刪除它們。JPA 1.0 沒有為此提供選項,因此當從源關係中刪除依賴物件時,必須從 EntityManager 中顯式刪除它。JPA 2.0 在 OneToMany 和 OneToOne 註釋和 XML 中提供了一個 orphanRemoval 選項。孤兒刪除將確保從關係中不再引用的任何物件都將從資料庫中刪除。
@Entity
public class Employee {
@Id
private long id;
...
@OneToMany(orphanRemoval=true, cascade={CascadeType.ALL})
private List<PhoneNumbers> phones;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-many name="phones" orphan-removal="true">
<cascade>
<cascade-all/>
</cascade>
</one-to-many>
</attributes>
</entity>
關係對映有一個 targetEntity 屬性,它允許指定關係的引用類(目標)。這通常不需要設定,因為它從欄位型別、get 方法返回值型別或集合的泛型型別中默認得出。
如果您的欄位使用公共介面型別,也可以使用它,例如欄位是介面 Address,但對映需要對映到實現類 AddressImpl。另一種用法是,如果您的欄位是超類型別,但您想將關係對映到子類。
@Entity
public class Employee {
@Id
private long id;
...
@OneToMany(targetEntity=Phone.class)
@JoinColumn(name="OWNER_ID")
private List phones;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-many name="phones" target-entity="org.acme.Phone">
<join-column name="OWNER_ID"/>
</one-to-many>
</attributes>
</entity>
集合對映包括 OneToMany、ManyToMany,以及 JPA 2.0 中的 ElementCollection。JPA 要求集合欄位或 get/set 方法的型別是 Java 集合介面之一,Collection、List、Set 或 Map。
您的欄位不應該為集合實現型別,例如 ArrayList。一些 JPA 提供者可能支援使用集合實現,許多提供者支援 EAGER 集合關係來使用實現類。您可以將任何實現設定為集合的例項值,但在從資料庫中讀取物件時,如果它是 LAZY,JPA 提供者通常會放入一個特殊的 LAZY 集合。
Java 中的 List 支援重複條目,而 Set 不支援。在資料庫中,通常不支援重複項。從技術上講,如果使用 JoinTable,這可能是可能的,但 JPA 不要求支援重複項,大多數提供者也不支援。
如果您需要重複支援,您可能需要建立一個代表並對映到連線表的物件。該物件仍然需要一個唯一的 Id,例如 GeneratedValue。參見 對映帶有附加列的連線表。
JPA 允許在檢索時按資料庫對集合值進行排序。這是透過 @OrderBy 註釋或 <order-by> XML 元素完成的。
OrderBy 的值是 JPQL ORDER BY 字串。這可以是一個屬性名稱,後面跟著 ASC 或 DESC,表示升序或降序排序。您還可以使用路徑或巢狀屬性,或使用 "," 表示多個屬性。如果未給出 OrderBy 值,則假設為目標物件的 Id。
OrderBy 值必須是目標物件的對映屬性。如果您想要一個有序的 List,您需要在目標物件中新增一個 index 屬性,並在其表中新增一個 index 列。您還必須確保設定索引值。JPA 2.0 將擴充套件支援使用 OrderColumn 的有序 List。
請注意,使用OrderBy不會保證集合在記憶體中排序。您有責任以正確的順序新增到集合中。Java 定義了SortedSet 介面和TreeSet 集合實現,它們確實會維護一個順序。JPA 並不特別支援SortedSet,但一些 JPA 提供者可能會允許您將SortedSet 或TreeSet 用於您的集合型別,並維護正確的排序。預設情況下,這些要求您的目標物件實現Comparable 介面或設定Comparator。您還可以使用Collections.sort() 方法在需要時對List 進行排序。在記憶體中進行排序的一種選擇是使用屬性訪問,並在您的 set 和 add 方法中呼叫Collections.sort()。
@Entity
public class Employee {
@Id
private long id;
...
@OneToMany
@OrderBy("areaCode")
private List<Phone> phones;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-many name="phones">
<order-by>areaCode</order-by>
</one-to-many>
</attributes>
</entity>
JPA 2.0 添加了對OrderColumn 的支援。OrderColumn 可用於定義任何集合對映上的排序List。它透過@OrderColumn 註解或<order-column> XML 元素定義。
OrderColumn 由對映維護,不應該作為目標物件的屬性。OrderColumn 的表取決於對映。對於OneToMany 對映,它將在目標物件的表中。對於ManyToMany 對映或使用JoinTable 的OneToMany,它將在聯接表中。對於ElementCollection 對映,它將在目標表中。
EMPLOYEE (表)
| ID | FIRSTNAME | LASTNAME | SALARY |
| 1 | Bob | Way | 50000 |
| 2 | Sarah | Smith | 60000 |
EMPLOYEE_PHONE (表)
| EMPLOYEE_ID | PHONE_ID | INDEX |
| 1 | 1 | 0 |
| 1 | 3 | 1 |
| 2 | 2 | 0 |
| 2 | 4 | 1 |
PHONE(表)
| ID | AREACODE | NUMBER |
| 1 | 613 | 792-7777 |
| 2 | 416 | 798-6666 |
| 3 | 613 | 792-9999 |
| 4 | 416 | 798-5555 |
@Entity
public class Employee {
@Id
private long id;
...
@OneToMany
@OrderColumn(name="INDEX")
private List<Phone> phones;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-many name="phones">
<order-column name="INDEX"/>
</one-to-many>
</attributes>
</entity>
雙向關係中的一個常見問題是應用程式更新關係的一方,但另一方沒有更新,並且變得不同步。在 JPA 中,就像在一般 Java 中一樣,應用程式或物件模型負責維護關係。如果您的應用程式向關係的一方新增內容,則必須向另一方新增內容。
這通常透過物件模型中的add 或set 方法來解決,這些方法處理關係的雙方,因此應用程式程式碼不必擔心它。解決此問題的方法有兩種:您可以將關係維護程式碼新增到關係的一方,並且只使用該方的 setter(例如,使另一方受保護),或者將其新增到雙方並確保避免無限迴圈。
例如
public class Employee {
private List phones;
...
public void addPhone(Phone phone) {
this.phones.add(phone);
if (phone.getOwner() != this) {
phone.setOwner(this);
}
}
...
}
public class Phone {
private Employee owner;
...
public void setOwner(Employee employee) {
this.owner = employee;
if (!employee.getPhones().contains(this)) {
employee.getPhones().add(this);
}
}
...
}
雙向OneToOne 和ManyToMany 關係的程式碼類似。
有些人期望 JPA 提供者具有自動維護關係的魔力。這實際上是 EJB CMP 2 規範的一部分。但是問題是,如果物件被分離或序列化到另一個 VM,或者在物件被管理之前建立了新的關係,或者在 JPA 範圍之外使用物件模型,那麼魔力就會消失,應用程式就必須自己解決問題,因此總的來說,最好將程式碼新增到物件模型中。但是一些 JPA 提供者確實支援自動維護關係。
在某些情況下,在新增子物件時不希望例項化大型集合。一種解決方案是不對映雙向關係,而是根據需要查詢它。一些 JPA 提供者還優化了它們的延遲集合物件以處理這種情況,因此您仍然可以向集合新增內容而不例項化它。
導致效能低下的最常見問題是使用EAGER 關係。這要求在讀取源物件時讀取相關物件。例如,使用EAGER managedEmployees 讀取公司總裁會導致讀取公司的所有Employee。解決方案是始終將所有關係設為LAZY。預設情況下,OneToMany 和ManyToMany 是LAZY,但OneToOne 和ManyToOne 不是,因此請確保將其配置為LAZY。請參閱延遲獲取。有時您配置了LAZY,但它不起作用,請參閱延遲不起作用。
另一個常見問題是n+1 問題。例如,假設您讀取所有Employee 物件,然後訪問它們的Address。由於每個Address 都被單獨訪問,因此這會導致 n+1 個查詢,這會成為一個主要的效能問題。這可以透過聯接獲取 和批次讀取 來解決。
延遲OneToOne 和ManyToOne 關係通常需要某種形式的編織或位元組碼生成。通常在 JSE 中執行時,需要agent 選項來允許位元組碼編織,因此請確保您已正確配置代理。一些 JPA 提供者執行動態子類生成,因此不需要代理。
代理示例
java -javaagent:eclipselink.jar ...
一些 JPA 提供者還提供靜態編織,或者除了動態編織之外。對於靜態編織,必須在您的 JPA 類上執行一些預處理器。
在 JEE 中執行時,延遲通常可以正常工作,因為 EJB 規範需要類載入器鉤子。但是一些 JEE 提供者可能不支援此功能,因此可能需要靜態編織。
另外,請確保您不要在不應該訪問關係時訪問關係。例如,如果您使用屬性訪問,並在您的 set 方法中訪問相關的延遲值,這會導致它被載入。要麼刪除 set 方法的副作用,要麼使用欄位訪問。
如果您的關係被標記為lazy,那麼如果它在物件被序列化之前沒有被例項化,那麼它可能不會被序列化。這可能會導致錯誤,或者在反序列化後訪問時返回null。
請參閱序列化和分離
從集合中刪除物件時,如果也希望從資料庫中刪除該物件,則必須在該物件上呼叫 remove()。在 JPA 1.0 中,即使關係是 cascade REMOVE,也仍然必須呼叫 remove(),只有父物件的刪除會被級聯,而不會從集合中刪除。
JPA 2.0 將提供一個選項,允許從集合中刪除觸發刪除。某些 JPA 提供商在 JPA 1.0 中支援此選項。
參見,級聯
@Entity
@Table(name ="Comment")
public class Comment {
@Id
@Column(name="Id")
@GeneratedValue(strategy=GenerationType.AUTO)
private int Id;
private int vehicleId;
private int userId;
private String post;
private Date timeStamp;
private double amountOffered = 0.0;
private boolean acceptOffer;
...
}
如果關係欄位的型別是類的公共介面,並且只有一個實現者,那麼解決方法很簡單,您只需要在對映中設定一個 targetEntity。參見,目標實體。
如果介面有多個實現者,那麼情況會更復雜。JPA 不直接支援對映介面。一個解決方案是將介面轉換為抽象類,並使用繼承來對映它。您也可以保留介面,建立抽象類,並確保每個實現者都擴充套件它,並將 targetEntity 設定為抽象類。
另一種解決方案是使用 get/set 方法為每個可能的實現者定義虛擬屬性,並分別對映它們,並將介面 get/set 標記為 transient。您也可以不對映屬性,而是在需要時查詢它。
參見,可變和異構關係
一些 JPA 提供商支援介面和可變關係。
- TopLink,EclipseLink : 透過他們的
@VariableOneToOne註釋和 XML 支援可變關係。對映到介面和查詢介面也透過他們的ClassDescriptor的InterfacePolicyAPI 支援。
- ElementCollection -
Collection或Map的Embeddable或Basic值。 - 對映列 -
OneToMany或ManyToMany或ElementCollection,其中包含一個Basic、Embeddable或Entity金鑰,該金鑰不是目標物件的一部分。 - 順序列 -
OneToMany或ManyToMany或ElementCollection現在可以包含一個OrderColumn,該列定義在使用List時集合的順序。 - 單向 OneToMany -
OneToMany不再需要定義ManyToOne反向關係。
- 可變 OneToOne,ManyToOne - 對介面或具有多個不同實現者的通用未對映繼承類的引用。
- 可變 OneToMany,ManyToMany -
Collection或Map的異構物件,這些物件共享一個介面或具有多個不同實現者的通用未對映繼承類。 - 巢狀集合關係,例如陣列的陣列,
List的List,或Map的Map,或其他此類組合。 - 物件關係資料型別 - 使用
STRUCT、VARRAY、REF或NESTEDTABLE型別儲存在資料庫中的關係。 - XML 關係 - 作為 XML 文件儲存的關係。
Java 定義了 Map 介面來表示其值為按金鑰索引的集合。有多種 Map 實現,最常見的是 HashMap,但也包括 Hashtable 和 TreeMap。
JPA 允許 Map 用於任何集合對映,包括 OneToMany、ManyToMany 和 ElementCollection。JPA 要求使用 Map 介面作為屬性型別,儘管一些 JPA 提供商也可能支援使用 Map 實現。
在 JPA 1.0 中,對映金鑰必須是集合值的對映屬性。可以使用 @MapKey 註釋或 <map-key> XML 元素定義對映關係。如果未指定 MapKey,則預設為目標物件的 Id。
@Entity
public class Employee {
@Id
private long id;
...
@OneToMany(mappedBy="owner")
@MapKey(name="type")
private Map<String, PhoneNumber> phoneNumbers;
...
}
@Entity
public class PhoneNumber {
@Id
private long id;
@Basic
private String type; // Either "home", "work", or "fax".
...
@ManyToOne
private Employee owner;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-many name="phoneNumbers" mapped-by="owner">
<map-key name="type"/>
</one-to-many>
</attributes>
</entity>
<entity name="PhoneNumber" class="org.acme.PhoneNumber" access="FIELD">
<attributes>
<id name="id"/>
<basic name="type"/>
<many-to-one name="owner"/>
</attributes>
</entity>
JPA 2.0 允許使用一個 Map,其中金鑰不是目標物件的一部分,可以被持久化。Map 金鑰可以是以下任何一個
- 一個
Basic值,儲存在目標表或連線表中。 - 一個
Embedded物件,儲存在目標表或連線表中。 - 另一個
Entity的外部索引鍵,儲存在目標表或連線表中。
對映列可用於任何集合對映,包括 OneToMany、ManyToMany 和 ElementCollection。
這使得可以靈活地使用複雜的數量,以便能夠對映不同的模型。使用的對映型別始終由 Map 的值決定,而不是金鑰。因此,如果金鑰是 Basic,但值是 Entity,則仍然使用 OneToMany 對映。但如果值是 Basic,但金鑰是 Entity,則使用 ElementCollection 對映。
這使得可以對映一些非常複雜的資料庫模式。例如,使用具有 MapKeyJoinColumn 的 ManyToMany 對映三方連線表,用於第三個外部索引鍵。對於 ManyToMany,金鑰始終儲存在 JoinTable 中。對於 OneToMany,如果定義了 JoinTable,則將其儲存在 JoinTable 中,否則將其儲存在目標 Entity 的表中,即使目標 Entity 不對映此列。對於 ElementCollection,金鑰儲存在元素表中。
可以使用 @MapKeyColumn 註釋或 <map-key-column> XML 元素定義金鑰為 Basic 值的對映關係,@MapKeyEnumerated 和 @MapKeyTemporal 也可用於 Enum 或 Calendar 型別。可以使用 @MapKeyJoinColumn 註釋或 <map-key-join-column> XML 元素定義金鑰為 Entity 值的對映關係,@MapKeyJoinColumns 也可用於複合外部索引鍵。註釋 @MapKeyClass 或 <map-key-class> XML 元素可在金鑰為 Embeddable 時使用,或用於指定目標類或型別(如果未使用泛型)。
EMPLOYEE (表)
| ID | FIRSTNAME | LASTNAME | SALARY |
| 1 | Bob | Way | 50000 |
| 2 | Sarah | Smith | 60000 |
PHONE(表)
| ID | OWNER_ID | PHONE_TYPE | AREACODE | NUMBER |
| 1 | 1 | home | 613 | 792-7777 |
| 2 | 1 | cell | 613 | 798-6666 |
| 3 | 2 | home | 416 | 792-9999 |
| 4 | 2 | fax | 416 | 798-5555 |
@Entity
public class Employee {
@Id
private long id;
...
@OneToMany(mappedBy="owner")
@MapKeyColumn(name="PHONE_TYPE")
private Map<String, Phone> phones;
...
}
@Entity
public class Phone {
@Id
private long id;
...
@ManyToOne
private Employee owner;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-many name="phones" mapped-by="owner">
<map-key-column name="PHONE_TYPE"/>
</one-to-many>
</attributes>
</entity>
<entity name="Phone" class="org.acme.Phone" access="FIELD">
<attributes>
<id name="id"/>
<many-to-one name="owner"/>
</attributes>
</entity>
EMPLOYEE (表)
| ID | FIRSTNAME | LASTNAME | SALARY |
| 1 | Bob | Way | 50000 |
| 2 | Sarah | Smith | 60000 |
PHONE(表)
| ID | OWNER_ID | PHONE_TYPE_ID | AREACODE | NUMBER |
| 1 | 1 | 1 | 613 | 792-7777 |
| 2 | 1 | 2 | 613 | 798-6666 |
| 3 | 2 | 1 | 416 | 792-9999 |
| 4 | 2 | 3 | 416 | 798-5555 |
PHONETYPE(表)
| ID | TYPE |
| 1 | home |
| 2 | cell |
| 3 | fax |
| 4 | work |
@Entity
public class Employee {
@Id
private long id;
...
@OneToMany(mappedBy="owner")
@MapKeyJoinColumn(name="PHONE_TYPE_ID")
private Map<PhoneType, Phone> phones;
...
}
@Entity
public class Phone {
@Id
private long id;
...
@ManyToOne
private Employee owner;
...
}
@Entity
public class PhoneType {
@Id
private long id;
...
@Basic
private String type;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-many name="phones" mapped-by="owner">
<map-key-join-column name="PHONE_TYPE_ID"/>
</one-to-many>
</attributes>
</entity>
<entity name="Phone" class="org.acme.Phone" access="FIELD">
<attributes>
<id name="id"/>
<many-to-one name="owner"/>
</attributes>
</entity>
<entity name="PhoneType" class="org.acme.PhoneType" access="FIELD">
<attributes>
<id name="id"/>
<basic name="type"/>
</attributes>
</entity>
EMPLOYEE (表)
| ID | FIRSTNAME | LASTNAME | SALARY |
| 1 | Bob | Way | 50000 |
| 2 | Sarah | Smith | 60000 |
EMPLOYEE_PHONE (表)
| EMPLOYEE_ID | PHONE_ID | TYPE |
| 1 | 1 | home |
| 1 | 2 | cell |
| 2 | 3 | home |
| 2 | 4 | fax |
PHONE (表)
| ID | AREACODE | NUMBER |
| 1 | 613 | 792-7777 |
| 2 | 613 | 798-6666 |
| 3 | 416 | 792-9999 |
| 4 | 416 | 798-5555 |
@Entity
public class Employee {
@Id
private long id;
...
@OneToMany
@MapKeyClass(PhoneType.class)
private Map<PhoneType, Phone> phones;
...
}
@Entity
public class Phone {
@Id
private long id;
...
}
@Embeddable
public class PhoneType {
@Basic
private String type;
...
}
<entity name="Employee" class="org.acme.Employee" access="FIELD">
<attributes>
<id name="id"/>
<one-to-many name="phones">
<map-key-class>PhoneType</map-key-class>
</one-to-many>
</attributes>
</entity>
<entity name="Phone" class="org.acme.Phone" access="FIELD">
<attributes>
<id name="id"/>
<many-to-one name="owner"/>
</attributes>
</entity>
<embeddable name="PhoneType" class="org.acme.PhoneType" access="FIELD">
<attributes>
<basic name="type"/>
</attributes>
</embeddable>
連線提取是一種用於在一個數據庫查詢中讀取多個物件的查詢最佳化技術。它涉及在 SQL 中連線兩個物件的表並選擇兩個物件的資料。連線提取通常用於 OneToOne 關係,但也可用於任何關係,包括 OneToMany 和 ManyToMany。
連線提取是解決經典 ORM n+1 效能問題的解決方案之一。問題是,如果你選擇了 n 個 Employee 物件,並訪問了每個物件的地址,在基本的 ORM(包括 JPA)中,你會得到 1 個用於 Employee 物件的資料庫選擇,然後 n 個數據庫選擇,每個 Address 物件一個。連線提取透過只要求一個選擇並選擇 Employee 及其 Address 來解決這個問題。
JPA 透過使用 JOIN FETCH 語法的 JPQL 支援連線提取。
SELECT emp FROM Employee emp JOIN FETCH emp.address
這會導致 Employee 和 Address 資料在一個查詢中被選擇。
使用 JPQL JOIN FETCH 語法,執行一個正常的 INNER 連線。這具有從結果集中過濾任何沒有地址的 Employee 的副作用。SQL 中的 OUTER 連線是一個不過濾連線上的缺失行,而是連線所有 null 值行的連線。如果你的關係允許 null 或集合關係的空集合,那麼你可以使用 OUTER 連線提取,這在 JPQL 中使用 LEFT 語法完成。
請注意,OUTER 連線在某些資料庫上效率可能較低,因此如果不需要,請避免使用 OUTER。
SELECT emp FROM Employee emp LEFT JOIN FETCH emp.address
JPA 沒有辦法指定始終對關係使用連線提取。通常,最好在查詢級別指定連線提取,因為某些用例可能需要相關物件,而其他用例可能不需要。JPA 在對映上支援 EAGER 選項,但這意味著將載入關係,而不是將它連線。將所有關係標記為 EAGER 可能是可取的,因為所有內容都需要載入,但將所有內容在一個巨大的選擇中連線提取可能會導致資料庫上的低效、過於複雜或無效的連線。
一些 JPA 提供程式將 EAGER 解釋為連線提取,因此這在某些 JPA 提供程式上可能有效。一些 JPA 提供程式支援一個單獨的選項來始終連線提取關係。
- TopLink,EclipseLink : 在對映上支援
@JoinFetch註釋和 XML 來定義始終連線提取關係。
JPA 1.0 不允許在 JPQL 中巢狀連線提取,儘管這可能受某些 JPA 提供程式支援。你可以連線提取多個關係,但不能連線巢狀關係。
SELECT emp FROM Employee emp LEFT JOIN FETCH emp.address LEFT JOIN FETCH emp.phoneNumbers
使用連接獲取的一個問題是可能會返回重複資料。例如,考慮連接獲取一個Employee的phoneNumbers關係。如果每個Employee在其phoneNumbers集合中擁有 3 個Phone物件,連線需要檢索回n*3 行。由於每行員工資料對應 3 行電話資料,因此員工資料行會被重複 3 次。所以,你讀取的資料比你在n+1個查詢中選擇物件時要多。通常,執行較少查詢這一事實彌補了可能讀取重複資料這一事實,但如果你考慮連線多個集合關係,你可能會開始檢索j*i個重複資料,這可能會成為一個問題。即使使用ManyToOne關係,你也可能正在選擇重複資料。考慮連接獲取一個Employee的經理:如果所有或大多數員工都有相同的經理,你最終會多次選擇此經理的資料。在這種情況下,你不使用連接獲取,而是允許對經理執行單個查詢會更好。
如果你開始連接獲取每個關係,你可能會得到一些非常大的連線。這對資料庫來說有時可能是一個問題,尤其是對於大型外連線。
連接獲取的一種替代解決方案,它不會遇到重複資料的問題,是使用批次獲取.
批次獲取是一種查詢最佳化技術,用於在有限的資料庫查詢集中讀取多個相關物件。它涉及以正常方式執行根物件的查詢。但是對於相關物件,原始查詢將與相關物件的查詢連線起來,允許所有相關物件在一個數據庫查詢中讀取。批次獲取可用於任何型別的關係。
批次獲取是解決經典 ORM n+1 效能問題的解決方案之一。問題是,如果你選擇n個Employee物件,並訪問它們的每個地址,在基本的 ORM(包括 JPA)中,你將獲得1個用於Employee物件的資料庫選擇,然後n個數據庫選擇,每個Address物件一個。批次獲取透過僅要求對Employee物件進行一次選擇,對Address物件進行一次選擇來解決此問題。
對於讀取集合關係和多個關係,批次獲取更有效,因為它不需要像連接獲取那樣選擇重複資料。
JPA 不支援批次讀取,但一些 JPA 提供商支援。
- TopLink,EclipseLink : 支援
@BatchFetch註釋和 xml 元素,以及"eclipselink.batch"查詢提示以啟用批次讀取。支援批次獲取的三種形式:JOIN、EXISTS和IN。
另請參閱,
通常,關係基於資料庫中的外部索引鍵,但有時它始終基於其他條件。例如,Employee擁有許多PhoneNumber,但也擁有一個單獨的家庭電話,或者是他電話中的一個帶有"home"型別的電話,或者是一組“活躍”專案,或者其他類似的條件。
JPA 不支援對映這些型別的關係,因為它只支援由外部索引鍵定義的對映,而不是基於其他列、常量值或函式。一些 JPA 提供商可能支援此功能。解決方法包括對映關係的外部索引鍵部分,然後在物件的 get/set 方法中過濾結果。你也可以查詢結果,而不是定義關係。
- TopLink,EclipseLink : 透過多種機制支援過濾和複雜關係。你可以使用
DescriptorCustomizer在任何對映上定義selectionCriteria,使用Expressioncriteria API。這允許應用任何條件,包括常量、函式或複雜連線。你也可以使用DescriptorCustomizer定義 SQL 或為對映的selectionQuery定義StoredProcedureCall。
有時需要定義一種關係,其中關係的型別可以是幾個不相關、異構值的其中之一。這可以是 OneToOne、ManyToOne、OneToMany 或 ManyToMany 關係。相關值可能共享一個公共介面,或者除了子類化Object之外,可能沒有其他共同點。也可以設想一種關係,它也可以是任何Basic值,甚至Embeddable。
通常,JPA 不支援可變、介面或異構關係。JPA 支援與繼承類的關係,因此最簡單的解決方法通常是為相關值定義一個公共超類。
另一種解決方案是使用 get/set 方法為每個可能的實現者定義虛擬屬性,並分別對映它們,並將異構 get/set 標記為transient。你也可以不對映屬性,而是根據需要查詢它。
對於異構Basic或Embeddable關係,一種解決方案是將值序列化為二進位制欄位。你也可以將值轉換為可以從中恢復值的String表示,或者將值儲存到兩列中,一列儲存String值,另一列儲存類名或型別。
一些 JPA 提供商支援介面、可變關係和/或異構關係。
- TopLink,EclipseLink : 透過他們的
@VariableOneToOne註釋和 XML 支援可變關係。對映到介面和查詢介面也透過他們的ClassDescriptor的InterfacePolicyAPI 支援。
在物件模型中,擁有複雜的集合關係(例如List的List(即矩陣),或Map的Map,或Map的List,等等)是比較常見的。不幸的是,這些型別的集合對映到關係資料庫的效果很差。
JPA 不支援巢狀集合關係,通常最好更改你的物件模型以避免它們,以便更輕鬆地進行永續性和查詢。一個解決方案是建立一個包裝巢狀集合的物件。
例如,如果一個Employee有一個Map的Project,其鍵是String project-type,值為List或Project。為了對映它,可以建立一個新的ProjectType類來儲存 project-type,以及一個OneToMany到Project。
public class Employee {
private long id;
private Map<String, List<Project>> projects;
}
public class Employee {
@Id
@GeneratedValue
private long id;
...
@OneToMany(mappedBy="employee")
@MapKey(name="type")
private Map<String, ProjectType> projects;
}
public class ProjectType {
@Id
@GeneratedValue
private long id;
@ManyToOne
private Employee employee;
@Column(name="PROJ_TYPE")
private String type;
@ManyToMany
private List<Project> projects;
}
EMPLOYEE (表)
| ID | FIRSTNAME | LASTNAME | SALARY |
| 1 | Bob | Way | 50000 |
| 2 | Sarah | Smith | 60000 |
PROJECTTYPE(表)
| ID | EMPLOYEE_ID | PROJ_TYPE |
| 1 | 1 | small |
| 2 | 1 | medium |
| 3 | 2 | large |
PROJECTTYPE_PROJECT(表)
| PROJECTTYPE_ID | PROJECT_ID |
| 1 | 1 |
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |