实体间的多对多的关联需要一张关联表。如果直接使用 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
。