實體間的多對多的關聯需要一張關聯表。如果直接使用 ManyToMany
來映射,JPA 就會隱式地幫我們自動管理關聯表,代碼寫出來和其他類型的關聯差別不大。例如,某州炒房團需要一個炒房跟蹤系統,那么該系統中的炒房客和房子就是多對多的關系:
public class Speculator implements Serializable { @Id private Integer id; @ManyToMany @JoinTable(joinColumns = @JoinColumn(name = "speculator_id"), inverseJoinColumns = @JoinColumn(name = "house_id")) private List<House> houses; // 此處省略若干行 } public class House implements Serializable { @Id private Integer id; @ManyToMany(mappedBy = "houses") private List<Speculator> speculators; // 此處省略若干行 }
如果炒房客 s
要賣掉房子 h
(嚴格點說是賣掉房子的產權部分),那么系統執行的代碼差不多就是 s.getHouses().remove(h)
??此坪唵?,然而底層的操作卻性能低下:JPA 會先從數據庫中取出炒房客的所有房產(s.getHouses()
),然后再刪除指定的那套房子;從數據庫層面上看,這將先從關聯表(speculator_house
)中找到該炒房客的所有房子的外鍵,然后從 house
表載入這些 House
對象,最后才從 speculator_house
刪除關聯。在 ORM 出現前,這種操作只需要改關聯表,根本不用關心其他房子。這種簡單的多對多映射寫法將關聯表隱藏起來,雖然簡化了代碼,卻也可能帶來性能隱患。
很自然地可以想到,如果把關聯表也映射成實體類,就能解決這個問題。speculator_house
包含兩個外鍵,可用作聯合主鍵。如果把它映射為 SpeculatorHouse
類,則該類與 Speculator
和 House
都是多對一的關系。關聯表實體類的代碼如下(EmbeddedId
的映射技巧見《JPA 應用技巧 2:主鍵外鍵合體映射》):
@Embeddable public class SpeculatorHouseId implements Serializable { private Integer speculatorId; private Integer houseId; // 此處省略若干行 } @Entity @Table(name = "speculator_house") public class SpeculatorHouse implements Serializable { @EmbeddedId private SpeculatorHouseId id; @MapsId("speculatorId") @ManyToOne private Speculator speculator; @MapsId("houseId") @ManyToOne private House house; // 此處省略若干行 }
Speculator
和 House
也要增加相應的關聯信息:
public class Speculator implements Serializable { @Id private Integer id; @ManyToMany @JoinTable(joinColumns = @JoinColumn(name = "speculator_id"), inverseJoinColumns = @JoinColumn(name = "house_id")) private List<House> houses; @OneToMany(mappedBy = "speculator") private List<SpeculatorHouse> speculatorHouses; // 此處省略若干行 } public class House implements Serializable { @Id private Integer id; @ManyToMany(mappedBy = "houses") private List<Speculator> speculators; @OneToMany(mappedBy = "house") private List<SpeculatorHouse> speculatorHouses; // 此處省略若干行 }
這樣既保留了多對多關系,又映射了關聯表,然后就可以根據實際情況選擇隱式或顯示的關聯表管理。例如,要得到一個炒房客的全部房子,就使用隱式管理:s.getHouses()
;而要刪除炒房客和某套房子的關聯,則用顯示管理:delete from SpeculatorHouse sh where sh.speculator = :s and sh.house = :h
。