Spring Boot 入門系列(二十八) JPA 的實體對映關係,一對一,一對多,多對多關係對映!

章為忠 發表於 2021-09-15
Spring

前面講了Spring Boot 使用 JPA,實現JPA 的增、刪、改、查的功能,同時也介紹了JPA的一些查詢,自定義SQL查詢等使用。JPA使用非常簡單,功能非常強大的ORM框架,無需任何資料訪問層和sql語句即可實現完整的資料操作方法。但是,之前都是介紹的單表的增刪改查等操作,多表多實體的資料操作怎麼實現呢?接下來聊一聊 JPA 的一對一,一對多,多對一,多對多等實體對映關係。

 

一、常用註解詳解

@JoinColumn指定該實體類對應的表中引用的表的外來鍵,name屬性指定外來鍵名稱,referencedColumnName指定應用表中的欄位名稱

@JoinColumn(name=”role_id”): 標註在連線的屬性上(一般多對1的1),指定了本類用1的外來鍵名叫什麼。

@JoinTable(name="permission_role") :標註在連線的屬性上(一般多對多),指定了多對多的中間表叫什麼。

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

@OneToOne 配置一對一關聯,屬性targetEntity指定關聯的物件的型別 。

@OneToMany註解“一對多”關係中‘一’方的實體類屬性(該屬性是一個集合物件),targetEntity註解關聯的實體類型別,mappedBy註解另一方實體類中本實體類的屬性名稱

@ManyToOne註解“一對多”關係中‘多’方的實體類屬性(該屬性是單個物件),targetEntity註解關聯的實體類型別

   屬性1: mappedBy="permissions" 表示,當前類不維護狀態,屬性值其實是本類在被標註的連結屬性上的連結屬性,此案例的本類時Permission,連線屬性是roles,連線屬性的類的連線屬性是permissions 

        屬性2: fetch = FetchType.LAZY 表示是不是懶載入,預設是,可以設定成FetchType.EAGER

        屬性3:cascade=CascadeType.ALL 表示當前類操作時,被標註的連線屬性如何級聯,比如班級和學生是1對多關係,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());
    }
}
@OneToMany(targetEntity=UsersDetail.class,fetch=FetchType.LAZY,mappedBy="source")
關聯的實體的主鍵一般是用來做外來鍵的。但如果此時不想主鍵作為外來鍵,則需要設定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、測試

   @RequestMapping("/save")
    public JSONResult save(){
        //使用者
        Users user = new Users();
        user.setName("springbootjpa");
        user.setAccount("admin");
        user.setPwd("123456");
        //詳情
        UsersDetail usersDetail = new UsersDetail();
        usersDetail.setAge(19);
        usersDetail.setAddress("beijing,haidian,");
        //儲存使用者和詳情
        user.setUserDetail(usersDetail);
        userRespository.save(user);
        return JSONResult.ok("儲存成功");
    }

 

二、一對多和對多對一

一對多和多對一的關係對映,最常見的場景就是:人員角色關係。實體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>();
}

最終生成的表結構 Users 表中會增加role_id 欄位。

 

3、測試

    @RequestMapping("/updateRole/{id}")
    public JSONResult updateRole(@PathVariable Long id) {
        Users user = userRespository.findById(id).orElse(null);
        Long roleId = Long.valueOf(25);
        Roles role = roleRespository.findById(roleId).orElse(null);
        if (user!=null){
            user.setRole(role);
        }
        userRespository.save(user);
        return JSONResult.ok("修改成功");
    }

主要特別注意的是更新和刪除的級聯操作:

其中 @OneToMany 和 @ManyToOne 用得最多,這裡再補充一下 關於級聯,一定要注意,要在關係的維護端,即 One 端。
比如 人員和角色,角色是One,人員是Many;cascade = CascadeType.ALL 只能寫在 One 端,只有One端改變Many端,不準Many端改變One端。 特別是刪除,因為 ALL 裡包括更新,刪除。
如果刪除一條評論,就把文章刪了,那算誰的。所以,在使用的時候要小心。一定要在 One 端使用。

 

三、多對多 

多對多的對映關係最常見的場景就是:許可權和角色關係。角色和許可權是多對多的關係。一個角色可以有多個許可權,一個許可權也可以被很多角色擁有。 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:這個屬性就是維護關係的意思!誰主類有此屬性誰不維護關係。
* 比如兩個多對多的關係是由role中的permissions維護的,那麼,只有操作role實體物件時,指定permissions,才可建立外來鍵的關係。
* 只有OneToOne,OneToMany,ManyToMany上才有mappedBy屬性,ManyToOne不存在該屬性; 並且mappedBy一直和joinXX互斥。

 

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

3、測試

    @RequestMapping("/save")
    public JSONResult save(){
        // 角色
        Roles role1 = new Roles();
        role1.setName("admin role");
        // 角色賦許可權
        Set<Permissions> ps = new HashSet<Permissions>();
        for (int i = 0; i < 3; i++) {
            Permissions pm = new Permissions();
            pm.setName("permission"+i);
            permissionRespository.save(pm);  /**由於我的Role類沒有設定級聯持久化,所以這裡需要先持久化pm,否則報錯!*/
            ps.add(pm);
        }
        role1.setPermissions(ps);
        // 儲存
        roleRespository.save(role1);
        return JSONResult.ok("儲存成功");
    }

 

配置說明:由於多對1不能用mapped那麼,它必然必須維護關係,即mapped屬性是在1的一方,維護關係是多的一方由User維護的,User的級聯是更新,Role的級聯是All,User的外來鍵是role_id指向Role。

 

說明:test1我們可以看到,由於role方是維護關係的,所以建立Roles.set(Permissions)就能把關係表建立,但是注意一點,由於我沒有設定級聯=all,而Permissions是個臨時物件,而臨時物件儲存時會持久化,如果不是我級聯儲存的話,那麼會報錯,解決辦法如測試範例,先通過save(pm),再操作。

          test2我們可以觀察到,當執行完後,中間表的刪除是由維護關係的role刪除了(自己都刪除了,關係肯定也需要維護的),但是,permission表還存在資料。

          test3我們可以觀察到,我把role.setPermission(null),就可以解除關係,中間表的對應的記錄也沒有了。


四、最後

維護關係是由mapped屬性決定,標註在那,那個就不維護關係。級聯操作是作用於當前類的操作發生時,對關係類進行級聯操作。

和hibernate使用沒多大區別啊!