Spring Data JPA 快速入門實戰筆記

Spring Data JPA 入門實戰筆記

相關概念

ORM思想

  • Object-Relational Mapping 表示對象關係映射,在面向對象的軟件開發中,通過ORM,就可以把對象映射到關係型數據庫中。
  • 主要目的:
    操作實體類就相當於操作數據庫表,不再重點關注sql語句
  • 建立兩個映射關係:
    實體類和表的映射關係
    實體類中屬性和表中字段的映射關係
  • 實現了ORM思想的框架:
    Mybatis、Hibernate

Hibernate框架

  • 一個開放源代碼的對象關係映射框架
  • 對JDBC進行了非常輕量級的對象封裝
  • 將POJO與數據庫表建立映射關係,是一個全自動的ORM框架

JPA規範

  • JPA的全稱是Java Persistence API, 即Java持久化API,是SUN公司推出的一套基於ORM的規範,內部是由一系列的接口和抽象類構成。

  • JPA通過JDK 5.0註解描述對象-關係表的映射關係,並將運行期的實體對象持久化到數據庫中。

  • 優點:
    ①標準化
    任何聲稱符合 JPA 標準的框架都遵循同樣的架構,提供相同的訪問API,這保證了基於JPA開發的企業應用能夠經過少量的修改就能夠在不同的JPA框架下運行。
    ②容器級特性支持
    JPA框架中支持大數據集、事務、併發等容器級事務,這使得 JPA 超越了簡單持久化框架的侷限,在企業應用發揮更大的作用。
    ③簡單方便
    在JPA框架下創建實體和創建Java 類一樣簡單,沒有任何的約束和限制,只需要使用 javax.persistence.Entity進行註釋,JPA的框架和接口也都非常簡單,沒有太多特別的規則和設計模式的要求,開發者可以很容易的掌握。
    ④ 查詢能力
    JPA的查詢語言是面向對象而非面向數據庫的,JPA定義了獨特的JPQL(Java Persistence Query Language),它是針對實體的一種查詢語言,操作對象是實體,能夠支持批量更新和修改、JOIN、GROUP BY、HAVING 等通常只有 SQL 才能夠提供的高級查詢特性,甚至還能夠支持子查詢。
    ⑤高級特性
    JPA 中能夠支持面向對象的高級特性,如類之間的繼承、多態和類之間的複雜關係,這樣的支持能夠讓開發者最大限度的使用面向對象的模型設計企業應用,而不需要自行處理這些特性在關係數據庫的持久化。

  • JPA與hibernate的關係
    JPA規範本質上就是一種ORM規範,注意不是ORM框架——JPA並未提供ORM實現,它只是制訂了規範,提供了編程的API接口,具體實現則由服務廠商來提供。

    JPA和Hibernate的關係就像JDBC和JDBC驅動的關係,JPA是規範,Hibernate除了作爲ORM框架之外,它也是一種JPA實現。如果使用JPA規範進行數據庫操作,底層需要hibernate作爲其實現類完成數據持久化工作。
    在這裏插入圖片描述


Spring Data JPA概述

  • Spring Data JPA 是 Spring 基於 ORM 框架、JPA 規範的基礎上封裝的一套JPA應用框架,可使開發者用極簡的代碼即可實現對數據庫的訪問和操作。
  • 它提供了包括增刪改查等在內的常用功能,且易於擴展,可以極大提高開發效率。
  • Spring Data JPA 讓我們解脫了DAO層的操作,基本上所有CRUD都可以依賴於它來實現,在實際的工作工程中,推薦使用Spring Data JPA + ORM(如:hibernate)完成操作,這樣在切換不同的ORM框架時提供了極大的方便,同時也使數據庫層操作更加簡單,方便解耦。

Spring Data JPA的特性

  • 極大簡化了數據庫訪問層代碼。
  • Dao層中只需要寫接口,就自動具有了增刪改查、分頁查詢等方法。

