Springboot整合Springdata-jpa
「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。
关于作者
- 作者介绍
🍓 博客主页:作者主页
🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆、阿里云专家博主、51CTO专家博主
🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨💻
1、简介
Spring Data 项⽬的⽬的是为了简化构建基于 Spring 框架应⽤的数据访问,包括关系型数据库库、⾮关 系型数据库、Map-Reduce 框架、云数据服务等。Spring Data JPA是Spring Data下⾯的⼀个⼦项⽬, 其提供了对JPA的操作⽀持。
spring-data-jpa 2.6.0 Reference Documentation
JPA(Java Persistence API)是当年的Sun官⽅提出的Java持久化规范。它为Java开发⼈员提供了⼀种对象/ 关联映射⼯具来管理Java应⽤中的关系数据。 它的出现主要是为了简化现有的持久化开发⼯作和整合ORM技术,结束现在Hibernate,TopLink,JDO 等ORM框架各⾃为营的局⾯。值得注意的是,JPA是在现有Hibernate,TopLink,JDO等ORM框架的基 础上发展⽽来的。JPA在JAVA EE 5的时候也加⼊了其规范之中。
**注意:**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项⽬
目录结构
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的实现。
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的配置参数 :
Spring Data JPA中采⽤泛型接⼝的形式,预先定义好了⼀些基本的CURD的⽅法。泛型中需要提供操作 的实体类型以及对应的主键类型。
注意:我们⽆需实现这些⽅法,Spring Data JPA框架中已经实现了,我们只需在依赖注⼊后,直 接使⽤即可。
接下来我们看看org.springframework.data.jpa.repository.JpaRepository
//
// 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继承的所有的⽗接⼝:
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();
}
}
我们发现了运行时,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=?
当在测试类中调⽤我们⾃⼰在接⼝中定义的 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 ?
可以输出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
@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
@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 ?
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_
如果返回值类型使⽤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 = ?
结果为:
@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的学⽣,⽽且还把学⽣关联地址也级联查询出来了。
级联删除:
@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语句对应的含义。
最后数据库表中的执⾏结果: