跳轉到內容

Java 永續性/標識和排序

來自 Wikibooks,開放世界中的開放書籍

物件 ID (OID) 是唯一標識物件的東西。在 JVM 中,這通常是物件的指標。在關係資料庫表中,一行透過其 主鍵 在其表中唯一標識。當將物件持久化到資料庫時,您需要一個物件的唯一識別符號,這使您能夠查詢物件、定義與物件的關聯關係,以及更新和刪除物件。在 JPA 中,物件 ID 透過 @Id 註解或 <id> 元素定義,並且應對應於物件表的 主鍵


示例 id 註解

[編輯 | 編輯原始碼]
...
@Entity
public class Employee {
    @Id
    private long id
    ...
}

示例 id XML

[編輯 | 編輯原始碼]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <generator class="assigned" />
    </attributes>
<entity/>

常見問題

[編輯 | 編輯原始碼]
奇怪的行為,唯一約束衝突。
[編輯 | 編輯原始碼]
您絕不應更改物件的 id。這樣做會導致錯誤,或者根據您的 JPA 提供程式而產生奇怪的行為。也不要建立具有相同 id 的兩個物件,或嘗試持久化與現有物件具有相同 id 的物件。如果您有一個可能存在的物件,請使用 EntityManagermerge() API,不要對現有物件使用 persist(),並避免將非託管的現有物件與其他託管物件相關聯。
沒有主鍵。
[編輯 | 編輯原始碼]
參見 沒有主鍵

物件 ID 可以是自然 ID 或生成 ID。自然 ID 是在物件中出現並在應用程式中具有一定含義的 ID。自然 ID 的示例包括電子郵件地址、電話號碼和社會保險號碼。生成 ID(也稱為代理 ID)是由系統生成的 ID。JPA 中的排序號是 JPA 實現生成的順序 ID,並自動分配給新物件。使用排序號的好處是它們保證是唯一的,允許物件的所有其他資料發生變化,是用於查詢和索引的有效值,並且可以有效地分配。自然 ID 的主要問題是所有事物最終都會發生變化;即使是一個人的社會保險號碼也可能發生變化。自然 ID 還會使資料庫中的查詢、外部索引鍵和索引效率降低。

在 JPA 中,@Id 可以透過 @GeneratedValue 註解或 <generated-value> 元素輕鬆分配一個生成的排序號。

示例生成 id 註解

[編輯 | 編輯原始碼]
...
@Entity
public class Employee {
    @Id
    @GeneratedValue
    private long id
    ...
}

示例生成的id XML

[編輯 | 編輯原始碼]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id">
            <generated-value/>
        </id>
    </attributes>
<entity/>

序列策略

[編輯 | 編輯原始碼]

有幾種生成唯一id的策略。一些策略與資料庫無關,而另一些則利用了內建的資料庫支援。

JPA 透過 GenerationType 列舉值定義了幾個 id 生成策略,這些策略提供支援:TABLE、SEQUENCE 和 IDENTITY。

選擇哪種序列策略很重要,因為它會影響效能、併發性和可移植性。

表排序

[編輯 | 編輯原始碼]

表排序使用資料庫中的一個表來生成唯一id。該表有兩個列,一個儲存序列的名稱,另一個儲存最後分配的id值。序列表中每個序列物件都有一行。每次需要新的id時,該序列的行都會遞增,並將新的id值傳遞迴應用程式以分配給物件。這只是序列表架構的一個例子,有關其他表排序架構,請參閱自定義

表排序是最便攜的解決方案,因為它只使用一個普通的資料庫表,因此與序列和標識不同,它可以在任何資料庫上使用。表排序還提供了良好的效能,因為它允許序列預分配,這對插入效能非常重要,但可能存在潛在的併發問題

在 JPA 中,@TableGenerator 註解或 <table-generator> 元素用於定義一個序列表。TableGenerator 定義了一個 pkColumnName 用於儲存序列名稱的列,valueColumnName 用於儲存最後分配的 id 的列,以及 pkColumnValue 用於儲存名稱列中的值(通常是序列名稱)。

示例序列表