快速入門案例

搭建 Spring Boot 環境

首先從 https://start.spring.io/ 構建一個Gradle的 SpringBoot 工程,選擇的組件包括Lombok、 Spring Data JPA 、Web 、MySQL。

下載之後解壓,先使用記事本打開 build.gradle ,在 repositories 添加以下配置:

mavenLocal()
maven {
    url 'http://maven.aliyun.com/nexus/content/groups/public/'
}

之後使用 IDEA 打開 build.gradle

完整 build.gradle 配置文件:

plugins {
	id 'org.springframework.boot' version '2.2.1.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
	id 'java'
}

group = 'com.sjh'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenLocal()
	maven {
            url 'http://maven.aliyun.com/nexus/content/groups/public/'
       	 }
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'mysql:mysql-connector-java'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

定義一個測試的 Controller:

@RestController
@RequestMapping("/test")
public class TestController {
    
    @RequestMapping("/hello")
    public String hello(){
        return "HELLO SPRING BOOT";
    }
}

找到 src/main/java 下的 SpringBoot 啓動類,由於此時還沒有配置數據庫,需要對@SpringBootApplication註解添加忽視數據庫配置的屬性(在配置數據庫信息後要取消):

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class SpringdatajpaApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringdatajpaApplication.class, args);
	}

}

然後啓動該類,訪問localhost:8080/test/hello:

此時 Spring Boot 的環境已經成功搭建了。


創建數據庫表

/*創建客戶表*/
CREATE TABLE customer (
	cust_id BIGINT(32) PRIMARY KEY AUTO_INCREMENT COMMENT '客戶編號(主鍵)',
	cust_name VARCHAR(32) NOT NULL COMMENT '客戶名稱(公司名稱)',
	cust_source VARCHAR(32) DEFAULT NULL COMMENT '客戶信息來源',
	cust_industry VARCHAR(32) DEFAULT NULL COMMENT '客戶所屬行業',
	cust_level VARCHAR(32) DEFAULT NULL COMMENT '客戶級別',
	cust_address VARCHAR(128) DEFAULT NULL COMMENT '客戶聯繫地址',
	cust_phone VARCHAR(64) DEFAULT NULL COMMENT '客戶聯繫電話'
);

配置數據庫信息

src/main/resources 下新建 application.yml 配置文件:

spring:
  datasource:
    url: jdbc:mysql:///test?characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: sjh2019
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

創建實體類

@Table 關聯實體類和表

@Column 關聯實體類屬性和表中字段

@Getter@Setter//Lombok的註解,自動生成getter、setter方法
@Entity//表明是一個實體類
@Table(name = "customer")
public class Customer {

    @Id//聲明主鍵
    @GeneratedValue(strategy = GenerationType.IDENTITY)//配置主鍵生產策略
    @Column(name = "cust_id")
    private Long id;//主鍵

    @Column(name = "cust_name")
    private String name;//名稱

    @Column(name = "cust_source")
    private String source;//來源

    @Column(name = "cust_industry")
    private String industry;//行業

    @Column(name = "cust_level")
    private String level;//級別

    @Column(name = "cust_address")
    private String address;//地址

    @Column(name = "cust_phone")
    private String phone;//聯繫方式

}

@Table 註解報錯其實是不影響正常執行的,如果要解決該問題,只需要配置一下IDEA的Database即可:


編寫持久層接口

只需要繼承 JpaRepository 即可,泛型參數列表中第一個參數是實體類類型,第二個參數是主鍵類型。

public interface CustomerRepo extends JpaRepository<Customer,Long> {
}

查詢

根據 id 查詢

@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomerRepoTest {

    @Autowired
    private CustomerRepo customerRepo;

    @Test
    public void findById(){
        Optional<Customer> customer = customerRepo.findById(1L);
        System.out.println(customer);
    }

}

結果(需要事先插入一些測試數據):


