Spring?Data?JPA?注解Entity關(guān)聯(lián)關(guān)系使用詳解
首先,實體與實體之間的關(guān)聯(lián)關(guān)系一共分為四種,分別為OneToOne、OneToMany、ManyToOne和ManyToMany;而實體之間的關(guān)聯(lián)關(guān)系又分為雙向和單向。實體之間的關(guān)聯(lián)關(guān)系是在JPA使用中最容易發(fā)生問題的地方。
1、OneToOne關(guān)聯(lián)關(guān)系
@OneToOne一般表示對象之間一對一的關(guān)聯(lián)關(guān)系,它可以放在field上面,也可以放在get/set方法上面。其中JPA協(xié)議有規(guī)定,如果配置雙向關(guān)聯(lián),維護(hù)關(guān)聯(lián)關(guān)系的是擁有外鍵的一方,而另一方必須配置mappedBy;如果是單項關(guān)聯(lián),直接配置在擁有外鍵的一方即可。
舉例說明:
user表是用戶的主信息,user_info是用戶的拓展信息,兩者之間是一對一的關(guān)系。user_info表里面有一個user_id作為關(guān)聯(lián)關(guān)系的外鍵,如果是單項關(guān)聯(lián),我們的寫法如下:
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String email;
private String sex;
private String address;
}
我們只需要在擁有外鍵的一方配置@OneToOne注解就可以了
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "user")
public class UserInfo {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private Integer ages;
private String telephone;
@OneToOne
private User user;
}
這就是單向關(guān)聯(lián)關(guān)系,那么如何設(shè)置雙向關(guān)聯(lián)關(guān)系呢? 我們保持UserInfo不變,在User實體對象里面添加一段代碼即可
@OneToOne(mappedBy = "user")
private UserInfo userInfo;
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
private String name;
private String email;
private String sex;
private String address;
@OneToOne(mappedBy = "user")
private UserInfo userInfo;
}
1.1 解讀OneToOne源碼
public @interface OneToOne {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default EAGER;
boolean optional() default true;
String mappedBy() default "";
boolean orphanRemoval() default false;
}
targetEntity:作為關(guān)聯(lián)目標(biāo)的實體類。
cascade:級聯(lián)操作策略,就是我們常說的級聯(lián)操作。
fetch:數(shù)據(jù)獲取方式EAGER(立即加載)/LAZY(延遲加載) optional:表示關(guān)聯(lián)的實體是否能夠存在null值 mappedBy:關(guān)聯(lián)關(guān)系被誰維護(hù)的一方對象里面的屬性名字,雙向關(guān)聯(lián)的時候必填。
1.2 mappedBy 注意事項
- 只有關(guān)聯(lián)關(guān)系的維護(hù)方才能操作兩個實體之間外鍵的關(guān)系。被維護(hù)方即使設(shè)置維護(hù)方屬性進(jìn)行存儲也不會更新外鍵關(guān)聯(lián)
- mappedBy不能與@JoinColumn或者@JoinTable同時使用,因為沒有任何意義,關(guān)聯(lián)關(guān)系不在這里面維護(hù)。
- mappedBy的值是指另一方的實體里面屬性的字段,而不是數(shù)據(jù)庫字段,也不是實體的對象的名字。也就是維護(hù)關(guān)聯(lián)關(guān)系的一方屬性字段名稱,或者加了@JoinColumn 或 @JoinTable注解的屬性字段名稱。如上面的User例子user里面的mappedBy的值,就是userinfo里面的user字段的名字。
1.3 CascadeType 用法
在CascadeType的用法中,CascadeType的枚舉值只有5個,分別如下:
- CascadeType.PERSIST 級聯(lián)新建
- CascadeType.REMOVE 級聯(lián)刪除
- CascadeType.PEFRESH 級聯(lián)刷新
- CascadeType.MERGE 級聯(lián)更新
- CascadeType.ALL 四項全選
測試級聯(lián)新建和級聯(lián)刪除:
第一步: 在@OneToOne上面添加 cascade = {CascadeType.PERSIST,CascadeType.REMOVE},代碼如下所示:
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "user")
public class UserInfo {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private Integer ages;
private String telephone;
@OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE})
private User user;
}
新增測試方法:
@Test
public void tesyPersistAndRemove(){
User user = User.builder()
.name("jackxx")
.email("123456@126.com")
.build();
UserInfo userInfo = UserInfo.builder()
.ages(12)
.user(user)
.telephone("12345678")
.build();
// 新建UserInfo,級聯(lián)新建User
userInfoRepo.save(userInfo);
// 刪除UserInfo,級聯(lián)刪除User
userInfoRepo.delete(userInfo);
}
執(zhí)行SQL如下所示:

從上面運行結(jié)果中可以看到,執(zhí)行insert的時候,會先插入user表,再插入user_info表。 執(zhí)行delete的時候,先刪除user_info表中數(shù)據(jù),再刪除user表中的數(shù)據(jù)。
上面只是講述級聯(lián)刪除的場景,下面我們再說一下關(guān)聯(lián)關(guān)系的刪除場景該怎么做?
1.4 orphanRemoval屬性用法
orphanRemoval表示當(dāng)關(guān)聯(lián)關(guān)系被刪除的時候,是否應(yīng)用級聯(lián)刪除。
首先我們,沿用上面的例子,當(dāng)我們刪除userinfo的時候,把user置空
userInfo.setUser(null); userInfoRepo.delete(userInfo);
再看運行結(jié)果
Hibernate: delete from user_info where id=?
我們只刪除了UserInfo的數(shù)據(jù),沒有刪除user的數(shù)據(jù),說明沒有進(jìn)行級聯(lián)刪除,我們將orphanRemoval屬性設(shè)置為true
@OneToOne(cascade = {CascadeType.PERSIST},orphanRemoval = true)
private User user;
測試代碼:
@Test
public void testRemove(){
User user = User.builder()
.name("jackxx")
.email("123456@126.com")
.build();
UserInfo userInfo = UserInfo.builder()
.ages(12)
.user(user)
.telephone("12345678")
.build();
// 新建UserInfo,級聯(lián)新建User
userInfoRepo.save(userInfo);
userInfo.setUser(null);
// 刪除UserInfo,級聯(lián)刪除User
userInfoRepo.delete(userInfo);
}
執(zhí)行結(jié)果如下所示:

在執(zhí)行結(jié)果中多了一條update語句,是因為去掉了CascadeType.REMOVE,這個時候不會進(jìn)行級聯(lián)刪除了。當(dāng)我們把user對象更新為null的時候,就會執(zhí)行一個update語句把關(guān)聯(lián)關(guān)系去掉。
1.5 orphanRemoval 和 CascadeType.REMOVE的區(qū)別
- CascadeType.REMOVE 級聯(lián)刪除,先刪除user表的數(shù)據(jù),再刪除user_info表的數(shù)據(jù)。 (因為存在外鍵關(guān)聯(lián),無法先刪除user_info表的數(shù)據(jù))
- orphanRemoval = true 先將user_info表中的數(shù)據(jù)外鍵user_id 更新為 null,然后刪除user_info表的數(shù)據(jù),再刪除user表的數(shù)據(jù)。
2、@JoinColumns & @JoinColumn
這兩個注解是集合關(guān)系,他們可以同時使用,@JoinColumn表示單字段,@JoinColumns表示多個@JoinColumn
@JoinColumn源碼
public @interface JoinColumn {
String name() default "";
String referencedColumnName() default "";
boolean unique() default false;
boolean nullable() default true;
boolean insertable() default true;
boolean updatable() default true;
String columnDefinition() default "";
String table() default "";
ForeignKey foreignKey() default @ForeignKey(PROVIDER_DEFAULT);
}
- name :代表外鍵的字段名。
- referencedColumnName :關(guān)聯(lián)表對應(yīng)的字段,如果不注明,默認(rèn)就是關(guān)聯(lián)表的主鍵
- unique:外鍵字段是否唯一
- nullable:外鍵字段是否允許為空
- insertable:是否跟隨一起新增
- updateable:是否跟隨一起更新
- columnDefinition:為列生成DDL時使用的SQL片段
- foreignKey:外鍵策略
// 外鍵策略
public enum ConstraintMode {
// 創(chuàng)建外鍵約束
CONSTRAINT,
// 不創(chuàng)建外鍵約束
NO_CONSTRAINT,
// 采用默認(rèn)行為
PROVIDER_DEFAULT
}
foreignKey的用法:
@OneToOne(cascade = {CascadeType.PERSIST},orphanRemoval = true)
@JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), name = "user_id")
private User user;
JoinColumns的用法:
@OneToOne(cascade = {CascadeType.PERSIST},orphanRemoval = true)
@JoinColumns({
@JoinColumn(name = "user_id",referencedColumnName = "ID"),
@JoinColumn(name = "user_ZIP",referencedColumnName = "ZIP")
})
private User user;
3、@ManyToOne & @OneToMany
@ManyToOne代表多對一的關(guān)聯(lián)關(guān)系,而@OneToMany代表一對多,一般兩個成對使用表示雙向關(guān)聯(lián)關(guān)系。在JPA協(xié)議中也是明確規(guī)定:維護(hù)關(guān)聯(lián)關(guān)系的是擁有外鍵的一方,而另一方必須配置mappedBy
public @interface OneToMany {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default LAZY;
String mappedBy() default "";
boolean orphanRemoval() default false;
}
public @interface ManyToOne {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default EAGER;
boolean optional() default true;
}
使用這兩個字段,需要注意以下幾點:
- @ManyToOne 一定是維護(hù)外鍵關(guān)系的一方,所以沒有mappedBy字段;
- @ManyToOne 刪除的時候一定不能把One的一方刪除了,所以也沒有orphanRemoval選項;
- @ManyToOne 的Lazy效果和 @OneToOne 的一樣,所以和上面的用法基本一致;
- @OneToMany 的Lazy是有效果的;
3.1 Lazy機(jī)制
舉例說明 : 假設(shè)User有多個地址Address
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
private String name;
private String email;
private String sex;
@OneToMany(mappedBy = "user",fetch = FetchType.LAZY)
private List<UserAddress> address;
}
@OneToMany 雙向關(guān)聯(lián)并且采用LAZY的機(jī)制
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserAddress {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String address;
@ManyToOne(cascade = CascadeType.ALL)
private User user;
}
測試代碼 :
@Test
@Transactional
public void testUserAddress(){
User user = User.builder()
.name("jackxx")
.email("123456@126.com")
.build();
UserAddress userAddress = UserAddress.builder()
.address("shanghai1")
.user(user)
.build();
UserAddress userAddress1 = UserAddress.builder()
.address("shanghai2")
.user(user)
.build();
addressRepo.saveAll(Lists.newArrayList(userAddress,userAddress1));
User u = userRepo.findById(1).get();
System.out.println(u.getName());
System.out.println(u.getAddress());
}
運行結(jié)果如下所示:

可以看到當(dāng)我們想要輸出Address信息的時候,才會加載Addres的信息
4、ManyToMany
@ManyToMany代表多對多的關(guān)聯(lián)關(guān)系、這種關(guān)聯(lián)關(guān)系任何一方都可以維護(hù)關(guān)聯(lián)關(guān)系。
我們假設(shè)user表和room表是多對多的關(guān)系,如下所示:
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
private String name;
@ManyToMany(mappedBy = "users")
private List<Room> rooms;
}
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Room {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
@ManyToMany
private List<User> users;
}
這種方法實不可取,當(dāng)用到@ManyToMany的時候一定是三張表,不要想著建兩張表,兩張表肯定是違背表的原則
改進(jìn)方法:創(chuàng)建中間表 修改Romm里面的內(nèi)容
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Room {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
@ManyToMany
@JoinTable(name = "user_room",
joinColumns = @JoinColumn(name = "room_id"),
inverseJoinColumns = @JoinColumn(name = "user_id"))
private List<User> users;
}
可以看到我們通過@JoinTable注解創(chuàng)建一張中間表,并且添加了兩個設(shè)定的外鍵,我們來看看@JoinTable的源碼:
public @interface JoinTable {
String name() default "";
String catalog() default "";
String schema() default "";
JoinColumn[] joinColumns() default {};
JoinColumn[] inverseJoinColumns() default {};
ForeignKey foreignKey() default @ForeignKey(PROVIDER_DEFAULT);
ForeignKey inverseForeignKey() default @ForeignKey(PROVIDER_DEFAULT);
UniqueConstraint[] uniqueConstraints() default {};
Index[] indexes() default {};
}
- name:中間表名稱
- joinColumns:維護(hù)關(guān)聯(lián)關(guān)系一方的外鍵字段的名字
- inverseJoinColumns:另一方表的外鍵字段的名字
在現(xiàn)實開發(fā)中,@ManyToMany注解用的比較少,一般都會使用成對的@ManyToOne 和 @OneToMany代替,因為我們的中間表可能還有一些約定的公共字段,如ID,update_time,create_time等其他字段
4.1 利用@ManyToOne 和 @OneToMany表達(dá)多對多的關(guān)聯(lián)關(guān)系
在上面的Demo中,我們稍作修改,新建一張user_room 中間表來存儲雙方的關(guān)聯(lián)關(guān)系和額外字段
如下所示: user_room中間表
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class user_room {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Date createTime;
private Date updateTime;
@ManyToOne
private User user;
@ManyToOne
private Room room;
}
user表
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@OneToMany(mappedBy = "user")
private List<user_room> userRoomList;
}
room表
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Room {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany(mappedBy = "room")
private List<user_room> roomList;
}
以上就是Spring Data JPA 注解Entity關(guān)聯(lián)關(guān)系使用詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring Data JPA Entity的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot創(chuàng)建動態(tài)定時任務(wù)的幾種方式小結(jié)
SpringBoot提供了多種實現(xiàn)定時任務(wù)的方式,包括使用@Scheduled注解、SchedulingConfigurer接口、TaskScheduler接口和Quartz框架,@Scheduled適合簡單的定時任務(wù),文中通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2024-10-10
Spring?Boot整合Log4j2.xml的問題及解決方法
這篇文章主要介紹了Spring?Boot整合Log4j2.xml的問題,本文給大家分享解決方案,需要的朋友可以參考下2023-09-09
Spring Boot使用Thymeleaf + Gradle構(gòu)建war到Tomcat
今天小編就為大家分享一篇關(guān)于Spring Boot使用Thymeleaf + Gradle構(gòu)建war到Tomcat,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12