[編輯 | 編輯原始碼]
SEQUENCE_TABLE
  SEQ_NAME     SEQ_COUNT   
EMP_SEQ 13
PROJ_SEQ 570

示例表生成器註解

[編輯 | 編輯原始碼]
...
@Entity
public class Employee {
    @Id
    @TableGenerator(name="TABLE_GEN", table="SEQUENCE_TABLE", pkColumnName="SEQ_NAME",
        valueColumnName="SEQ_COUNT", pkColumnValue="EMP_SEQ")
    @GeneratedValue(strategy=GenerationType.TABLE, generator="TABLE_GEN")
    private long id;
    ...
}

示例表生成器 XML

[編輯 | 編輯原始碼]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id">
            <generated-value strategy="TABLE" generator="EMP_SEQ"/>
            <table-generator name="EMP_SEQ" table="SEQUENCE_TABLE" pk-column-name="SEQ_NAME"
                value-column-name="SEQ_COUNT" pk-column-value="EMP_SEQ"/>
        </id>
    </attributes>
<entity/>

常見問題

[編輯 | 編輯原始碼]
分配序列號時出錯。
[編輯 | 編輯原始碼]
如果您在資料庫中沒有定義 SEQUENCE 表,或者其架構與您配置的架構不匹配,或者與您的 JPA 提供商預設情況下期望的架構不匹配,則可能會出現諸如“找不到表”,“無效列”之類的錯誤。確保您正確建立了序列表,或者將 @TableGenerator 配置為與您建立的表匹配,或者讓您的 JPA 提供商為您建立表(大多數 JPA 提供商支援模式建立)。您也可能會收到諸如“找不到序列”之類的錯誤,這意味著您沒有為您的序列在表中建立一行。您必須為您的序列在序列表中插入一行初始行,其中包含初始 id(例如 INSERT INTO SEQUENCE_TABLE (SEQ_NAME, SEQ_COUNT) VALUES ("EMP_SEQ", 0)),或者讓您的 JPA 提供商為您建立模式。
序列表中的死鎖或併發性差。
[編輯 | 編輯原始碼]
參見併發問題

序列物件

[編輯 | 編輯原始碼]

序列物件使用特殊的資料庫物件來生成id。序列物件僅在某些資料庫(如 Oracle、DB2 和 Postgres)中受支援。通常,SEQUENCE 物件具有名稱、增量和其他資料庫物件設定。每次選擇 <sequence>.NEXTVAL 時,序列都會按增量遞增。

序列物件提供了最佳的排序選項,因為它們效率最高並且併發性最好,但是它們的可移植性最差,因為大多數資料庫不支援它們。序列物件透過在資料庫序列物件上設定增量來支援序列預分配,增量的大小為序列預分配的大小。

在 JPA 中,@SequenceGenerator 註解或 <sequence-generator> 元素用於定義一個序列物件。SequenceGenerator 定義了一個 sequenceName 用於資料庫序列物件的名稱,以及一個 allocationSize 用於序列預分配大小或序列物件增量。

示例序列生成器註解

[編輯 | 編輯原始碼]
...
@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="EMP_SEQ")
    @SequenceGenerator(name="EMP_SEQ", sequenceName="EMP_SEQ", allocationSize=100)
    private long id;
   
}

示例序列生成器 XML

[編輯 | 編輯原始碼]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id">
            <generated-value strategy="SEQUENCE" generator="EMP_SEQ"/>
            <sequence-generator name="EMP_SEQ" sequence-name="EMP_SEQ" allocation-size="100"/>
        </id>
    </attributes>
<entity/>

常見問題

[編輯 | 編輯原始碼]
分配序列號時出錯。
[編輯 | 編輯原始碼]
如果您在資料庫中沒有定義 SEQUENCE 物件,則可能會出現諸如“找不到序列”之類的錯誤。確保您建立了序列物件,或者讓您的 JPA 提供商為您建立模式(大多數 JPA 提供商支援模式建立)。在建立序列物件時,確保序列的 INCREMENTSequenceGeneratorallocationSize 相匹配。建立序列物件的 DDL 依賴於資料庫,對於 Oracle,它是 CREATE SEQUENCE EMP_SEQ INCREMENT BY 100 START WITH 100
無效的、重複的或負的序列號。
[編輯 | 編輯原始碼]
如果您的序列物件的 INCREMENT 與您的 allocationSize 不匹配,則可能發生這種情況。這會導致 JPA 提供商認為它獲得了比實際更多的序列,並最終導致值重複或出現負數。如果您的序列物件的 STARTS WITH 是 0 而不是等於或大於 allocationSize 的值,這也會發生在某些 JPA 提供商上。

標識排序

[編輯 | 編輯原始碼]

標識排序使用資料庫中的特殊 IDENTITY 列,允許資料庫在插入行的物件時自動分配一個 id。IDENTITY 列在許多資料庫中受支援,例如 MySQL、DB2、SQL Server、Sybase 和 PostgreSQL。Oracle 從 Oracle 12c 開始支援 IDENTITY 列。如果使用的是舊版本,可以使用序列物件和觸發器來模擬它們。

雖然識別符號排序看起來是最簡單的方法來分配 ID,但它們存在幾個問題。其中之一是,由於 ID 直到行插入後才會由資料庫分配,因此在提交或重新整理呼叫之前無法在物件中獲取 ID。識別符號排序也不允許預分配序列,因此可能需要為插入的每個物件進行一次選擇操作,這可能會造成重大的效能問題,因此一般不推薦使用。

在 JPA 中,沒有用於識別符號排序的註釋或元素,因為沒有額外的資訊需要指定。只需要將 GeneratedValue 的策略設定為 IDENTITY

示例識別符號註釋

[edit | edit source]
...
@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private long id;
    ...
}

示例識別符號 XML

[edit | edit source]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id">
            <generated-value strategy="IDENTITY"/>
        </id>
    </attributes>
<entity/>

PostgreSQL 序列列

[edit | edit source]

序列物件是資料庫構造,可以根據需要例項化為不同的物件。當呼叫其 "next" 方法時,它們提供增量值。請注意,增量可以是大於 1 的值,例如以 2、3 等遞增。可以為每個表構建一個 "on insert" 觸發器,該觸發器呼叫為該特定表主鍵建立的序列物件,請求下一個值並將其插入到具有新記錄的表中。MySQL 或 SQL Server 等產品中的標識資料型別是封裝型別,它們執行相同的操作,而無需設定觸發器和序列(儘管 PostgreSQL 至少使用其 Serial 和 Big Serial *偽* 資料型別自動化了大部分操作(這些實際上會建立一個 int/bigint 列並執行一個 "宏" 在執行 CREATE TABLE 語句時建立序列和 "on insert" 觸發器)。)。

常見問題

[edit | edit source]
將 null 插入資料庫,或插入時出錯。
[edit | edit source]
這通常是由於未將 @Id 配置為使用 @GeneratedValue(strategy=GenerationType.IDENTITY) 造成的。請確保正確配置。也可能是您的 JPA 提供程式不支援您正在使用的資料庫平臺上的識別符號排序,或者您尚未配置資料庫平臺。大多數提供程式要求您透過 persistence.xml 屬性設定資料庫平臺,大多數提供程式還允許您自定義自己的平臺,如果它沒有得到直接支援。也可能是您沒有將表中的主鍵列設定為標識型別。
物件在持久化後沒有分配 ID。
[edit | edit source]
識別符號排序要求在分配 ID 之前發生插入操作,因此它不會像其他型別的排序那樣在持久化時分配。您必須在當前事務上呼叫 commit(),或者在 EntityManager 上呼叫 flush()。也可能是您沒有將表中的主鍵列設定為標識型別。
子物件的 ID 在持久化時未從父物件分配。
[edit | edit source]
一個常見問題是,生成的 Id 是透過 OneToOneManyToOne 對映成為子物件 Id 的一部分。在這種情況下,由於 JPA 要求子物件為 Id 定義重複的 Basic 對映,因此它的 Id 將插入為 null。對此的一種解決方案是將子物件中 Id 對映上的 Column 標記為 insertable=false, updateable=false,並使用正常的 JoinColumn 定義 OneToOneManyToOne,這將確保外部索引鍵欄位由 OneToOneManyToOne 填充,而不是 Basic。另一種選擇是先持久化父物件,然後在持久化子物件之前呼叫 flush()
插入效能差。
[edit | edit source]
識別符號排序不支援序列預分配,因此需要在每次插入後進行一次選擇操作,在某些情況下會使插入成本增加一倍。請考慮使用序列表或序列物件來允許序列預分配。
丟失最新的可用 ID。
[edit | edit source]
MySQL 錯誤 199 會導致其自動遞增計數器在重啟時丟失。因此,如果刪除了最後一個實體並重啟了 MySQL 伺服器,相同的 ID 將被重新使用,因此不再唯一。