查詢所有

@Test
public void findAll(){
    List<Customer> customers = customerRepo.findAll();
    System.out.println(customers);
}

由於只插入了一條測試數據,結果仍然只有一條,但是可以發現 SQL 語句是不同的,不再有 WHERE 篩選:


添加和修改

添加

如果添加對象的 id 在表中還不存在,會執行添加操作。

@Test
public void save(){
    Customer customer = new Customer();
    customer.setId(3L);
    customer.setName("kobe");
    customerRepo.save(customer);
}

此時數據庫沒有 id=3 的記錄,根據 SQL 語句可以發現執行的是 insert 操作。


修改

如果添加對象的 id 在表中已經存在,會執行更新操作。

@Test
public void update(){
    Customer customer = new Customer();
    customer.setId(3L);
    customer.setName("sjh");
    customerRepo.save(customer);
}

此時數據庫有 id=3 的記錄,根據 SQL 語句可以發現執行的是 update 操作。


刪除

@Test
public void delete(){
    customerRepo.deleteById(3L);
}

可以發現此時 SQL 執行的是 delete 操作:


執行過程和原理總結:

  • 通過 JdkDynamicAopProxyinvoke 方法創建了一個動態代理對象 SimpleJpaRepository
  • SimpleJpaRepository 中封裝了JPA的操作。
  • 通過 hibernate(封裝了JDBC)完成數據庫操作。

複雜查詢

接口方法查詢

統計總數

使用 count() 方法查詢總數

@Test
public void count(){
    long count = customerRepo.count();
    System.out.println("記錄數:"+count);
}

執行的 SQL 語句和結果:


判斷存在

使用 existsById() 方法根據 id 查詢滿足條件的記錄數,如果值大於0代表存在,反之。

@Test
public void exists(){
    boolean exists = customerRepo.existsById(2L);
    System.out.println("該客戶記錄:"+ (exists?"存在":"不存在"));
}

執行的 SQL 語句和結果:


根據 id 查詢

與之前不同的是,使用 getOne() 方法根據 id 查詢。

注意使用 getOne() 方法要加上 @Transactional 註解,否則無法正常執行。

@Test
@Transactional
public void getOne(){
    Customer one = customerRepo.getOne(1L);
    System.out.println(one);
}

執行的 SQL 語句和結果:


findById() 方法的不同:

findById() 調用 find 方法,屬於立即加載
getOne() 調用 getReference 方法,獲得的是一個動態代理對象,屬於延遲加載,使用時才進行查詢,需要事務的支持。


使用 JPQL 查詢

根據名稱查詢

在接口中增加一個根據名稱查詢的方法:

//根據客戶名稱查詢
@Query(value = "from Customer where name = ?1 ")//?1表示參數列表的第一個參數
Customer findByName(String name);

測試類和方法:

@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomerRepoJPQLTest {

    @Autowired
    private CustomerRepo customerRepo;

    @Test
    public void findByName(){
        Customer sjh = customerRepo.findByName("sjh");
        System.out.println(sjh);
    }

}

執行的 SQL 和結果:


根據 id 和名稱查詢

接口方法:

//根據客戶id和名稱查詢
@Query(value = "from Customer where id = ?1 and name = ?2 ")
Customer findByIdAndName(Long id, String name);

測試方法:

@Test
public void findByIdAndName(){
    Customer sjh = customerRepo.findByIdAndName(4L,"sjh2");
    System.out.println(sjh);
}

執行的 SQL 和結果:


根據 id 來更新客戶名稱

接口方法:

//根據客戶id更新名稱
@Query(value = "update Customer set name = ?2 where id = ?1 ")
@Modifying//更新和刪除操作要使用該註解表明是修改操作
void updateNameById(Long id, String name);

測試方法:

@Test
@Transactional//JPQL除了查詢之外的修改操作需要事務支持
@Rollback(value = false)//默認回滾,需要取消該屬性
public void updateNameById(){
    customerRepo.updateNameById(1L,"sjh1");
}

執行的 SQL 和結果:


SQL 查詢

查詢全部

接口方法:

nativeQuery 屬性代表是否使用本地查詢,false是默認值即使用 JPQL 查詢, true表示使用 SQL 查詢。

@Query(value = "select * from customer", nativeQuery = true)
List<Customer> findAllBySQL();

測試方法:

@Test
public void findAllBySQL(){
    List<Customer> customers = customerRepo.findAllBySQL();
    System.out.println(customers);
}

執行的 SQL 和結果:


模糊查詢

接口方法:

nativeQuery 屬性代表是否使用本地查詢,false是默認值即使用 JPQL 查詢, true表示使用 SQL 查詢。

@Query(value = "select * from customer where cust_name like ?1 ",nativeQuery = true)
List<Customer> findByNameLike(String pattern);

測試方法:

@Test
public void findByNameLike(){
    List<Customer> customers = customerRepo.findByNameLike("sjh%");
    System.out.println(customers);
}

執行的 SQL 和結果:


方法名稱規則查詢

是對 JPQL 的封裝,只需要按照 Spring Data JPA 提供的方法名稱規則定義方法,不需要再進行配置。

findBy+ 查詢條件,在運行階段會根據方法名稱進行解析。

基礎查詢

根據 id 查詢 :

Optional<Customer> findById(Long id);

根據名稱查詢:

Customer findByName(String name);

模糊查詢

根據名稱模糊查詢:

Customer findByNameLike(String name);

多條件查詢

根據 id 和名稱精確查詢:

Customer findByIdAndName(Long id, String name);

根據名稱模糊查詢和行業精準查詢:

Customer findByNameLikeAndIndustry(String name, String industry);

Specifications動態查詢

使用動態查詢需要繼承另一個接口JpaSpecificationExecutor,泛型參數爲實體類類型:

public interface CustomerRepo extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer>

該接口的方法包括:

public interface JpaSpecificationExecutor<T> {
    
    //查詢單個對象
    Optional<T> findOne(@Nullable Specification<T> var1);

    //查詢全部
    List<T> findAll(@Nullable Specification<T> var1);
	
    //查詢全部並分頁,Pageable是分頁參數
    Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);

    //查詢全部並排序,Sort是排序參數
    List<T> findAll(@Nullable Specification<T> var1, Sort var2);

    //統計總數
    long count(@Nullable Specification<T> var1);
    
}

Specification 代表的是查詢條件,是一個接口,需要自定義實現該接口的toPredicate方法:

@Nullable
Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);

root 代表查詢的根對象,封裝了查詢屬性;CriteriaQuery 代表頂層查詢對象(一般不用);CriteriaBuilder代表查詢構造器,封裝了查詢條件。


條件查詢

查詢名稱爲 sjh 的記錄:

@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomerRepoSpecTest {

    @Autowired
    private CustomerRepo customerRepo;

    @Test
    public void findOne(){
        //構建自定義查詢條件
        Specification<Customer> spec = (Specification<Customer>) (root, criteriaQuery, criteriaBuilder) -> {
            Path<Object> name = root.get("name");//獲取比較的屬性
            return criteriaBuilder.equal(name, "sjh");
        };
        Optional<Customer> customer = customerRepo.findOne(spec);
        System.out.println(customer);
    }
    
}

執行的 SQL 和結果:


查詢名稱爲 sjh,行業爲 it 的記錄:

@Test
public void findOne2(){
    Specification<Customer> spec = (Specification<Customer>) (root, criteriaQuery, criteriaBuilder) -> {
        Path<Object> name = root.get("name");
        Path<Object> industry = root.get("industry");
        Predicate p1 = criteriaBuilder.equal(name, "sjh");
        Predicate p2 = criteriaBuilder.equal(industry, "it");
        return criteriaBuilder.and(p1,p2);
    };
    Optional<Customer> customer = customerRepo.findOne(spec);
    System.out.println(customer);
}

執行的 SQL 和結果:


根據客戶名稱模糊查詢:

對於 gt、lt、ge、le、like 等比較需要將 path 對象通過 as(Class<X> var1) 方法轉換成相應的類型參數。

@Test
public void findAll(){
    Specification<Customer> spec = (Specification<Customer>) (root, criteriaQuery, criteriaBuilder) -> {
        Path<Object> name = root.get("name");
        return criteriaBuilder.like(name.as(String.class),"s%");
    };
    List<Customer> customers = customerRepo.findAll(spec);
    System.out.println(customers);
}

執行的 SQL 和結果:


條件+排序查詢

根據名稱模糊查詢+根據 id 倒序排序:

使用 Sort.by()方法創建排序對象

@Test
public void findSort(){
    Specification<Customer> spec = (Specification<Customer>) (root, criteriaQuery, criteriaBuilder) -> {
        Path<Object> name = root.get("name");
        return criteriaBuilder.like(name.as(String.class),"s%");
    };
    //創建排序對象
    Sort sort = Sort.by(Sort.Direction.DESC, "id");
    List<Customer> customers = customerRepo.findAll(spec, sort);
    System.out.println(customers);
}

執行的 SQL 和結果:


條件+分頁查詢

分頁查詢,查詢第1頁,每頁顯示2條:

@Test
public void findPage(){
    Specification<Customer> spec = null;
    Pageable pageable = PageRequest.of(0,2);
    //創建分頁對象
    Page<Customer> customers = customerRepo.findAll(spec, pageable);
    System.out.println("總條數:" + customers.getTotalElements());
    System.out.println("頁數"+ customers.getTotalPages());
    System.out.println("數據"+ customers.getContent());
}

執行的 SQL 和結果:


多表查詢

一對多

一的一方爲主表,多的一方爲從表,需要在從表新建一列作爲外鍵,取值來源於主表的主鍵。

主表使用之前的 customer 表,從表使用 linkman 表。

  • 一對一
  • 一對多
    一的一方爲主表,多的一方爲從表,需要在從表新建一列作爲外鍵,取值來源於主表的主鍵
  • 多對多
    需要一張中間表,最少需要兩個字段作爲外鍵指向兩張表的主鍵,組成了聯合主鍵

實體類關係

  • 包含 通過實體類的包含關係描述表關係
  • 繼承

步驟

  • 明確表關係
  • 確定表關係(描述 通過外鍵|中間表)
  • 編寫實體類,在實體類中描述表關係(包含關係)
  • 配置映射關係

創建 linkman 表

/*創建聯繫人表*/
CREATE TABLE linkman (
  lkm_id BIGINT(32) PRIMARY KEY AUTO_INCREMENT COMMENT '聯繫人編號(主鍵)',
  lkm_name VARCHAR(16)  COMMENT '聯繫人姓名',
  lkm_gender CHAR(1)  COMMENT '聯繫人性別',
  lkm_phone VARCHAR(16)  COMMENT '聯繫人辦公電話',
  lkm_mobile VARCHAR(16)  COMMENT '聯繫人手機',
  lkm_email VARCHAR(64)  COMMENT '聯繫人郵箱',
  lkm_position VARCHAR(16)  COMMENT '聯繫人職位',
  lkm_memo VARCHAR(512)  COMMENT '聯繫人備註',
  lkm_cust_id BIGINT(32) NOT NULL COMMENT '客戶id(外鍵)',
  CONSTRAINT `FK_cst_linkman_lkm_cust_id` FOREIGN KEY (`lkm_cust_id`) REFERENCES `customer` (`cust_id`)
);


創建實體類和對應接口

@Getter@Setter
@Entity
@Table(name="linkman")
public class Linkman {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "lkm_id")
    private Long id;

    @Column(name ="lkm_name" )
    private String name;

    @Column(name = "lkm_gender")
    private String gender;

    @Column(name = "lkm_phone")
    private String phone;

    @Column(name = "lkm_mobile")
    private String mobile;

    @Column(name = "lkm_email")
    private String email;

    @Column(name = "lkm_position")
    private String position;

    @Column(name = "lkm_memo")
    private String memo;
    
}

-----
    
public interface LinkmanRepo extends JpaRepository<Linkman,Long> {
}

配置關係

在 Customer 類配置一對多關係

@OneToMany :配置一對多關係,mappedBy屬性 = 主表實體類在從表實體類中對應的屬性名。

@OneToMany(mappedBy = "customer")
@JoinColumn(name ="lkm_cust_id" ,referencedColumnName = "cust_id")
private Set<Linkman> linkmen=new HashSet<>();

在 Linkman 類配置多對一關係

在從表一方維護外鍵關係,提示效率。

@ManyToOne :配置多對一關係,targetEntity屬性 = 主表對應實體類的字節碼。

@JoinColumn:配置外鍵關係,name = 外鍵名稱,referencedColumnName = 主表主鍵名稱。

@ManyToOne(targetEntity = Customer.class)
@JoinColumn(name ="lkm_cust_id" ,referencedColumnName = "cust_id")
private Customer customer;

添加記錄

測試類和方法:

@RunWith(SpringRunner.class)
@SpringBootTest
public class OneToManyTest {

    @Autowired
    private CustomerRepo customerRepo;

    @Autowired
    private LinkmanRepo linkmanRepo;

    @Test
    @Transactional
    @Rollback(value = false)
    public void save(){
        //創建客戶和聯繫人
        Customer customer = new Customer();
        customer.setName("customer");
        Linkman linkman = new Linkman();
        linkman.setName("linkman");
        //保存外鍵關係
        linkman.setCustomer(customer);
        //保存客戶和聯繫人
        customerRepo.save(customer);
        linkmanRepo.save(linkman);
    }

}

執行的 SQL 和結果:


刪除記錄

**刪除從表數據:**可以任意刪除。
刪除主表數據:

  • 有從表數據

    • 在默認情況下,它會把外鍵字段置爲null,然後刪除主表數據。如果在數據庫的表 結構上,外鍵字段有非空約束,默認情況就會報錯。
    • 如果配置了放棄維護關聯關係的權利,則不能刪除(與外鍵字段是否允許爲null, 沒有關係)因爲在刪除時,它根本不會去更新從表的外鍵字段了。
    • 如果還想刪除,使用級聯刪除。
  • 沒有從表數據引用:隨便刪


級聯操作

指操作一個對象同時操作它的關聯對象
使用方法:只需要在操作主體的註解上配置 cascade 屬性。

//配置一對多關係
@OneToMany(mappedBy = "customer",cascade = CascadeType.ALL)
private Set<Linkman> linkmen = new HashSet<>();

級聯添加:

只需要保存客戶就可以保存聯繫人

@Test
@Transactional
@Rollback(false)
public void save(){
    //創建客戶和聯繫人
    Customer customer = new Customer();
    customer.setName("customer");
    Linkman linkman = new Linkman();
    linkman.setName("linkman");
    //保存外鍵關係
    linkman.setCustomer(customer);
    customer.getLinkmen().add(linkman);
    //保存客戶和聯繫人
    customerRepo.save(customer);
}

執行的 SQL 和結果:


級聯刪除:

只需要刪除客戶就可以刪除聯繫人

@Test
@Transactional
@Rollback(false)
public void delete(){
    Customer customer = customerRepo.getOne(13L);
    customerRepo.delete(customer);
}

執行的 SQL 和結果:


對象導航查詢

含義:查詢一個對象(get方法查詢)的同時,通過此對象可以查詢它的關聯對象。

對象導航查詢一到多默認使用延遲加載的形式查詢, 關聯對象是集合,使用立即加載可能浪費資源。

對象導航查詢多到一默認使用立即加載的形式查詢, 關聯對象是一個對象,所以使用立即加載。

如果要改變加載方式,在實體類註解配置加上fetch屬性即可,LAZY 表示延遲加載,EAGER 表示立即加載。


多對多

創建數據庫表

創建一個 user 表,字段爲 id 和 name。

創建一個 role 表,字段爲 id 和 name。

創建一個 user_role 中間表 ,字段爲 uid 和 rid。


創建對應實體類

@ManyToMany:配置多表之間的關係,targetEntity = 對方實體類的class對象。

@JoinTable:配置中間表關係,name = 中間表表名,joinColumns 配置自己在中間表的字段和對應實體類的屬性,inverseJoinColumns配置對方在中間表的字段和對應實體類的屬性。

User類:

@Getter@Setter
@Entity
@Table(name = "user")
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "name")
    private String name;

    @ManyToMany(targetEntity = Role.class)
    @JoinTable(name = "user_role",
            joinColumns = {@JoinColumn(name = "uid",referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name ="rid" ,referencedColumnName ="id" )})
    private Set<Role> roles=new HashSet<>();

}

Role類:

@Getter
@Setter
@Entity
@Table(name = "role")
public class Role implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "name")
    private String name;


    @ManyToMany(targetEntity = User.class)
    @JoinTable(name = "user_role",
            joinColumns = {@JoinColumn(name = "rid",referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name ="uid" ,referencedColumnName ="id" )})
    private Set<User> users=new HashSet<>();
    
}

創建接口

public interface UserRepo extends JpaRepository<User,Long> {
}
----------
public interface RoleRepo extends JpaRepository<Role,Long> {
}

添加記錄

@RunWith(SpringRunner.class)
@SpringBootTest
public class ManyToManyTest {

    @Autowired
    private UserRepo userRepo;

    @Autowired
    private RoleRepo roleRepo;

    @Test
    @Transactional
    @Rollback(false)
    public void save(){
        User user = new User();
        user.setName("sjh");
        Role role = new Role();
        role.setName("tw");

        user.getRoles().add(role);
        userRepo.save(user);
        roleRepo.save(role);
    }
    
}

執行的 SQL 和結果:


如果保存方法中,雙方同時執行報錯操作就會出錯:

@Test
@Transactional
@Rollback(false)
public void save(){
    ...
        user.getRoles().add(role);
    role.getUsers().add(user);
    ...
}

在多對多保存中,如果雙向都設置關係,意味着雙方都維護中間表,都會往中間表插入數據,中間表的2個字段又作爲聯合主鍵,所以報錯,主鍵重複,解決保存失敗的問題:只需要在任意一方放棄對中間表的維護權即可

修改 Role 中的外鍵配置爲:

@ManyToMany(mappedBy = "roles")
private Set<User> users=new HashSet<>();

再次執行保存操作就可以成功執行了。


級聯操作

和一對多的級聯操作類似,只需要添加 cascade屬性

在 User 類的 roles 屬性的 @ManyToMany 添加級聯屬性

@ManyToMany(targetEntity = Role.class, cascade = CascadeType.ALL)

測試方法:

只需要添加用戶即可自動添加角色並維護中間表關係。

@Test
@Transactional
@Rollback(false)
public void save(){
    User user = new User();
    user.setName("sjh");
    Role role = new Role();
    role.setName("tw");
    user.getRoles().add(role);
    userRepo.save(user);
}

執行的 SQL 和結果:


刪除也是一樣的:

只需要刪除用戶就會自動刪除對應的角色並維護中間表關係。

@Test
@Transactional
@Rollback(false)
public void delete(){
    userRepo.deleteById(5L);
}

執行的 SQL 和結果:


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章