首頁 > 軟體

Spring Data JPA 註解Entity關聯關係使用詳解

2022-09-30 14:01:56

首先,實體與實體之間的關聯關係一共分為四種,分別為OneToOne、OneToMany、ManyToOne和ManyToMany;而實體之間的關聯關係又分為雙向和單向。實體之間的關聯關係是在JPA使用中最容易發生問題的地方。

1、OneToOne關聯關係

@OneToOne一般表示物件之間一對一的關聯關係,它可以放在field上面,也可以放在get/set方法上面。其中JPA協定有規定,如果設定雙向關聯,維護關聯關係的是擁有外來鍵的一方,而另一方必須設定mappedBy;如果是單項關聯,直接設定在擁有外來鍵的一方即可。

舉例說明:

user表是使用者的主資訊,user_info是使用者的拓展資訊,兩者之間是一對一的關係。user_info表裡面有一個user_id作為關聯關係的外來鍵,如果是單項關聯,我們的寫法如下:

@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;
}

這就是單向關聯關係,那麼如何設定雙向關聯關係呢? 我們保持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:作為關聯目標的實體類。
cascade:級聯操作策略,就是我們常說的級聯操作。
fetch:資料獲取方式EAGER(立即載入)/LAZY(延遲載入) optional:表示關聯的實體是否能夠存在null值 mappedBy:關聯關係被誰維護的一方物件裡面的屬性名字,雙向關聯的時候必填。

1.2 mappedBy 注意事項

  • 只有關聯關係的維護方才能操作兩個實體之間外來鍵的關係。被維護方即使設定維護方屬性進行儲存也不會更新外來鍵關聯
  • mappedBy不能與@JoinColumn或者@JoinTable同時使用,因為沒有任何意義,關聯關係不在這裡面維護。
  • mappedBy的值是指另一方的實體裡面屬性的欄位,而不是資料庫欄位,也不是實體的物件的名字。也就是維護關聯關係的一方屬性欄位名稱,或者加了@JoinColumn 或 @JoinTable註解的屬性欄位名稱。如上面的User例子user裡面的mappedBy的值,就是userinfo裡面的user欄位的名字。

1.3 CascadeType 用法

在CascadeType的用法中,CascadeType的列舉值只有5個,分別如下:

  • CascadeType.PERSIST 級聯新建
  • CascadeType.REMOVE 級聯刪除
  • CascadeType.PEFRESH 級聯重新整理
  • CascadeType.MERGE 級聯更新
  • CascadeType.ALL 四項全選

測試級聯新建和級聯刪除:

第一步: 在@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,級聯新建User
    userInfoRepo.save(userInfo);
    // 刪除UserInfo,級聯刪除User
    userInfoRepo.delete(userInfo);
}

執行SQL如下所示:

從上面執行結果中可以看到,執行insert的時候,會先插入user表,再插入user_info表。 執行delete的時候,先刪除user_info表中資料,再刪除user表中的資料。

上面只是講述級聯刪除的場景,下面我們再說一下關聯關係的刪除場景該怎麼做?

1.4 orphanRemoval屬性用法

orphanRemoval表示當關聯關係被刪除的時候,是否應用級聯刪除。

首先我們,沿用上面的例子,當我們刪除userinfo的時候,把user置空

userInfo.setUser(null);
userInfoRepo.delete(userInfo);

再看執行結果

Hibernate: delete from user_info where id=?

我們只刪除了UserInfo的資料,沒有刪除user的資料,說明沒有進行級聯刪除,我們將orphanRemoval屬性設定為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,級聯新建User
    userInfoRepo.save(userInfo);
    userInfo.setUser(null);
    // 刪除UserInfo,級聯刪除User
    userInfoRepo.delete(userInfo);
}

執行結果如下所示:

在執行結果中多了一條update語句,是因為去掉了CascadeType.REMOVE,這個時候不會進行級聯刪除了。當我們把user物件更新為null的時候,就會執行一個update語句把關聯關係去掉。

1.5 orphanRemoval 和 CascadeType.REMOVE的區別

  • CascadeType.REMOVE 級聯刪除,先刪除user表的資料,再刪除user_info表的資料。 (因為存在外來鍵關聯,無法先刪除user_info表的資料)
  • orphanRemoval = true 先將user_info表中的資料外來鍵user_id 更新為 null,然後刪除user_info表的資料,再刪除user表的資料。

2、@JoinColumns & @JoinColumn

這兩個註解是集合關係,他們可以同時使用,@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 :關聯表對應的欄位,如果不註明,預設就是關聯表的主鍵
  • unique:外來鍵欄位是否唯一
  • nullable:外來鍵欄位是否允許為空
  • insertable:是否跟隨一起新增
  • updateable:是否跟隨一起更新
  • columnDefinition:為列生成DDL時使用的SQL片段
  • foreignKey:外來鍵策略
// 外來鍵策略
public enum ConstraintMode {
  // 建立外來鍵約束
  CONSTRAINT,
  // 不建立外來鍵約束
  NO_CONSTRAINT,
  // 採用預設行為
  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代表多對一的關聯關係,而@OneToMany代表一對多,一般兩個成對使用表示雙向關聯關係。在JPA協定中也是明確規定:維護關聯關係的是擁有外來鍵的一方,而另一方必須設定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 一定是維護外來鍵關係的一方,所以沒有mappedBy欄位;
  • @ManyToOne 刪除的時候一定不能把One的一方刪除了,所以也沒有orphanRemoval選項;
  • @ManyToOne 的Lazy效果和 @OneToOne 的一樣,所以和上面的用法基本一致;
  • @OneToMany 的Lazy是有效果的;

3.1 Lazy機制

舉例說明 : 假設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 雙向關聯並且採用LAZY的機制

@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());
}

執行結果如下所示:

可以看到當我們想要輸出Address資訊的時候,才會載入Addres的資訊

4、ManyToMany

@ManyToMany代表多對多的關聯關係、這種關聯關係任何一方都可以維護關聯關係。

我們假設user表和room表是多對多的關係,如下所示:

@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;
}

這種方法實不可取,當用到@ManyToMany的時候一定是三張表,不要想著建兩張表,兩張表肯定是違背表的原則

改進方法:建立中間表 修改Romm裡面的內容

@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註解建立一張中間表,並且新增了兩個設定的外來鍵,我們來看看@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:維護關聯關係一方的外來鍵欄位的名字
  • inverseJoinColumns:另一方表的外來鍵欄位的名字

在現實開發中,@ManyToMany註解用的比較少,一般都會使用成對的@ManyToOne 和 @OneToMany代替,因為我們的中間表可能還有一些約定的公共欄位,如ID,update_time,create_time等其他欄位

4.1 利用@ManyToOne 和 @OneToMany表達多對多的關聯關係

在上面的Demo中,我們稍作修改,新建一張user_room 中間表來儲存雙方的關聯關係和額外欄位

如下所示: 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關聯關係使用詳解的詳細內容,更多關於Spring Data JPA Entity的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com