跳轉到內容

Java 永續性/OneToMany

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

OneToMany

[編輯 | 編輯原始碼]

Java 中的 OneToMany 關係是指源物件有一個屬性儲存目標物件的集合,並且如果這些目標物件具有反向關係回到源物件,則它將是 ManyToOne 關係。 Java 和 JPA 中的所有關係都是單向的,也就是說,如果源物件引用目標物件,則不能保證目標物件也與源物件存在關係。 這與關係資料庫不同,在關係資料庫中,關係透過外部索引鍵定義,並進行查詢,以便始終存在反向查詢。

JPA 還定義了 ManyToMany 關係,它類似於 OneToMany 關係,只是反向關係(如果定義)是 ManyToMany 關係。 JPA 中 OneToManyManyToMany 關係的主要區別在於 ManyToMany 始終使用中間關係聯接表來儲存關係,而 OneToMany 可以使用聯接表,也可以使用目標物件表中的外部索引鍵來引用源物件表的主鍵。 如果 OneToMany 使用目標物件表中的外部索引鍵,JPA 要求關係是雙向的(必須在目標物件中定義反向 ManyToOne 關係),並且源物件必須使用 mappedBy 屬性來定義對映。

在 JPA 中,OneToMany 關係透過 @OneToMany 註解或 <one-to-many> 元素定義。

OneToMany 關係資料庫示例

[編輯 | 編輯原始碼]

EMPLOYEE (表)

EMP_ID FIRSTNAME LASTNAME SALARY MANAGER_ID
1 Bob Way 50000 2
2 Sarah Smith 75000 null

PHONE (表)

ID TYPE AREA_CODE P_NUMBER OWNER_ID
1 home 613 792-0000 1
2 work 613 896-1234 1
3 work 416 123-4444 2

OneToMany 關係和反向 ManyToOne 註解示例

[編輯 | 編輯原始碼]
@Entity
public class Employee {
  @Id
  @Column(name="EMP_ID")
  private long id;
  ...
  @OneToMany(mappedBy="owner")
  private List<Phone> phones;
  ...
}
@Entity
public class Phone {
  @Id
  private long id;
  ...
  @ManyToOne(fetch=FetchType.LAZY)
  @JoinColumn(name="OWNER_ID")
  private Employee owner;
  ...
}

OneToMany 關係和反向 ManyToOne XML 示例

[編輯 | 編輯原始碼]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <one-to-many name="phones" target-entity="org.acme.Phone" mapped-by="owner"/>
    </attributes>
</entity>
<entity name="Phone" class="org.acme.Phone" access="FIELD">
    <attributes>
        <id name="id"/>
        <many-to-one name="owner" fetch="LAZY">
            <join-column name="OWNER_ID"/>
        </many-to-one>
    </attributes>
</entity>

請注意,此 @OneToMany 對映需要反向 @ManyToOne 對映才能完成,請參閱 ManyToOne

Getter 和 Setter

[編輯 | 編輯原始碼]

關係是雙向的,因此當應用程式更新關係的一端時,另一端也應該更新,並保持同步。 在 JPA 中,與 Java 一樣,維護關係的責任在於應用程式或物件模型。 如果您的應用程式向關係的一端新增內容,那麼它必須向另一端新增內容。

這可以透過物件模型中的 add 或 set 方法解決,這些方法處理關係的兩端,因此應用程式程式碼不需要擔心它。 有兩種方法可以解決這個問題,您可以將關係維護程式碼僅新增到關係的一端,並僅從一端使用 setter(例如,將另一端設為 protected),或者將其新增到兩端,並確保避免無限迴圈。

例如

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;
    ...

   /**
    * You have to ensure that the previous owner of this phone is no longer the owner of this 
    * phone before you attribute it a new owner. Ensure this either by a 
    * @requires !this.employee.getPhones().contains(this) or by adding to the beginning of 
    * the method body:
    * if(this.employee != null)
    *     this.employee.removePhone(this);
    */
    public void setOwner(Employee employee) {
        this.owner = employee;
        if (!employee.getPhones().contains(this)) { // warning this may cause performance issues if you have a large data set since this operation is O(n)
            employee.getPhones().add(this);
        }
    }
    ...
}

有些人希望 JPA 提供商擁有自動維護關係的“魔法”。 這實際上是 EJB CMP 2 規範的一部分。 但是問題是,如果物件被分離或序列化到另一個 VM,或者在被管理之前將新物件關聯起來,或者在 JPA 範圍之外使用物件模型,那麼“魔法”就會消失,應用程式將不得不自己解決問題,因此通常最好將程式碼新增到物件模型中。 但是,一些 JPA 提供商確實支援自動維護關係。

在某些情況下,在新增子物件時例項化大型集合是不可取的。 一個解決方案是不對映雙向關係,而是根據需要查詢它。 此外,一些 JPA 提供商優化了他們的延遲集合物件來處理這種情況,因此您仍然可以向集合新增內容而無需例項化它。

聯接表

[編輯 | 編輯原始碼]

物件和關係表之間的一個常見不匹配是,OneToMany 在 Java 中不需要反向引用,但在資料庫中需要反向引用外部索引鍵。 通常最好在 Java 中定義 ManyToOne 反向引用,如果您不能或不想這樣做,那麼可以使用中間聯接表來儲存關係。 這類似於 ManyToMany 關係,但是如果您向目標外部索引鍵新增唯一約束,則可以強制它是 OneToMany

JPA 使用 @JoinTable 註解和 <join-table> XML 元素定義聯接表。 JoinTable 可以用於 ManyToManyOneToMany 對映。

另請參閱,單向 OneToMany

使用聯接表資料庫的 OneToMany 示例

[編輯 | 編輯原始碼]

EMPLOYEE (表)

EMP_ID FIRSTNAME LASTNAME
1 Bob 可以
2 Sarah Smith
3 Sarah Smith

EMP_PHONE (表)

EMP_ID PHONE_ID
1 1
1 2
2 3

PHONE (表)

ID TYPE PHONE_ID P_NUMBER
1 home 1 792-0000
2 work 1 896-1234
3 work 2 123-4444

使用聯接表註解的 OneToMany 示例

[編輯 | 編輯原始碼]
@Entity
public class Employee {
  @Id
  @Column(name="EMP_ID")
  private long id;
  ...
  @OneToMany
  @JoinTable
  (
      name="EMP_PHONE",
      joinColumns={ @JoinColumn(name="EMP_ID", referencedColumnName="EMP_ID") },
      inverseJoinColumns={ @JoinColumn(name="PHONE_ID", referencedColumnName="ID", unique=true) }
  )
// While Update this will also insert collection row another insert
  private List<Phone> phones;
  ...
}

使用 JoinTable XML 的 OneToMany 示例

[編輯 | 編輯原始碼]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id">
            <column name="EMP_ID"/>
        </id>
        <one-to-many name="phones">
            <join-table name="EMP_PHONE">
                <join-column name="EMP_ID" referenced-column-name="EMP_ID"/>
                <inverse-join-column name="PHONE_ID" referenced-column-name="ID" unique="true" />
            </join-table>
        </one-to-many>
    </attributes>
</entity>

另請參閱

[編輯 | 編輯原始碼]

常見問題

[編輯 | 編輯原始碼]
重新整理後物件不在集合中。
[編輯 | 編輯原始碼]
參見物件損壞

單向 OneToMany,無反向 ManyToOne,無 Join Table (僅限 JPA 2.x)

[編輯 | 編輯原始碼]

JPA 1.0 不支援沒有 JoinTable 的單向 OneToMany 關係。從 JPA 2.0 開始,支援單向 OneToMany。在 JPA 2.x 中,可以在 OneToMany 上使用 @JoinColumn 來定義外部索引鍵,一些 JPA 提供程式可能已經支援這一點。

單向 OneToMany 的主要問題是外部索引鍵由目標物件的表擁有,因此如果目標物件不知道該外部索引鍵,則插入和更新該值很困難。在單向 OneToMany 中,源物件擁有外部索引鍵欄位,並負責更新其值。

單向 OneToMany 中的目標物件是一個獨立的物件,因此它不應該以任何方式依賴於外部索引鍵,即外部索引鍵不能是其主鍵的一部分,也不能通常對其設定非空約束。您可以建模一個物件集合,其中目標沒有對映外部索引鍵,但使用它作為其主鍵,或者使用 Embeddable 集合對映沒有主鍵,請參見嵌入式集合

如果您的 JPA 提供程式不支援單向 OneToMany 關係,那麼您需要新增一個反向引用 ManyToOneJoinTable。通常,如果您確實想要在資料庫中建模單向 OneToMany,最好使用 JoinTable

有一些創新的解決方法來定義單向 OneToMany。一種是使用 JoinTable 來對映它,但使目標表成為 JoinTable。這將導致額外的連線,但對於大多數讀取來說可以正常工作,寫入當然不會正常工作,因此這只是一個只讀解決方案,而且是一個很糟糕的解決方案。

JPA 2.x 單向 OneToMany 關係資料庫示例

[編輯 | 編輯原始碼]

EMPLOYEE (表)

EMP_ID FIRSTNAME LASTNAME
1 Bob 可以
2 Sarah Smith

PHONE (表)

ID TYPE AREA_CODE P_NUMBER OWNER_ID
1 home 613 792-0000 1
2 work 613 896-1234 1
3 work 416 123-4444 2

JPA 2.x 單向 OneToMany 關係註釋示例

[編輯 | 編輯原始碼]
@Entity
public class Employee {
  @Id
  @Column(name="EMP_ID")
  private long id;
  ...
  @OneToMany
  @JoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID")
  private List<Phone> phones;
  ...
}
@Entity
public class Phone {
  ...
  @Column(name="OWNER_ID")
  private long ownerId;
  ...
}

非空連線列的示例

[編輯 | 編輯原始碼]

如果您的連線列是非空的,則需要指定 @JoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID", nullable = false)

@Entity
public class Employee {
  @Id
  @Column(name="EMP_ID")
  private long id;
  ...
  @OneToMany
  @JoinColumn(name="OWNER_ID", referencedColumnName="EMP_ID", nullable = false)
  private List<Phone> phones;
  ...
}
華夏公益教科書