高階

[edit | edit source]

複合主鍵

[edit | edit source]

複合主鍵是由表中多個列組成的主鍵。如果表中沒有單個列是唯一的,可以使用複合主鍵。通常,擁有一個單列主鍵(如生成的序列號)更有效且更簡單,但有時複合主鍵是可取的且不可避免的。

複合主鍵在遺留資料庫模式中很常見,其中有時可以使用 *級聯鍵*。這指的是一個模型,其中依賴物件的鍵定義包含其父物件的主鍵;例如,COMPANY 的主鍵是 COMPANY_IDDEPARTMENT 的主鍵由 COMPANY_IDDEP_ID 組成,EMPLOYEE 的主鍵由 COMPANY_IDDEP_IDEMP_ID 組成,等等。雖然這通常與面向物件的設計原則不符,但一些 DBA 更喜歡這種模型。該模型的難點包括限制員工無法更換部門、外部索引鍵關係變得更加複雜以及所有主鍵操作(包括查詢、更新和刪除)效率較低。但是,每個部門都控制著自己員工的 ID,如果需要,資料庫 EMPLOYEE 表可以根據 COMPANY_IDDEP_ID 進行分割槽,因為這些 ID 包含在每個查詢中。

複合主鍵的其他常見用法包括多對多關係,其中連線表具有其他列,因此表本身對映到一個物件,其主鍵由一對外部索引鍵列和依賴或聚合一對多關係組成,其中子物件的主鍵由其父物件的主鍵和一個本地唯一欄位組成。

在 JPA 中,有兩種聲明覆合主鍵的方法:IdClassEmbeddedId

Id 類

[edit | edit source]

IdClass 定義一個單獨的 Java 類來表示主鍵。它是透過 @IdClass 註釋或 <id-class> XML 元素定義的。IdClass 必須定義一個屬性(欄位/屬性),該屬性反映實體中的每個 Id 屬性。它必須具有相同的屬性名稱和型別。使用 IdClass 時,您仍然需要使用 @Id 標記實體中的每個 Id 屬性。

IdClass 的主要目的是用作傳遞給 EntityManager find()getReference() API 的結構。 一些 JPA 產品也會將 IdClass 用作快取鍵來跟蹤物件的標識。 因此,需要(取決於 JPA 產品)在 IdClass 上實現 equals()hashCode() 方法。 確保 equals() 方法檢查主鍵的每個部分,並正確使用 equals 用於物件,== 用於基本型別。 確保 hashCode() 方法對兩個相等的物件返回相同的值。

TopLink / EclipseLink : 不需要在 id 類中實現 equals()hashCode()

示例 id 類註釋

[edit | edit source]
...
@Entity
@IdClass(EmployeePK.class)
public class Employee {
    @Id
    private long employeeId;

    @Id
    private long companyId;

    @Id
    private long departmentId;
    ...
}

示例 id 類 XML

[edit | edit source]
<entity class="org.acme.Employee">
    <id-class class="org.acme.EmployeePK"/>
    <attributes>
        <id name="employeeId"/>
        <id name="companyId"/>
        <id name="departmentId"/>
    </attributes>
<entity/>

示例 id 類

[edit | edit source]
...
public class EmployeePK implements Serializable {
    private long employeeId;

    private long companyId;

    private long departmentId;
    
    public EmployeePK(long employeeId, long companyId, long departmentId) {
        this.employeeId   = employeeId;
        this.companyId    = companyId;
        this.departmentId = departmentId;
    }

    public boolean equals(Object object) {
        if (object instanceof EmployeePK) {
            EmployeePK pk = (EmployeePK)object;
            return employeeId == pk.employeeId && companyId == pk.companyId && departmentId == pk.departmentId;
        } else {
            return false;
        }
    }

    public int hashCode() {
        return (int)(employeeId + companyId + departmentId);
    }
}

嵌入式 Id

[edit | edit source]

EmbeddedId 定義了一個單獨的 Embeddable Java 類來包含實體的主鍵。 它透過 @EmbeddedId 註釋或 <embedded-id> XML 元素定義。 EmbeddedIdEmbeddable 類必須使用 Basic 對映定義實體的每個 id 屬性。 EmbeddedIdEmbeddable 中的所有屬性都被認為是主鍵的一部分。

EmbeddedId 也用作傳遞給 EntityManager find()getReference() API 的結構。 一些 JPA 產品也會將 EmbeddedId 用作快取鍵來跟蹤物件的標識。 因此,需要(取決於 JPA 產品)在 EmbeddedId 上實現 equals()hashCode() 方法。 確保 equals() 方法檢查主鍵的每個部分,並正確使用 equals 用於物件,== 用於基本型別。 確保 hashCode() 方法對兩個相等的物件返回相同的值。

TopLink / EclipseLink : 不需要在 id 類中實現 equals()hashCode()

示例嵌入式 id 註釋

[edit | edit source]
...
@Entity
public class Employee {
    @EmbeddedId
    private EmployeePK id
    ...
}

示例嵌入式 id XML

[edit | edit source]
<entity class="org.acme.Employee">
    <attributes>
        <embedded-id name="org.acme.EmployeePK"/>
    </attributes>
<entity/>
<embeddable class="org.acme.EmployeePK">
    <attributes>
        <basic name="employeeId"/>
        <basic name="companyId"/>
        <basic name="departmentId"/>
    </attributes>
<embeddable/>

示例嵌入式 id 類

[edit | edit source]
...
@Embeddable
public class EmployeePK {
    @Basic
    private long employeeId;

    @Basic
    private long companyId;

    @Basic
    private long departmentId;
        
    public EmployeePK(long employeeId, long companyId, long departmentId) {
        this.employeeId = employeeId;
        this.companyId = companyId;
        this.departmentId = departmentId;
    }

    public boolean equals(Object object) {
        if (object instanceof EmployeePK) {
            EmployeePK pk = (EmployeePK)object;
            return employeeId == pk.employeeId && companyId == pk.companyId && departmentId == pk.departmentId;
        } else {
            return false;
        }
    }

    public int hashCode() {
        return (int)(employeeId + companyId + departmentId);
    }
}

透過 OneToOne 和 ManyToOne 關係實現主鍵

[edit | edit source]

一個常見的模型是讓一個依賴物件共享其父物件的主鍵。 在 OneToOne 的情況下,子物件的主鍵與父物件相同,而在 ManyToOne 的情況下,子物件的主鍵由父物件的主鍵和另一個本地唯一的欄位組成。

JPA 1.0 不允許在 OneToOneManyToOne 上使用 @Id,但 JPA 2.0 允許。

在處理複合主鍵時,最大的陷阱之一是實現與表具有多列主鍵的實體類的關聯(使用 @JoinColumns 註釋)。 許多 JPA 實現可能會丟擲看似不一致的異常,如果未為 *每個* @JoinColumns 註釋指定 referencedColumnName,而 JPA 要求為複合外部索引鍵(即使所有引用列名稱都等於引用表中的列名稱)。 請參閱 http://download.oracle.com/javaee/5/api/javax/persistence/JoinColumns.html

JPA 1.0

[edit | edit source]

不幸的是,JPA 1.0 處理此模型的效果不佳,並且情況變得複雜,因此為了讓您的生活更輕鬆,您可以考慮為子物件定義一個生成的唯一 id。 JPA 1.0 要求所有 @Id 對映都為 Basic 對映,因此如果您的 Id 來自透過 OneToOneManyToOne 對映的外部索引鍵列,您還必須為外部索引鍵列定義一個 Basic @Id 對映。 這樣做的部分原因是,Id 必須是標識和快取目的的簡單物件,以及用於 IdClassEntityManager find() API 中。

由於您現在對同一外部索引鍵列有兩個對映,您必須定義哪個對映將寫入資料庫(它必須是 Basic 對映),因此 OneToOneManyToOne 外部索引鍵必須被定義為只讀。 這是透過將 JoinColumn 屬性 insertableupdatable 設定為 false 來完成的,或者透過使用 @PrimaryKeyJoinColumn 而不是 @JoinColumn 來完成的。

對同一列有兩個對映的副作用是,您現在必須保持兩者同步。 這通常透過讓 OneToOne 屬性的 set 方法也將其 Basic 屬性值設定為目標物件的 id 來完成。 如果目標物件的主鍵是 GeneratedValue,這可能會變得非常複雜,在這種情況下,您必須確保在關聯兩個物件之前已分配目標物件的 id。

有時我認為,如果 JPA 主鍵只是使用 Column 集合在實體上定義,而不是將它們與屬性對映混在一起,那將簡單得多。 這將使您能夠以您想要的任何方式對映主鍵欄位。 可以使用通用 List 將主鍵傳遞給 find() 方法,並且 JPA 提供者將負責正確地雜湊和比較主鍵,而不是使用者的 IdClass。 但也許對於簡單單例主鍵模型,JPA 模型更直觀。

TopLink / EclipseLink : 允許將主鍵指定為列列表,而不是使用 Id 對映。 這允許將 OneToOneManyToOne 對映外部索引鍵用作主鍵,而無需重複對映。 它也允許透過任何其他對映型別定義主鍵。 這是透過使用 DescriptorCustomizerClassDescriptor addPrimaryKeyFieldName API 來完成的。
Hibernate / Open JPA / EclipseLink (截至 1.2): 允許在 OneToOneManyToOne 對映上使用 @Id 註釋。

示例 OneToOne id 註釋

[edit | edit source]
...
@Entity
public class Address {
    @Id
    @Column(name="OWNER_ID")
    private long ownerId;
    
    @OneToOne
    @PrimaryKeyJoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID")
    private Employee owner;
    ...
    
    public void setOwner(Employee owner) {
        this.owner = owner;
        this.ownerId = owner.getId();
    }
    ...
}

示例 OneToOne id XML

[edit | edit source]
<entity class="org.acme.Address">
    <attributes>
        <id name="ownerId">
            <column name="OWNER_ID"/>
        </id>
        <one-to-one name="owner">
            <primary-key-join-column name="OWNER_ID" referencedColumnName="EMP_ID"/>
        </one-to-one>
    </attributes>
<entity/>

示例 ManyToOne id 註釋

[edit | edit source]
...
@Entity
@IdClass(PhonePK.class)
public class Phone {
    @Id
    @Column(name="OWNER_ID")
    private long ownerId;

    @Id
    private String type;
    
    @ManyToOne
    @PrimaryKeyJoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID")
    private Employee owner;
    ...
    
    public void setOwner(Employee owner) {
        this.owner = owner;
        this.ownerId = owner.getId();
    }
    ...
}

示例 ManyToOne id XML

[edit | edit source]
<entity class="org.acme.Phone">
    <id-class class="org.acme.PhonePK"/>
    <attributes>
        <id name="ownerId">
            <column name="OWNER_ID"/>
        </id>
        <id name="type"/>
        <many-to-one name="owner">
            <primary-key-join-column name="OWNER_ID" referencedColumnName="EMP_ID"/>
        </many-to-one>
    </attributes>
</entity>

JPA 2.0

[edit | edit source]

在 JPA 2.0 中,為 OneToOneManyToOne 定義一個 Id 要簡單得多。可以將 @Id 註解或 id XML 屬性新增到 OneToOneManyToOne 對映中。物件的 Id 將從目標物件的 Id 匯出。如果 Id 是一個單一值,那麼源物件的 Id 與目標物件的 Id 相同。如果它是一個複合 Id,那麼 IdClass 將包含 BasicId 屬性,以及目標物件的 Id 作為關係值。如果目標物件也具有複合 Id,那麼源物件的 IdClass 將包含目標物件的 IdClass

JPA 2.0 ManyToOne id 註解示例

[edit | edit source]
...
@Entity
@IdClass(PhonePK.class)
public class Phone {

    @Id
    private String type;
    
    @ManyToOne
    @Id
    @JoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID")
    private Employee owner;
    ...//getters and setters
}

JPA 2.0 ManyToOne id XML 示例

[edit | edit source]
<entity class="org.acme.Address">
    <id-class class="org.acme.PhonePK"/>
    <attributes>
        <id name="type"/>
        <many-to-one name="owner" id="true">
            <join-column name="OWNER_ID" referencedColumnName="EMP_ID"/>
        </many-to-one>
    </attributes>
<entity/>

JPA 2.0 id 類示例

[edit | edit source]
...
public class PhonePK implements Serializable {
    private String type;
    private long owner;
    
    public PhonePK() {}

    public PhonePK(String type, long owner) {
        this.type = type;
        this.owner = owner;
    }

    public boolean equals(Object object) {
        if (object instanceof PhonePK) {
            PhonePK pk = (PhonePK)object;
            return type.equals(pk.type) && owner == pk.owner;
        } else {
            return false;
        }
    }

    public int hashCode() {
        return (int)(type.hashCode() + owner);
    }
 
    //getter and setters with names matching the properties with many-to-one relationships
}

高階排序

[edit | edit source]

併發和死鎖

[edit | edit source]

表排序的一個問題是,序列表可能會成為併發瓶頸,甚至會導致死鎖。如果在與插入相同的事務中分配序列 ID,這會導致併發性差,因為序列行將被鎖定在事務持續時間內,阻止任何需要分配序列 ID 的其他事務。在某些情況下,整個序列表或表頁面可能會被鎖定,導致即使分配其他序列的事務也會等待甚至死鎖。如果使用大型序列預分配大小,這將不再是一個問題,因為序列表很少被訪問。一些 JPA 提供程式使用單獨的(非 JTA)連線來分配序列 ID,從而避免或限制此問題。在這種情況下,如果您使用 JTA 資料來源連線,那麼在您的 persistence.xml 中包含一個非 JTA 資料來源連線也很重要。

保證順序 ID

[edit | edit source]

表排序還允許分配真正的順序 ID。序列和標識排序是非事務性的,通常在資料庫上快取值,導致分配的 ID 中出現較大差距。這通常不是問題,並且需要良好的效能,但是如果效能和併發性不太重要,並且需要真正的順序 ID,那麼可以使用表序列。透過將序列的 allocationSize 設定為 1 並確保序列 ID 在插入的同一事務中分配,您可以保證沒有間隙的序列 ID(但通常最好忍受間隙並獲得良好的效能)。

用完數字

[edit | edit source]

程式設計師經常有一種偏執的妄想恐懼,即用完序列號。由於大多數序列策略只是不斷增加一個數字,因此不可避免地,你最終會用完。但是,只要使用足夠大的數字精度來儲存序列 ID,這就不成問題。例如,如果您將 ID 儲存在 NUMBER(5) 列中,這將允許 99,999 個不同的 ID,這在大多數系統中最終會用完。但是,如果您將 ID 儲存在 NUMBER(10) 列中,這更常見,這將儲存 9,999,999,999 個 ID,或者每秒一個 ID,持續約 300 年(比大多數資料庫存在的時間更長)。但也許您的系統會處理大量資料,並且(希望)會存在很長時間。如果您將 ID 儲存在 NUMBER(20) 中,這將是 99,999,999,999,999,999,999 個 ID,或者每毫秒一個 ID,持續約 3,000,000,000 年,這很安全。

但是您還需要在 Java 中儲存此 ID。如果您將 ID 儲存在 Java int 中,這將是一個 32 位數字,即 4,294,967,296 個不同的 ID(實際上是 2,147,483,648 個正 ID),或者每秒一個 ID,持續約 100 年。如果您改為使用 long,這將是一個 64 位數字,即 9,223,372,036,854,775,808 個不同的 ID,或者每毫秒一個 ID,持續約 300,000,000 年,這很安全。我建議使用 long 而不是 int,因為我已經看到過在大型資料庫中 int ID 用完的情況(發生的情況是它們變為負數,直到它們環繞到 0 並開始出現約束錯誤)。

自定義

[edit | edit source]

JPA 支援三種不同的生成 ID 策略,但還有許多其他方法。通常,JPA 策略就足夠了,所以您只會在遺留情況下使用不同的方法。

有時應用程式具有特定於應用程式的生成 ID 策略,例如在 ID 前新增國家程式碼或分支編號。有幾種方法可以整合自定義 ID 生成策略,最簡單的方法就是將 ID 定義為普通 ID,並在建立物件時讓應用程式分配 ID 值。

一些 JPA 產品提供了額外的排序和 ID 生成選項以及配置鉤子。

TopLinkEclipseLink : 提供了一些額外的排序選項。UnaryTableSequence 允許使用單列表。QuerySequence 允許使用自定義 SQL 或儲存過程。還存在一個 API,允許使用者提供自己的程式碼來分配 ID。
Hibernate : 透過 @GenericGenerator 註解提供 GUID ID 生成選項。

透過觸發器生成主鍵

[edit | edit source]

可以將資料庫表定義為具有自動分配其主鍵的觸發器。這通常不是一個好主意(儘管一些 DBA 可能會認為它是),最好使用 JPA 提供程式生成的序列 ID,或者在應用程式中分配 ID。透過觸發器分配 ID 的主要問題是,應用程式和物件需要將此值返回。對於透過觸發器分配的非主鍵值,可以在提交或重新整理物件後重新整理物件以獲取這些值。但是,這對於 ID 是不可能的,因為 ID 是重新整理物件所必需的。

如果您有另一種方法來選擇觸發器生成的 ID,例如使用另一個唯一欄位選擇物件的行,您可以在插入後發出此 SQL select 以獲取 ID 並將其設定回物件。您可以在 JPA @PostPersist 事件中執行此 select。一些 JPA 提供程式可能不允許/不喜歡在事件期間執行查詢,它們也可能不會在事件回撥期間拾取對物件的更改,因此這樣做可能會有問題。此外,一些 JPA 提供程式可能不允許在不使用 GeneratedValue 的情況下取消分配/為空主鍵,因此您可能會遇到問題。一些 JPA 提供程式內建支援將分配在觸發器(或儲存過程)中的值返回到物件中。

TopLink / EclipseLink : 提供 ReturningPolicy,允許從資料庫插入或更新後返回任何欄位值,包括主鍵。這透過 @ReturnInsert@ReturnUpdate 註解或 <return-insert><return-update> XML 元素在 eclipselink-orm.xml 中定義。

透過事件生成主鍵

[edit | edit source]

如果應用程式生成自己的 ID 而不是使用 JPA GeneratedValue,那麼有時希望在 JPA 事件中執行此 ID 生成,而不是應用程式程式碼必須生成和設定 ID。在 JPA 中,這可以透過 @PrePersist 事件來實現。

沒有主鍵

[編輯 | 編輯原始碼]

有時您的物件或表沒有主鍵。在這種情況下,最佳解決方案通常是向物件和表新增一個生成的 ID。如果您沒有此選項,有時表中會有一列或一組列構成唯一值。您可以將這組唯一的列用作 JPA 中的 ID。JPA Id 不一定始終與資料庫表主鍵約束匹配,也不需要主鍵或唯一約束。

如果您的表確實沒有唯一列,那麼將所有列用作 ID。通常情況下,當這種情況發生時,資料是隻讀的,因此即使表允許具有相同值的重複行,物件也將是相同的,因此 JPA 認為它們是同一個物件並不重要。允許更新和刪除的問題是,沒有辦法唯一地標識物件的 row,因此所有匹配的 row 將被更新或刪除。

如果您的物件沒有 ID,但其表有,則可以。將物件設為 Embeddable 物件,可嵌入物件沒有 ID。您需要一個包含此 EmbeddableEntity 來持久化和查詢它。

華夏公益教科書