天天看點

SpringBoot從入門到精通(二十八)JPA 的實體映射關系,一對一,一對多,多對多關系映射!

前面講了Spring Boot 使用 JPA,實作JPA 的增、删、改、查的功能,同時也介紹了JPA的一些查詢,自定義SQL查詢等使用。JPA使用非常簡單,功能非常強大的ORM架構,無需任何資料通路層和sql語句即可實作完整的資料操作方法。

但是,之前都是介紹的單表的增删改查等操作,多表多實體的資料操作怎麼實作呢?接下來聊一聊 JPA 的一對一,一對多,多對一,多對多等實體映射關系。

一、常用注解詳解

1、實體定義注解

(1) @JoinColumn指定該實體類對應的表中引用的表的外鍵,name屬性指定外鍵名稱,referencedColumnName指定應用表中的字段名稱

(2) @JoinColumn(name=”role_id”): 标注在連接配接的屬性上(一般多對一中的‘一’方),指定了本類的外鍵名叫什麼。

(3) @JoinTable(name="permission_role") :标注在連接配接的屬性上(一般多對多),指定了多對多的中間表叫什麼。

備注:Join的标注,和下面幾個标注的mappedBy屬性互斥!

2、關系映射注解

(1) @OneToOne 配置一對一關聯,屬性targetEntity指定關聯的對象的類型 。

(2) @OneToMany注解“一對多”關系中‘一’方的實體類屬性(該屬性是一個集合對象),targetEntity注解關聯的實體類類型,mappedBy注解另一方實體類中本實體類的屬性名稱

(3)@ManyToOne注解“一對多”關系中‘多’方的實體類屬性(該屬性是單個對象),targetEntity注解關聯的實體類類型

  • 屬性1: mappedBy="permissions" 表示,目前類不維護狀态,屬性值其實是本類在被标注的連結屬性上的連結屬性,此案例的本類時Permission,連接配接屬性是roles,連接配接屬性的類的連接配接屬性是permissions
  • 屬性2: fetch = FetchType.LAZY 表示是不是懶加載,預設是,可以設定成FetchType.EAGER
  • 屬性3:cascade=CascadeType.ALL 表示目前類操作時,被标注的連接配接屬性如何級聯,比如班級和學生是一對多關系,cascade标注在班級類中,那麼執行班級的save操作的時候(班級.學生s.add(學生)),能級聯儲存學生,否則報錯,需要先save學生,變成持久化對象,在班級.學生s.add(學生)

注意:隻有OneToOne,OneToMany,ManyToMany上才有mappedBy屬性,ManyToOne不存在該屬性;

二、一對一

首先,一對一的實體關系最常用的場景就是主表與從表,即主表存關鍵經常使用的字段,從表儲存非關鍵字段,類似 User與UserDetail 的關系。主表和詳細表通過外鍵一一映射。

一對一的映射關系通過@OneToOne 注解實作。通過 @JoinColumn 配置一對一關系。

其實,一對一有好幾種,這裡舉例的是常用的一對一雙向外鍵關聯(改造成單向很簡單,在對應的實體類去掉要關聯其它實體的屬性即可),并且配置了級聯删除和添加,相關類如下:

1、User 實體類定義:

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter
@Entity
@Table(name = "Users")
public class Users {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String account;
    private String pwd;
    @OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE})
    @JoinColumn(name="detailId",referencedColumnName = "id")
    private UsersDetail userDetail;
    @Override
    public String toString() {
        return String.format("Book [id=%s, name=%s, user detail=%s]", id, userDetail.getId());
    }
}      

上面的示例中,@OneToOne注解關聯實體映射,關聯的實體的主鍵一般是用來做外鍵的。但如果此時不想主鍵作為外鍵,則需要設定referencedColumnName屬性。當然這裡關聯實體(Address)的主鍵 id 是用來做主鍵,是以這裡第20行的 referencedColumnName = "id" 實際可以省略。

2、從表 UserDetail 實體類定義

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter
@Entity
@Table(name = "UsersDetail")
public class UsersDetail {
    @Id
    @GeneratedValue
    private Long id;
    @Column(name = "address")
    private String address;
    @Column(name = "age")
    private Integer age;
    @Override
    public String toString() {
        return String.format("UsersDetail [id=%s, address=%s, age=%s]", id,address,age);
    }
}      

代碼說明:子類無需任何定義,關系均在主類中維護。

3、驗證測試

建立單元測試方法,驗證一對一關系的儲存和查詢功能。

@Test
public void testOneToOne(){
    // 使用者
    User user = new User();
    user.setName("one2one");
    user.setPassword("123456");
    user.setAge(20);
    // 詳情
    UserDetail userDetail = new UserDetail();
    userDetail.setAddress("beijing,haidian,");
    // 儲存使用者和詳情
    user.setUserDetail(userDetail);
    userRepository.save(user);

    User result = userRepository.findById(7L).get();
    System.out.println("name:"+result.getName()+",age:"+result.getAge()+", address:"+result.getUserDetail().getAddress());
}
      

單擊Run Test或在方法上右擊,選擇Run 'testOneToOne',運作單元測試方法,結果如下圖所示。結果表明建立的單元測試運作成功,使用者資訊(User)和使用者詳細資訊(UserDetail)儲存成功,實作了一對一實體的級聯儲存和關聯查詢。

SpringBoot從入門到精通(二十八)JPA 的實體映射關系,一對一,一對多,多對多關系映射!

二、一對多和對多對一

一對多和多對一的關系映射,最常見的場景就是:人員角色關系。實體Users:人員。 實體 Roles:角色。 人員 和角色是一對多關系(雙向)。那麼在JPA中,如何表示一對多的雙向關聯呢?

JPA使用@OneToMany和@ManyToOne來辨別一對多的雙向關聯。一端(Roles)使用@OneToMany,多端(Users)使用@ManyToOne。在JPA規範中,一對多的雙向關系由多端(Users)來維護。也就是說多端(Users)為關系維護端,負責關系的增删改查。

一端(Roles)則為關系被維護端,不能維護關系。 一端(Roles)使用@OneToMany注釋的mappedBy="role"屬性表明Author是關系被維護端。

多端(Users)使用@ManyToOne和@JoinColumn來注釋屬性 role,@ManyToOne表明Article是多端,@JoinColumn設定在Users表中的關聯字段(外鍵)。

1、原先的User 實體類修改如下:

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter
@Entity
@Table(name = "Users")
public class Users {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String account;
    private String pwd;
    @OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE})
    @JoinColumn(name="detailId",referencedColumnName = "id")
    private UsersDetail userDetail;
    /**一對多,多的一方必須維護關系,即不能指定mapped=""**/
    @ManyToOne(fetch = FetchType.LAZY,cascade=CascadeType.MERGE)
    @JoinColumn(name="role_id")
    private Roles role;
    @Override
    public String toString() {
        return String.format("Book [id=%s, name=%s, user detail=%s]", id, userDetail.getId());
    }
}      

2、角色實體類

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Getter
@Setter
@Entity
@Table(name = "Roles")
public class Roles {
    @Id
    @GeneratedValue()
    private Long id;
    private String name;
  
    @OneToMany(mappedBy="role",fetch=FetchType.LAZY,cascade=CascadeType.ALL)
    private Set<Users> users = new HashSet<Users>();
}      

其中 @OneToMany 和 @ManyToOne 用得最多,這裡再補充一下 關于級聯,一定要注意,要在關系的維護端,即 One 端。

比如 人員和角色,角色是One,人員是Many;cascade = CascadeType.ALL 隻能寫在 One 端,隻有One端改變Many端,不準Many端改變One端。 特别是删除,因為 ALL 裡包括更新,删除。

如果删除一條評論,就把文章删了,那算誰的。是以,在使用的時候要小心。一定要在 One 端使用。

最終生成的表結構 Users 表中會增加role_id 字段。

@Test
public void testOneToMany() {
    // 儲存角色
    Role role = new Role();
    role.setId(3L);
    role.setName("管理者");
    roleRepository.save(role);
    // 修改人員角色
    User user = userRepository.findById(7L).orElse(null);
    Role admin = roleRepository.findById(3L).orElse(null);
    if (user!=null){
        user.setRole(admin);
    }
    userRepository.save(user);
    User result = userRepository.findById(7L).get();
    System.out.println("name:"+result.getName()+",age:"+result.getAge()+", role:"+result.getRole().getName());
} 
      

特别注意的是更新和删除的級聯操作。

單擊Run Test或在方法上右擊,選擇Run 'testOneToMany',運作單元測試方法,結果下圖所示。

SpringBoot從入門到精通(二十八)JPA 的實體映射關系,一對一,一對多,多對多關系映射!

三、多對多

多對多的映射關系最常見的場景就是:權限和角色關系。角色和權限是多對多的關系。一個角色可以有多個權限,一個權限也可以被很多角色擁有。

JPA中使用@ManyToMany來注解多對多的關系,由一個關聯表來維護。這個關聯表的表名預設是:主表名+下劃線+從表名。(主表是指關系維護端對應的表,從表指關系被維護端對應的表)。這個關聯表隻有兩個外鍵字段,分别指向主表ID和從表ID。字段的名稱預設為:主表名+下劃線+主表中的主鍵列名,從表名+下劃線+從表中的主鍵列名。

需要注意的:

1、多對多關系中一般不設定級聯儲存、級聯删除、級聯更新等操作。

2、可以随意指定一方為關系維護端,在這個例子中,我指定 User 為關系維護端,是以生成的關聯表名稱為: role_permission,關聯表的字段為:role_id 和 permission_id。

3、多對多關系的綁定由關系維護端來完成,即由 role1.setPermissions(ps);來綁定多對多的關系。關系被維護端不能綁定關系,即permission不能綁定關系。

4、多對多關系的解除由關系維護端來完成,即由 role1.getPermissions().remove(permission);來解除多對多的關系。關系被維護端不能解除關系,即permission不能解除關系。

5、如果Role和Permission已經綁定了多對多的關系,那麼不能直接删除Permission,需要由Role解除關系後,才能删除Permission。但是可以直接删除Role,因為Role是關系維護端,删除Role時,會先解除Role和Permission的關系,再删除Role。

下面,看看角色Roles 和 權限 Permissions 的多對多的映射關系實作,具體代碼如下:

1、角色Roles 實體類定義:

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Getter
@Setter
@Entity
@Table(name = "Roles")
public class Roles {
    @Id
    @GeneratedValue()
    private Long id;
    private String name;
   
    @ManyToMany(cascade = CascadeType.MERGE,fetch = FetchType.LAZY)
    @JoinTable(name="permission_role")
    private Set<Permissions> permissions = new HashSet<Permissions>();
    @OneToMany(mappedBy="role",fetch=FetchType.LAZY,cascade=CascadeType.ALL)
    private Set<Users> users = new HashSet<Users>();
}      

代碼說明:

cascade表示級聯操作,all是全部,一般用MERGE 更新,persist表示持久化即新增

此類是維護關系的類,删除它,可以删除對應的外鍵,但是如果需要删除對應的權限就需要CascadeType.all

cascade:作用在本放,對于删除或其他操作本方時,對标注連接配接方的影響!和資料庫一樣!!

2、權限Permissions 實體類定義:

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.Set;
/**
 * 權限表
*/
@Getter
@Setter
@Entity
@Table(name="Permissions")
public class Permissions {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String type;
    private String url;
    @Column(name="perm_code")
    private String permCode;
    @ManyToMany(mappedBy="permissions",fetch = FetchType.LAZY)
    private Set<Roles> roles;
}      

注意:不能兩邊用mappedBy:這個屬性就是維護關系的意思!誰主類有此屬性誰不維護關系。

  1. 比如兩個多對多的關系是由role中的permissions維護的,那麼,隻有操作role實體對象時,指定permissions,才可建立外鍵的關系。
  2. 隻有OneToOne,OneToMany,ManyToMany上才有mappedBy屬性,ManyToOne不存在該屬性; 并且mappedBy一直和joinXX互斥。

注解中屬性的漢語解釋:權限不維護關系,關系表是permission_role,全部懶加載,角色的級聯是更新 (多對多關系不适合用all,不然删除一個角色,那麼所有此角色對應的權限都被删了,級聯删除一般用于部分一對多時業務需求上是可删的,比如品牌類型就不适合删除一個類型就級聯删除所有的品牌,一般是把此品牌的類型設定為null(解除關系),然後執行删除,就不會報錯了!)

@Test
public void testManyToMany(){
    // 角色
    Roles role1 = new Roles();
    role1.setName("admin role");
    // 角色賦權限
    Set<Permissions> ps = new HashSet<Permissions>();
    for (int i = 0; i < 3; i++) {
        Permission pm = new Permission();
        pm.setName("permission"+i);
        permissionRespository.save(pm);  /**由于Role類沒有設定級聯持久化,是以這裡需要先持久化pm,否則報錯!*/
        ps.add(pm);
    }
    role1.setPermissions(ps);
    // 儲存
    roleRespository.save(role1);
}
      

 配置說明:由于多對一不能用mapped,那麼它必然必須維護關系,維護關系是多的一方由User維護的,User的級聯是更新,Role的級聯是All,User的外鍵是role_id指向Role。

最後

維護關系是由mapped屬性決定,标注在那,那個就不維護關系。級聯操作是作用于目前類的操作發生時,對關系類進行級聯操作。和hibernate使用沒多大差別啊!

推薦閱讀:

SpringBoot從入門到精通(二十七)JPA實作自定義查詢,完全不需要寫SQL! SpringBoot從入門到精通(二十六)超級簡單的資料持久化架構!Spring Data JPA 的使用! SpringBoot從入門到精通(二十五)搞懂自定義系統配置 SpringBoot從入門到精通(二十四)3分鐘搞定Spring Boot 多環境配置! SpringBoot從入門到精通(二十三)Mybatis系列之——實作Mybatis多資料源配置 SpringBoot從入門到精通(二十二)使用Swagger2優雅建構 RESTful API文檔