自动化SQL编写Springboot整合Springdata-jpa—让编程更简单

1,941 阅读19分钟

Springboot整合Springdata-jpa

「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。

关于作者

  • 作者介绍

🍓 博客主页:作者主页
🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆、阿里云专家博主51CTO专家博主
🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻


1、简介

Spring Data 项⽬的⽬的是为了简化构建基于 Spring 框架应⽤的数据访问,包括关系型数据库库、⾮关 系型数据库、Map-Reduce 框架、云数据服务等。Spring Data JPA是Spring Data下⾯的⼀个⼦项⽬, 其提供了对JPA的操作⽀持。

spring-data

spring-data-jpa

spring-data-jpa 2.6.0 Reference Documentation

image-20220108113231120

image-20220108113443900

JPA(Java Persistence API)是当年的Sun官⽅提出的Java持久化规范。它为Java开发⼈员提供了⼀种对象/ 关联映射⼯具来管理Java应⽤中的关系数据。 它的出现主要是为了简化现有的持久化开发⼯作和整合ORM技术,结束现在Hibernate,TopLink,JDO 等ORM框架各⾃为营的局⾯。值得注意的是,JPA是在现有Hibernate,TopLink,JDO等ORM框架的基 础上发展⽽来的。JPA在JAVA EE 5的时候也加⼊了其规范之中。

image-20220108113520856

**注意:**JPA是⼀套规范,⽽不是产品。Hibernate是⼀个ORM框架,它实现了JPA的规范。

Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的⼀套JPA应⽤框架,可使开发者⽤ 极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常⽤功能,使⽤ Spring Data JPA 可以极⼤提⾼开发效率! Spring Data JPA让我们解脱了DAO层的操作,基本上所有CRUD都可以依赖于它来实现。

在springboot的⾃动配置的功能下,springdata-jpa使⽤起来变的更加的⽅便。

2、使用

springboot中使⽤Spring Data JPA,需要引⼊其对应的starter,maven会⾃动引⼊JPA及其相应的实 现,它默认采⽤的是hibernate的实现,通过⼏步简单的操作,就可以使⽤spring-data-jpa框架了,例如 : springdata-jpa项⽬

目录结构

image-20220108120248839

pom.xml⽂件:引⼊springdata-jpa其对应的starter

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.sxau</groupId>
    <artifactId>demojpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demojpa</name>
    <description>JPAdemo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.12.4</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

maven会⾃动引⼊JPA及其相应的实现,我们观察一下maven引入的依赖,发现它默认采⽤的是hibernate的实现。

image-20220108114854349

application.properties⽂件信息

spring.datasource.url=jdbc:mysql://localhost:3306/advertise?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT&2B
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#如果需要,可以进行自动建表
spring.jpa.hibernate.ddl-auto=update
#如果需要,可以显示执行的sql语句句
# 如果需要,可以进行自动建表
#create可以每次启动测试先进行删除表,然后再建表
# update可以每次启动测试时先检查数据库中是否有表,没对应的表则会自动建表,有表单话就不再创建了
spring.jpa.showsql=true
#如果需要,可以格式化sq1语句
spring.jpa.properties.hibernate.format_sql=true

springboot中对spring Data JPA的配置参数 :

官网地址

image-20220108115516908

Spring Data JPA中采⽤泛型接⼝的形式,预先定义好了⼀些基本的CURD的⽅法。泛型中需要提供操作 的实体类型以及对应的主键类型。

注意:我们⽆需实现这些⽅法,Spring Data JPA框架中已经实现了,我们只需在依赖注⼊后,直 接使⽤即可。

接下来我们看看org.springframework.data.jpa.repository.JpaRepository

image-20220108115818501

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.data.jpa.repository;

import java.util.List;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;

@NoRepositoryBean
//泛型T 表示要操作的实体类是什么类型,泛型ID表示这个实体类中对应数据库中主键列的属性是什么类型
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    List<T> findAll();

    List<T> findAll(Sort sort);

    List<T> findAllById(Iterable<ID> ids);

    <S extends T> List<S> saveAll(Iterable<S> entities);

    void flush();

    <S extends T> S saveAndFlush(S entity);

    <S extends T> List<S> saveAllAndFlush(Iterable<S> entities);

    /** @deprecated */
    @Deprecated
    default void deleteInBatch(Iterable<T> entities) {
        this.deleteAllInBatch(entities);
    }

    void deleteAllInBatch(Iterable<T> entities);

    void deleteAllByIdInBatch(Iterable<ID> ids);

    void deleteAllInBatch();

    /** @deprecated */
    @Deprecated
    T getOne(ID id);

    T getById(ID id);

    <S extends T> List<S> findAll(Example<S> example);

    <S extends T> List<S> findAll(Example<S> example, Sort sort);
}

JpaRepository继承的所有的⽗接⼝:

image-20220108120014007

Repository 接⼝为顶级接⼝,其中没有提供任何抽象⽅法。

CrudRepository 接⼝,提供了基本的CURD⽅法。

PagingAndSortingRepository 接⼝,提供了对分⻚和排序⽀持的⽅法。

QueryByExampleExecutor 接⼝,提供了QBE的查询⽅式。

JpaRepository 接⼝,继承了以上接⼝,并重写了⼀些⽅法,以及定义了⼀些新⽅法。

在使⽤的时候,只需要⾃定义mapper层接⼝,然后继承JpaRepository接⼝,并指定接⼝中泛型的类型。

mapper层接⼝UserMapper

package com.sxau.demojpa.mapper;

import com.sxau.demojpa.pojo.User;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.dao
 * @ClassName: UserMapper
 * @Author: 张晟睿
 * @Date: 2022/1/7 16:05
 * @Version: 1.0
 */
public interface UserMapper extends JpaRepository<User, Long> {
}

实体类User

package com.sxau.demojpa.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.bean
 * @ClassName: User
 * @Author: 张晟睿
 * @Date: 2022/1/7 16:06
 * @Version: 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table
public class User implements Serializable {
//    @ld表示当前属性对应t_user表中的主键列
//    @GeneratedValue 表示该属性对应的t user表中列值是自动增长的
//    @Column(nullable-false,unique-true)表示当前属性对应t user表中的列值是非空唯- -的
//    @Enumerated(EnumType.STRING)表示当前属性对应枚举类型的String形式,而不是原始编号
//    注意, 如果是oralce数据库的话,可以使用序列来产生主键列的值,上面代码的注 释部分有说明。

    private static final Long serialVersionUID = 1L;
    @Id
    @GeneratedValue
    private Long id;
//    nullable 是否为空  unique是否唯一
    @Column(nullable = false, unique = true)
    private String name;

    @Column(nullable = false)
    private Integer age;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Gender gender;

	public User(String name, Integer age, Gender gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
}

我们来解释一下上面几个注解是什么意思哈

@Entity表示当前类是JPA中的⼀个实体类
@Table(name="t_user")表示当前实体类对应数据库中的表为 t_user
@Id表示当前属性对应t_user表中的主键列
@@GeneratedValue表示该属性对应的t_user表中列值是⾃动增⻓的
@@Column(nullable=false,unique=true)表示当前属性对应t_user表中的列值是⾮空唯⼀的
@Enumerated(EnumType.STRING)表示当前属性对应枚举类型的String形式,⽽不是原始编号

测试类

package com.sxau.demojpa;

import com.sxau.demojpa.mapper.StudentMapper;
import com.sxau.demojpa.mapper.UserMapper;
import com.sxau.demojpa.pojo.Address;
import com.sxau.demojpa.pojo.Gender;
import com.sxau.demojpa.pojo.Student;
import com.sxau.demojpa.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class DemojpaApplicationTests {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private StudentMapper studentMapper;
    @Test
    void contextLoads() {
        User user1 = new User("tom1",20,Gender.MALE);
        User user2 = new User("tom2",21,Gender.MALE);
        User user3 = new User("tom3",22,Gender.FEMALE);
        userMapper.save(user1);
        userMapper.save(user2);
        userMapper.save(user3);

        System.out.println("-----------------------");
        List<User> list = userMapper.findAll();
        list.forEach(System.out::println);

        System.out.println("-----------------------");
        User user = userMapper.findById(3L).orElse(null);
        System.out.println("更新前: "+user);

        user.setName("zsr");
        userMapper.save(user);
        System.out.println("更新后: "+user);

        System.out.println("-----------------------");
        userMapper.deleteAllInBatch();
    }
}

image-20220108164446208

我们发现了运行时,JPA会自动建表,主要因为我们在application.properties进行配置:

 spring.jpa.hibernate.ddl-auto=update

mapper层接⼝中,只继承了JpaRepository接⼝,并没有定义其他⽅法,但是基本的操作依然使⽤成 功调⽤。

3、查询

Spring Data JPA中提供了强⼤的查询功能,我们只需要在mapper层接⼝中按规则,定出对应的查询⽅法, 那么Spring Data JPA会根据我们所定义的==⽅法名==,⾃动⽣成对应的sql语句,例如:

package com.sxau.demojpa.mapper;

import com.sxau.demojpa.pojo.Gender;
import com.sxau.demojpa.pojo.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.dao
 * @ClassName: UserMapper
 * @Author: 张晟睿
 * @Date: 2022/1/7 16:05
 * @Version: 1.0
 */
public interface UserMapper extends JpaRepository<User, Long> {
    User findByName(String name);

    List<User> findByAge(int age);

    List<User> findByNameOrAge(String name, int age);

    List<User> findByNameLike(String name);

    User findByNameIgnoreCase(String name);

    List<User> findByAgeOrderByNameDesc(int age);

    //first和top的效果是⼀样的,默认去第⼀条数据
    User findFirstByOrderByAgeAsc();

    User findFirstByOrderByAgeDesc();

    User findTopByOrderByAgeAsc();

    User findTopByOrderByAgeDesc();

    //first和top后⾯都可以跟数字,表示取查询出来数据的前N条数据
    List<User> findFirst2ByGender(Gender gender);
}

当在测试类中调⽤我们⾃⼰在接⼝中定义的 findByName ⽅法的时候,会⾃动⽣成对应的sql语句:

    @Test
    public void test_findByName() throws Exception {
        User user = userMapper.findByName("zsr");
        System.out.println(user);
    }

控制台中输出的sql为:

Hibernate: 
    select
        user0_.id as id1_2_,
        user0_.age as age2_2_,
        user0_.gender as gender3_2_,
        user0_.name as name4_2_ 
    from
        t_user user0_ 
    where
        user0_.name=?

image-20220108170838413

当在测试类中调⽤我们⾃⼰在接⼝中定义的 findFirst2ByGender ⽅法的时候,会⾃动⽣成对应的sql 语句:

@Test
public void test_findFirst2ByGender() throws Exception {
 List<User> list = userMapper.findFirst2ByGender(Gender.MALE);
 list.forEach(System.out::println);
}

控制台中输出的sql为:

Hibernate:
 select
 user0_.id as id1_1_,
 user0_.age as age2_1_,
 user0_.gender as gender3_1_,
 user0_.name as name4_1_
 from
 t_user user0_
 where
 user0_.gender=? limit ?

image-20220108170925056

可以输出sql语句,因为在application.properties中配置了对应的属性: spring.jpa.properties.hibernate.format_sql=true

在⽅法中所⽀持的关键字有很多,官⽹说明如下:

Keyword ⽅法名中⽀持的关键字

Sample ⽅法名示例

JPQL snippet JPA⽣成的sql中的⽚段(查询条件部分 )

Supported keywords inside method names

Keyword
And
or
Is, Equals
Between
LessThan
LessThanEqual
GreaterThan !
GreaterThanEqual
After
Before
IsNull, Null
IsNotNull,NotNull
Like
NotLike
Startingwith
EndingWith
Containing
OrderBy
Not
In
NotIn
True
False
IgnoreCase
只要按照以上规则在接⼝中定义⽅法,那么Spring Data JPA会⾃动⽣成对应的==条件查询==语句

4、排序

Spring Data JPA中可以使⽤ Sort 对查询的结果进⾏排序,让 Sort 作为查询⽅法的参数即可。 ⽗接⼝中已经有定好的⽅法的参数含有 Sort ,例如 JpaRepository 中的 List findAll(Sort sort);

也可以⾃⼰在接⼝中⾃定义⽅法的参数中添加 Sort 参数:

package com.sxau.demojpa.mapper;

import com.sxau.demojpa.pojo.Gender;
import com.sxau.demojpa.pojo.User;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.dao
 * @ClassName: UserMapper
 * @Author: 张晟睿
 * @Date: 2022/1/7 16:05
 * @Version: 1.0
 */
public interface UserMapper extends JpaRepository<User, Long> {

    List<User> findByGender(Gender gender, Sort sort);

    List<User> findFirst25ByGender(Gender gender, Sort sort);
}
@Test
public void test_sort1() throws Exception {
    List<User> list = userMapper.findAll(Sort.by(Sort.Direction.DESC, "age"));
    list.forEach(System.out::println);
}

⽣成的sql语句为:

Hibernate: 
    select
        user0_.id as id1_2_,
        user0_.age as age2_2_,
        user0_.gender as gender3_2_,
        user0_.name as name4_2_ 
    from
        t_user user0_ 
    order by
        user0_.age desc

image-20220108173143243

@Test
public void test_sort2() throws Exception {
    List<User> list = userMapper.findByGender(Gender.MALE,
                                              Sort.by(Sort.Direction.ASC, "age"));
    list.forEach(System.out::println);
}

⽣成的sql语句为:

Hibernate: 
    select
        user0_.id as id1_2_,
        user0_.age as age2_2_,
        user0_.gender as gender3_2_,
        user0_.name as name4_2_ 
    from
        t_user user0_ 
    where
        user0_.gender=? 
    order by
        user0_.age asc

image-20220108173322754

@Test
public void test_sort3() throws Exception {
    List<User> list = userMapper.findFirst25ByGender(Gender.MALE,
                                                     Sort.by(Sort.Direction.ASC, "age"));
    list.forEach(System.out::println);
}

⽣成的sql语句为:

Hibernate: 
    select
        user0_.id as id1_2_,
        user0_.age as age2_2_,
        user0_.gender as gender3_2_,
        user0_.name as name4_2_ 
    from
        t_user user0_ 
    where
        user0_.gender=? 
    order by
        user0_.age asc limit ?

image-20220108173529184

Sort中的by⽅法的声明为:可以接受多个排序的参数,

 public static Sort by(Direction direction, String... properties)

5、分页

Spring Data JPA中分⻚功能和排序的使⽤⽅式类似,只需要在查询⽅法的参数列表中添加⼀ 个 Pageable 类型的参数即可。

⽗接⼝中已经有定好的⽅法的参数含有 Pageable ,例如 PagingAndSortingRepository 中的 Page <T> findAll(Pageable pageable);

也可以⾃⼰在接⼝中⾃定义⽅法的参数中添加 Pageable 参数:

package com.sxau.demojpa.mapper;

import com.sxau.demojpa.pojo.Gender;
import com.sxau.demojpa.pojo.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.dao
 * @ClassName: UserMapper
 * @Author: 张晟睿
 * @Date: 2022/1/7 16:05
 * @Version: 1.0
 */
public interface UserMapper extends JpaRepository<User, Long>{
 
 Page<User> findByGender(Gender gender,Pageable pageable);
 
 Page<User> findFirst25ByGender(Gender gender, Pageable pageable);
 
 List<User> findTop25ByGender(Gender gender, Pageable pageable);
 
}

⽅法的返回类型是可以是Page类型的,也可以是List集合类型的,只是Page类型的返回值可以提 供更多的分⻚相关信息的获取。

@Test
public void test_pageable1() throws Exception {
    int page = 0;//当前⻚数--第⼀⻚从0开始计算
    int size = 2;//每⻚指定多少个元素
    Pageable pageable = PageRequest.of(page, size);
    Page<User> pageObj = userMapper.findAll(pageable);
    System.out.println("总⻚数:\t\t" + pageObj.getTotalPages());
    System.out.println("总数据量:\t\t" + pageObj.getTotalElements());
    System.out.println("当前的⻚码:\t\t" + pageObj.getNumber());
    System.out.println("每⻚条数:\t\t" + pageObj.getSize());
    System.out.println("当前⻚实际条数:\t\t" + pageObj.getNumberOfElements());
    System.out.println("当前⻚是否有数据:\t\t" + pageObj.hasContent());
    System.out.println("当前⻚内容:\t\t" + pageObj.getContent());
    System.out.println("分⻚查询的排序规则为:\t\t" + pageObj.getSort());
    System.out.println("当前是否为第⼀⻚:\t\t" + pageObj.isFirst());
    System.out.println("当前是否为最后⼀⻚:\t\t" + pageObj.isLast());
    System.out.println("当前是否有上⼀⻚:\t\t" + pageObj.hasPrevious());
    System.out.println("当前是否有下⼀⻚:\t\t" + pageObj.hasNext());
    System.out.println("返回上⼀⻚Pageable对象为\t\t"+pageObj.previousPageable());
    System.out.println("返回下⼀⻚Pageable对象为\t\t"+pageObj.nextPageable());
}

⽣成的sql语句为:

Hibernate: 
    select
        user0_.id as id1_2_,
        user0_.age as age2_2_,
        user0_.gender as gender3_2_,
        user0_.name as name4_2_ 
    from
        t_user user0_ limit ?
Hibernate: 
    select
        count(user0_.id) as col_0_0_ 
    from
        t_user user0_

image-20220108174540948

如果返回值类型使⽤List⽽不是Page类型的 ,那么就⽆法直接获取到这些分⻚相关的数据信息。

6、注解

Spring Data JPA中可以在接⼝中⾃定义⽅法上⾯使⽤ @Query 注解,在该注解中可以使⽤JPQL或SQL来 指定此⽅法被调⽤的时候需要执⾏的sql语⾔是什么。

JPQL(JavaPersistence Query Language)是⼀种⾯向对象的查询语⾔,它在ORM框架中最终会 翻译成为sql进⾏执⾏。在hibernate框架中,这种语句叫做HQL(Hibernate Query Language)。

JPQL是⾯向对象的查询语⾔,在查询语句中,出现的不是表的名字、字段的名字,⽽是类的名字和类中 属性的名字,因为在ORM框架中,类和表,属性和字段都做好了映射关系,所以JPQL最后是可以根据映 射关系转换sql语句的。

JPQL的特点:

  • 语句中不能出现表名,列名,只能出现java的类名,属性名,并且区分⼤⼩写
  • 语句中出现的关键字和sql语句中的是⼀样的意思,不区分⼤⼩写
  • 语句中不能写select * ⽽是要写select 类的别名,或者写select 具体的属性名
@Query("select u from User u where u.name = ?1")
User findByUserName(String name);

该语句中的 ?1 代表第⼀个参数,?2 代表第⼆个参数

也可以配合 @Param 注解使⽤命名参数(Named Parameters)的⽅式:

@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
User findByLastnameOrFirstname(@Param("lastname") String lastname, @Param("firstname") String firstname);

@Query 注解除了⽀持JPQL之外,还可以⽀持原⽣sql语句的编写:

@Query(value="select * from t_user where name = ?1",nativeQuery=true)
User findByUserNameByNative(String name);

⽣成的sql语句为:

Hibernate: 
    select
        * 
    from
        t_user 
    where
        name = ?

结果为:

image-20220108175234809

@Query 注解还可以⽀持更新和删除的语句,但是需要结合 @Modifying 注解,以及事务管理的注解 @Transactional

@Transactional注解在spring的事务管理中,已经对其进⾏了学习和使⽤了。

@Transactional
@Modifying
@Query("update User u set u.name = ?1 where u.id = ?2")
int updateNameByUserId(String name, Long id);
@Transactional(timeout = 10)
@Modifying
@Query("delete from User u where u.id = ?1")
void deleteByUserId(Long id);

7、关联

Spring Data JPA中除了单表操作外,还可以⽀持多表级联操作,但是这需要表和表之间有关联关系,同 时还需要在实体类中把这种关联关系给配置映射出来(Object-Relationl Mapping)。

1对1关系: 假设⼀个学⽣对应⼀个地址,⼀个地址也对应⼀个学⽣,那么学⽣和地址之间就是1 对1关系。

注意,1对1关系中,数据库中表的外键列在哪⼀⽅都可以。

实体类Address

package com.sxau.demojpa.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.bean
 * @ClassName: Address
 * @Author: 张晟睿
 * @Date: 2022/1/8 11:06
 * @Version: 1.0
 */
@Entity
@Table(name = "t_address")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    private String city;
}

实体类Student

package com.sxau.demojpa.pojo;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.bean
 * @ClassName: Student
 * @Author: 张晟睿
 * @Date: 2022/1/8 11:09
 * @Version: 1.0
 */
@Entity
@Table(name = "t_student")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {

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

    private String name;
/**
 * @OneToOne 表示当前studnet实体类和Address实现类之间是1对1关系
 * cascade=CascadeType.ALL 表示student和address之间的级联类型为ALL
 *
 * @JoinColumn 该注解专⻔⽤来指定外键列的名字
 * 在这⾥表示:将来student表中将会有⼀个外键列为 address_id,
 * 该外键列默认引⽤Address表中的主键列(id)的值
 */
    @OneToOne(cascade=CascadeType.ALL)
    @JoinColumn(name = "address_id")
    private Address address;
}

StudentMapper文件

package com.sxau.demojpa.mapper;

import com.sxau.demojpa.pojo.Student;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.dao
 * @ClassName: StudentMapper
 * @Author: 张晟睿
 * @Date: 2022/1/8 11:23
 * @Version: 1.0
 */
public interface StudentMapper extends JpaRepository<Student, Long> {
}

测试类

@Autowired
private StudentDao studentDao;
@Test
public void test_one2one() throws Exception {
}

运⾏测试⽅法后,可以看到建表语句,根据我们配置的映射信息,之间建表

# 如果需要,可以进⾏⾃动建表
# create可以每次启动测试先进⾏删除表,然后再建表
# update可以每次启动测试时先检查数据库中是否有表,没对应的表则会⾃动建表,有表单话就不再创建了
spring.jpa.hibernate.ddl-auto=update

控制台中输出的建表语句:

Hibernate: 
    
    create table t_address (
       id bigint not null auto_increment,
        city varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table t_student (
       id bigint not null auto_increment,
        name varchar(255),
        address_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table t_student 
       add constraint FK3n8ay7rgq1krcs8bv8nd3k6pq 
       foreign key (address_id) 
       references t_address (id)

在此基础上可以进⾏级联操作测试:

级联保存

@Autowired
private StudentDao studentDao;
@Test
public void test_one2one() throws Exception {
    Student student = new Student();
        Address address = new Address();
        student.setName("zsr");
        address.setCity("山西");
        //学⽣对象和地址对象在内存中建⽴关系,最后会映射成数据库中俩个表中数据的关系
        student.setAddress(address);
        studentMapper.save(student);
}

⽣成的sql语句为:

Hibernate: 
    insert 
    into
        t_address
        (city) 
    values
        (?)
Hibernate: 
    insert 
    into
        t_student
        (address_id, name) 
    values
        (?, ?)

级联查询

    @Test
    public void test_one2one_select() throws Exception {
        Student stu = studentMapper.findById(1L).orElse(null);
        System.out.println(stu);
        System.out.println(stu.getAddress());
    }

⽣成的sql语句:

Hibernate: 
    select
        student0_.id as id1_1_0_,
        student0_.address_id as address_3_1_0_,
        student0_.name as name2_1_0_,
        address1_.id as id1_0_1_,
        address1_.city as city2_0_1_ 
    from
        t_student student0_ 
    left outer join
        t_address address1_ 
            on student0_.address_id=address1_.id 
    where
        student0_.id=?

运⾏结果: 说明不仅查询到了id为1的学⽣,⽽且还把学⽣关联地址也级联查询出来了。

image-20220108180620112

级联删除

    @Test
    public void test_one2one_delete() throws Exception {
        Student stu = studentMapper.findById(1L).orElse(null);
        studentMapper.delete(stu);
    }

⽣成的sql语句:

Hibernate: 
    select
        student0_.id as id1_1_0_,
        student0_.address_id as address_3_1_0_,
        student0_.name as name2_1_0_,
        address1_.id as id1_0_1_,
        address1_.city as city2_0_1_ 
    from
        t_student student0_ 
    left outer join
        t_address address1_ 
            on student0_.address_id=address1_.id 
    where
        student0_.id=?
Hibernate: 
    select
        student0_.id as id1_1_0_,
        student0_.address_id as address_3_1_0_,
        student0_.name as name2_1_0_,
        address1_.id as id1_0_1_,
        address1_.city as city2_0_1_ 
    from
        t_student student0_ 
    left outer join
        t_address address1_ 
            on student0_.address_id=address1_.id 
    where
        student0_.id=?
Hibernate: 
    delete 
    from
        t_student 
    where
        id=?
Hibernate: 
    delete 
    from
        t_address 
    where
        id=?

级联删除的时候,也需要查询⼀次数据,保证数据的存在且有对应关系的。

1对N关系

我们举一个例子生动形象讲一下,假设⼀个⽼师有多辆⻋,⼀辆⻋只属于⼀个⽼师,那么⽼师和汽⻋的关系就是1对N。

注意,1对N关系中,数据库表中的外键列需要设置在N的⼀⽅,否则会有数据冗余。

实体类Teacher

package com.sxau.demojpa.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.pojo
 * @ClassName: Teacher
 * @Author: 张晟睿
 * @Date: 2022/1/14 13:09
 * @Version: 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "t_teacher")
public class Teacher {

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

    private String name;

    private Double salary;

    /**
     * @OneToMany 表示我(teacher)和对⽅(car)之间是⼀对多关系
     *
     * mappedBy="teacher"
     * 表示我们之间⼀多对关系的外键列
     * 是由对⽅(car)的teacher属性类映射维护的
     *
     */
    @OneToMany(mappedBy="teacher",cascade=CascadeType.ALL,fetch =
            FetchType.EAGER)
    private List<Car> cars = new ArrayList<>();

}

实体类Car

package com.sxau.demojpa.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.pojo
 * @ClassName: Car
 * @Author: 张晟睿
 * @Date: 2022/1/14 13:10
 * @Version: 1.0
 */
@Entity
@Table(name = "t_car")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Car {

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

    private String type;

    private Double price;

    /**
     * @ManyToOne 表示我(car)和对⽅(teacher)之间的关系是多对⼀
     * @JoinColumn 表示当前属性为外键列属性,并且指定外键列的名字
     */
    @ManyToOne
    @JoinColumn(name="teacher_id")
    private Teacher teacher;
}

TeacherMapper.java

package com.sxau.demojpa.mapper;

import com.sxau.demojpa.pojo.Student;
import com.sxau.demojpa.pojo.Teacher;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.mapper
 * @ClassName: TeacherMapper
 * @Author: 张晟睿
 * @Date: 2022/1/14 13:13
 * @Version: 1.0
 */
public interface TeacherMapper extends JpaRepository<Teacher, Long> {
}

CarMapper.java

package com.sxau.demojpa.mapper;

import com.sxau.demojpa.pojo.Car;
import com.sxau.demojpa.pojo.Student;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.mapper
 * @ClassName: CarMapper
 * @Author: 张晟睿
 * @Date: 2022/1/14 13:12
 * @Version: 1.0
 */
public interface CarMapper extends JpaRepository<Car, Long> {
}

测试类

级联保存

控制台中输出的建表语句为:

Hibernate: 
    
    create table t_car (
       id bigint not null auto_increment,
        price double precision,
        type varchar(255),
        teacher_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table t_teacher (
       id bigint not null auto_increment,
        name varchar(255),
        salary double precision,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table t_car 
       add constraint FKsi8is87l0wb3a31t0hs25k2p3 
       foreign key (teacher_id) 
       references t_teacher (id)

⽣成的sql语句为:

Hibernate: 
    insert 
    into
        t_teacher
        (name, salary) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        t_car
        (price, teacher_id, type) 
    values
        (?, ?, ?)
Hibernate: 
    insert 
    into
        t_car
        (price, teacher_id, type) 
    values
        (?, ?, ?)
级联查询

⽣成的sql语句:

Hibernate: 
    select
        teacher0_.id as id1_3_0_,
        teacher0_.name as name2_3_0_,
        teacher0_.salary as salary3_3_0_,
        cars1_.teacher_id as teacher_4_1_1_,
        cars1_.id as id1_1_1_,
        cars1_.id as id1_1_2_,
        cars1_.price as price2_1_2_,
        cars1_.teacher_id as teacher_4_1_2_,
        cars1_.type as type3_1_2_ 
    from
        t_teacher teacher0_ 
    left outer join
        t_car cars1_ 
            on teacher0_.id=cars1_.teacher_id 
    where
        teacher0_.id=?

运行测试实例之后,发展爆红了,哈哈哈,这在我的意料之中,我们先明白一下,因为在JPA自动化的查询过程中,涉及到联表会循环往复的做查询操作,此时我们需要声明一下来结束循环的查询。导致我们的栈内存溢出。

将我们实体类Teacher和实体类Car中的@Data改为

@Setter
@Getter
@EqualsAndHashCode// 自动生成get、set、toString、equals方法

我们发展问题就可以解决了

注意,@OneToMany(mappedBy="teacher",cascade=CascadeType.ALL,fetch = FetchType.EAGER) 该配置中的 fetch = FetchType.EAGER ,表示当前关联关系为实时加载。

EAGER 表示热切的;渴望的;渴求的,在这⾥表示查询Teacher数据时,会⽴刻发出sql查询出对 应的Car

注意,@OneToMany(mappedBy="teacher",cascade=CascadeType.ALL,fetch = FetchType.LAZY) 该配置中的 fetch = FetchType.LAZY , 表示当前关联关系为懒加载。

LAZY 表示懒散的;懒惰的,延迟的,在这⾥表示查询Teacher数据时,并不会⽴刻发出sql查询出 对应的Car,⽽是要等到真正使⽤到 private List cars 这个属性的时候,才会发出sql语 句查询数据。

注意,当使⽤延迟加载的时候,我们默认获取到的是⼀个代理对象,在真正使⽤这个对象的时候, 才会发出sql查询数据库中的数据。可以使⽤ getClass ⽅法来看到这个代理对象真正的运⾏时类 型是什么。

修改 fetch = FetchType.LAZY , 然后再来执⾏对应的测试⽅法,

实体类Teacher

@OneToMany(mappedBy="teacher",cascade=CascadeType.ALL,fetch =
FetchType.LAZY)
private List<Car> cars = new ArrayList<>();

测试类:

@Test
public void test_one2many_select() throws Exception{
    Teacher t = teacherMapper.findById(1L).orElse(null);
    System.out.println(t);
    System.out.println(t.getCars());
}

执⾏结果又报错为:(不要着急小伙伴们,还是在意料之中)

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.sxau.demojpa.pojo.Teacher.cars, could not initialize proxy - no Session

首先我们先分析一下报错的结果,这⾥的关键在于:could not initialize proxy - no Session

原因:由于是设置了懒加载,那么查询Teacher时所关联出的Car对象是⼀个代理对象(proxy),⽽这 个代理对象在我们使⽤的时候(上⾯代码的第6⾏,也就说注释掉第6⾏后代码是不报错的),需要发出 sql语句,去查询真正的数据,但是在JPA中执⾏sql时候,⼀定需要当前有session的⽀持,但是这时候, session已经关闭了,默认情况下当前我们执⾏完JPA的操作(findById⽅法)时候就关闭了。

在这⾥其实只要在测试⽅法上加上 @Transactional 注解,就可以保证该测试⽅法在执⾏完之前都是有 事务管理的,这时候session就不会执⾏完 findById ⽽⾃动关闭了。

或者这个实体类里面有很多关联关系,会无限查询。解决的方法是:实体类中一对多的引用属性上加上注解@JsonIgnore,表示查询的时候不查询这个属性的数据,在实体类中多对一-的引用属性上加上注解@JsonIgnoreProperties,表示查询的时候不查询这个引用属性的数据。

⽣成的sql语句和执⾏结果为:

Hibernate: 
    select
        teacher0_.id as id1_3_0_,
        teacher0_.name as name2_3_0_,
        teacher0_.salary as salary3_3_0_ 
    from
        t_teacher teacher0_ 
    where
        teacher0_.id=?
com.sxau.demojpa.pojo.Teacher@12d8157e
Hibernate: 
    select
        cars0_.teacher_id as teacher_4_1_0_,
        cars0_.id as id1_1_0_,
        cars0_.id as id1_1_1_,
        cars0_.price as price2_1_1_,
        cars0_.teacher_id as teacher_4_1_1_,
        cars0_.type as type3_1_1_ 
    from
        t_car cars0_ 
    where
        cars0_.teacher_id=?
[com.sxau.demojpa.pojo.Car@e5ca7496, com.sxau.demojpa.pojo.Car@3fb8aaaa]

从结果中,可以看出来,开始先只是发sql语句查询到了Teacher对象,等后⾯真正⽤Car的时候, 才发了另⼀条sql语句去对Car进⾏了查询。最后也查询到Teacher所关联到的俩个Car对象了。

级联删除
@Test
public void test_one2many_delete() throws Exception{
    Teacher t = teacherMapper.findById(1L).orElse(null);
    teacherMapper.delete(t);
}

⽣成的sql语句:

Hibernate: 
    select
        teacher0_.id as id1_3_0_,
        teacher0_.name as name2_3_0_,
        teacher0_.salary as salary3_3_0_ 
    from
        t_teacher teacher0_ 
    where
        teacher0_.id=?
Hibernate: 
    select
        teacher0_.id as id1_3_0_,
        teacher0_.name as name2_3_0_,
        teacher0_.salary as salary3_3_0_ 
    from
        t_teacher teacher0_ 
    where
        teacher0_.id=?
Hibernate: 
    select
        cars0_.teacher_id as teacher_4_1_0_,
        cars0_.id as id1_1_0_,
        cars0_.id as id1_1_1_,
        cars0_.price as price2_1_1_,
        cars0_.teacher_id as teacher_4_1_1_,
        cars0_.type as type3_1_1_ 
    from
        t_car cars0_ 
    where
        cars0_.teacher_id=?
Hibernate: 
    delete 
    from
        t_car 
    where
        id=?
Hibernate: 
    delete 
    from
        t_car 
    where
        id=?
Hibernate: 
    delete 
    from
        t_teacher 
    where
        id=?

从结果中可以看出,在删除⽼师的数据的同时,也级联的吧⽼师关联的汽⻋数据也删除了。

N对N关系

同样的,我们举一个例子生动形象讲一下,假设⼀个玩家可以玩啊多个游戏,⼀个游戏也可以有多个玩家,那么玩家和游戏的关系就是N对N。

注意,1对N关系中,数据库中需要有第三张桥表来关联另外俩张表。

实体类Player

package com.sxau.demojpa.pojo;

import lombok.*;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.pojo
 * @ClassName: Player
 * @Author: 张晟睿
 * @Date: 2022/1/14 14:17
 * @Version: 1.0
 */
@Entity
@Table(name = "t_player")
@Setter
@Getter
@EqualsAndHashCode// 自动生成get、set、toString、equals方法
@AllArgsConstructor
@NoArgsConstructor
public class Player {

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

    private String name;

    /**
     * 多对多,⼀般不设置任何级联操作。(保存、更新、删除)
     * 级联查询时候默认⾏为,不设置也会有的。
     *
     * @ManyToMany 表示当前类(Player)和对⽅(Game)之间是多对多关系
     * @JoinTable 表示多对多关系的设计中,第三张桥表的相关信息
     * name="player_game" 表示桥表的名字为player_game
     * joinColumns=@JoinColumn(name = "player_id")
     * 表示桥表中的player_id为外键类,并引⽤⾃我(Play)的主键值
     * inverseJoinColumns=@JoinColumn(name = "game_id")
     * inverse单词 在这⾥表示对⽅的意思。
     * 表示桥表中的game_id为外键列,并引⽤⾃对⽅(Game)的主键值
     */
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "player_game",
            joinColumns = @JoinColumn(name = "player_id"),
            inverseJoinColumns = @JoinColumn(name = "game_id")
    )
    private List<Game> gameList = new ArrayList<>();
}

实体类Game

package com.sxau.demojpa.pojo;

import lombok.*;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.pojo
 * @ClassName: Game
 * @Author: 张晟睿
 * @Date: 2022/1/14 14:18
 * @Version: 1.0
 */
@Entity
@Table(name = "t_game")
@Setter
@Getter
@EqualsAndHashCode// 自动生成get、set、toString、equals方法
@AllArgsConstructor
@NoArgsConstructor
public class Game {

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

    private String name;

    /**
     * mappedBy属性,在双向关联中,必定会出现
     * <p>
     * mappedBy表示 外键的配置/维护 由对⽅来处理
     * 如果不配置mappedBy,则默认表示 外键的配置/维护 由我⽅来处理
     * <p>
     * mappedBy="gameList" 表示 我⽅(Game)和对⽅(Player)之间关系的维护
     * 是由对⽅(Player)的属性(gameList)来进⾏配置/维护的。
     * 其实是由对⽅(Player)中的属性(gameList)上的注解进⾏配置/维护的
     */
    @ManyToMany(mappedBy = "gameList")
    private List<Player> playerList = new ArrayList<>();
}

PlayerMapper.java

package com.sxau.demojpa.mapper;

import com.sxau.demojpa.pojo.Player;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.mapper
 * @ClassName: PlayMapper
 * @Author: 张晟睿
 * @Date: 2022/1/14 14:22
 * @Version: 1.0
 */
public interface PlayerMapper extends JpaRepository<Player, Long> {
}

GameMapper.java

package com.sxau.demojpa.mapper;

import com.sxau.demojpa.pojo.Game;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @ProjectName: demojpa
 * @Package: com.sxau.demojpa.mapper
 * @ClassName: GameMapper
 * @Author: 张晟睿
 * @Date: 2022/1/14 14:23
 * @Version: 1.0
 */
public interface GameMapper extends JpaRepository<Game, Long> {
}

实体类:

注意,当前没有配置级联保存/删除

	@Autowired
    private PlayerMapper playerMapper;

    @Autowired
    private GameMapper gameMapper;

@Test
    public void test_many2many_insert() {
        Player p1 = new Player();
        p1.setName("tom1");
        Player p2 = new Player();
        p2.setName("tom2");
        Game g1 = new Game();
        g1.setName("疯狂篮球");
        Game g2 = new Game();
        g2.setName("疯狂⾜球");
        Game g3 = new Game();
        g3.setName("疯狂排球");
        List<Game> gameList1 = new ArrayList<>();
        gameList1.add(g1);
        gameList1.add(g2);
        gameList1.add(g3);
        List<Game> gameList2 = new ArrayList<>();
        gameList2.add(g1);
        gameList2.add(g3);
        //建⽴对象之间的关联关系
        p1.setGameList(gameList1);
        p2.setGameList(gameList2);
        //注意,这样要记得先保存game对象的数据
        //然后再保存player对象的数据,因为是让player关联的game
        gameMapper.save(g1);
        gameMapper.save(g2);
        gameMapper.save(g3);
        playerMapper.save(p1);
        playerMapper.save(p2);
    }

在这个操作中,要注意先保存game,然后再保存player,因为是让player关联的game

控制台中输出的建表语句为:

Hibernate: 
    
    create table player_game (
       player_id bigint not null,
        game_id bigint not null
    ) engine=InnoDB
Hibernate: 
    
    create table t_game (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table t_player (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table player_game 
       add constraint FKijjwgqv8y2mdiwxhk2i56e4jy 
       foreign key (game_id) 
       references t_game (id)
Hibernate: 
    
    alter table player_game 
       add constraint FKpc16kqwtroqa8jcwu91f5204p 
       foreign key (player_id) 
       references t_player (id)

⽣成的sql语句为:

Hibernate: 
    insert 
    into
        t_game
        (name) 
    values
        (?)
Hibernate: 
    insert 
    into
        t_game
        (name) 
    values
        (?)
Hibernate: 
    insert 
    into
        t_game
        (name) 
    values
        (?)
Hibernate: 
    insert 
    into
        t_player
        (name) 
    values
        (?)
Hibernate: 
    insert 
    into
        player_game
        (player_id, game_id) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        player_game
        (player_id, game_id) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        player_game
        (player_id, game_id) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        t_player
        (name) 
    values
        (?)
Hibernate: 
    insert 
    into
        player_game
        (player_id, game_id) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        player_game
        (player_id, game_id) 
    values
        (?, ?)

可以根据代码的编写执⾏属性,观察这些sql语句对应的含义。

最后数据库表中的执⾏结果:

image-20220114144556192