Spring Data JPA
JPA(Java Persistence API), 中文的字面意思就是Java的持久层 API , JPA就是定义了一系列标准,让实体类和数据库中的表建立一个对应的关系,当我们在使用 java 操作实体类的时候能达到操作数据库中表的效果(不用写SQL,就可以达到效果),JPA 的实现思想即是 ORM(Object Relational Mapping),对象关系映射,用于在关系型数据库和业务实体对象之间作一个映射。
JPA 并不是一个框架,是一类框架的总称,持久层框架 Hibernate 是 JPA 的一个具体实现,本文要谈的 spring-data-jpa 又是在 Hibernate 的基础之上的封装实现。
当我们项目中使用 spring-data-jpa 的时候,你会发现并没有 SQL 语句,其实框架的底层已经帮我们实现了,我们只需要遵守规范使用就可以了,下面会详细谈到 spring-data-jpa 的各种规范细则。
spring-data-jpa常用配置
下面把SpringBoot 项目关于 JPA 的常用配置 application.properties 配置如下:
#项目端口的常用配置
server.port=8081
# 数据库连接的配置
spring.datasource.url=jdbc:mysql:///jpa?useSSL=false
spring.datasource.username=root
spring.datasource.password=zempty123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接池的配置,hikari 连接池的配置
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.auto-commit=true
#通过 jpa 自动生成数据库中的表
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
下面重点分析一下 jpa 中的三个配置:
-
spring.jpa.hibernate.ddl-auto=update
该配置较常用,当服务首次启动会在数据库中生成相应表,后续启动服务时如果实体类有增加属性会在数据中添加相应字段,原来数据仍在,其他配置值:create
:该值慎用,每次重启项目的时候都会删除表结构,重新生成,原来数据会丢失不见。create-drop
:慎用,当项目关闭,数据库中的表会被删掉。validate
: 验证数据库和实体类的属性是否匹配,不匹配将会报错。
-
spring.jpa.show-sql=true
该配置当在执行数据库操作的时候会在控制台打印 SQL 语句,方便检查排错。 -
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
数据库的方言配置。
类映射到数据库表的常用注解分析
spring-data-jpa提供了很多注解,下面我们把日常常用注解总结如下:
-
@Entity
类注解。用来注解该类是一个实体类,用于和数据库中的表建立关联关系。首次启动项目的时候,默认会在数据中生成一个同实体类相同名字的表(table),也可以通过注解中的 name 属性来修改表(table)名称, 如@Entity(name="stu")
, 这样数据库中表的名称则是 stu 。 -
@Table
类注解。该注解可以用来修改表的名字。该注解完全可以忽略掉不用,@Entity 注解已具备该注解的功能。 -
@Id
类的属性注解。该注解表明该属性字段是一个主键。该属性必须具备,不可缺少。 -
@GeneratedValue
类的属性注解。该注解通常和 @Id 主键注解一起使用,用来定义主键的呈现形式,该注解通常有多种使用策略:-
@GeneratedValue(strategy= GenerationType.IDENTITY)
该注解由数据库自动生成,主键自增型,在 MySQL 数据库中使用最频繁,oracle 不支持。 -
@GeneratedValue(strategy= GenerationType.AUTO)
主键由程序控制,默认的主键生成策略,oracle 默认是序列化的方式,MySQL 默认是主键自增的方式。 -
@GeneratedValue(strategy= GenerationType.SEQUENCE)
根据底层数据库的序列来生成主键,条件是数据库支持序列,Oracle支持,MySQL不支持。 -
@GeneratedValue(strategy= GenerationType.TABLE)
使用一个特定的数据库表格来保存主键,较少使用。 -
@GeneratedValue(generator = "snowflakeIdGenerator")
使用一个指定的ID生成器来生成主键,如雪花算法生成器:@Id @GenericGenerator(name = "snowflakeIdGenerator", strategy = "com.demo.util.SnowflakeIdGenerator") @GeneratedValue(generator = "snowflakeIdGenerator") private String id;
点击查看代码
public class SnowflakeIdGenerator implements IdentifierGenerator, Configurable { @Override public void configure(Type type, Properties properties, ServiceRegistry serviceRegistry) throws MappingException { } @Override public Serializable generate(SharedSessionContractImplementor sharedSessionContractImplementor, Object o) throws HibernateException { long snowflakeId = SpringUtils.getBean(SnowflakeIdWorker.class).nextValue(); // long转为String return String.valueOf(snowflakeId); } }
-
-
@Column
类的属性注解。该注解可以定义一个字段映射到数据库属性的具体特征。比如字段长度,映射到数据库时属性的具体名字等。 -
@Transient
类的属性注解。该注解标注的字段不会被应射到数据库当中。
以上使用的注解是定义一个实体类的常用注解,通过上述的注解我们就可以通过实体类生成数据库中的表,实体类和表建立一个对应的关系,下面贴出一个实体类的定义 demo :
点击查看代码
package com.zempty.springbootjpa.entity;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@Setter
@Getter
@Accessors(chain = true)
@Entity(name = "stu")
//@Table(name = "stu")
public class Student {
@Id
@GeneratedValue(strategy= GenerationType.TABLE)
private long id;
@Column(length = 100)
private String name;
@Transient
private String test;
private int age;
private LocalTime onDuty;
private LocalDate onPosition;
private LocalDateTime birthdayTime;
}
该实体类中的 @Setter
,@Getter
, @Accessors(chain =true)
均是 lombok 的注解。
使用上述实体类的注解,当运行项目的时候就会在数据库中生成一个表名是 stu 的表。
使用spring-data-jpa关键字进行增删改查
在使用 spring-data-jpa 进行数据库的增删改查的时候,基本上我们无需写 SQL 语句的,但是我们必须要遵守它的规则,下面就来聊一聊。
如何定义 DAO 层
spring-data-jpa 的数据层,我们只需要定义一个接口继承 JpaRepository 就好,
JpaRepository 接口中定义了丰富的查询方法供我们使用,足以供我们进行增删改查的工作,参考代码如下:
定义一个 Student 的 dao 层,这样我们的增删改查就已经有了
public interface StudentRepository extends JpaRepository<Student,Integer> {
}
在 SpringBoot 项目中在 DAO 层我们不需要写 @Repository
注解 ,我们在使用的时候直接注入使用就好,这里需要说明一点,我们在更新数据的时候,可以查询获取后更改属性,再调用 save()
方法保存。
使用关键字自定义查询
我们可以使用 jpa 提供的 find
和 get
关键字完成常规的查询操作,使用 delete
关键字完成删除,使用 count
关键字完成统计等。
public interface StudentRepository extends JpaRepository<Student,Integer> {
// 查询数据库中指定名字的学生
List<Student> findByName(String name);
// 根据名字和年龄查询学生
List<Student> getByNameAndAge(String name, Integer age);
//删除指定名字的学生
Long deleteByName(String name);
// 统计指定名字学生的数量
Long countByName(String name);
}
-
find
:点击查看代码
@GetMapping("/find/{name}") public List<Student> findStudentsByName(@PathVariable("name") String name) { return studentRepository.findByName(name); }
-
get
:点击查看代码
@GetMapping("/get/{name}/{age}") public List<Student> getStudentByNameAndAge(@PathVariable("name") String name,@PathVariable("age") Integer age) { return studentRepository.getByNameAndAge(name, age); }
-
delete
:点击查看代码
@DeleteMapping("/delete/{name}") //删除的时候一定要添加事务注解 @Transactional public Long deleteStudentByName(@PathVariable("name") String name) { return studentRepository.deleteByName(name); }
-
count
:点击查看代码
@GetMapping("/count/{name}") public Long countStudentByName(@PathVariable("name") String name) { return studentRepository.countByName(name); }
jpa 使用 SQL 增删改查
SQL的两种呈现形式
- JPQL 形式的 SQL 语句,from 后面是以类名呈现的。
- 原生的 SQL 语句,需要使用 nativeQuery = true 指定使用原生 SQL。
public interface ClassRoomRepository extends JpaRepository<ClassRoom,Integer> {
//使用的 JPQL 的 SQL 形式 from 后面是类名
// ?1 代表是的是方法中的第一个参数
@Query("select s from ClassRoom s where s.name =?1")
List<ClassRoom> findClassRoom1(String name);
//这是使用正常的 SQL 语句去查询
// :name 是通过 @Param 注解去确定的
@Query(value = "select * from class_room c where c.name =:name", nativeQuery = true)
List<ClassRoom> findClassRoom2(@Param("name")String name);
}
SQL 中参数传递两种形式:
- 使用问号
?
,紧跟数字序列,数字序列从1 开始,如 ?1 接收第一个方法参数的值。 - 使用冒号
:
,紧跟参数名,参数名是通过@Param
注解来确定。
使用 Sort 对数据进行排序:
spring-data-jpa 提供了一个 Sort 类来进行分类排序,下面通过代码来说明 Sort 的使用:
public interface TeacherRepositoty extends JpaRepository<Teacher,Integer> {
// 正常使用,只是多加了一个 sort 参数而已
@Query("select t from Teacher t where t.subject = ?1")
List<Teacher> getTeachers(String subject, Sort sort);
}
上述代码正常的写 SQL 语句,只是多加了一个 Sort 参数而已,如何实例化 Sort 应该是我们关注的重点,现在看测试代码如下:
@GetMapping("/sort/{subject}")
public List<Teacher> getTeachers(@PathVariable("subject") String subject) {
// 第一种方法实例化出 Sort 类,根据年龄进行升序排列
Sort sort1 = Sort.by(Sort.Direction.ASC, "age");
//定义多个字段的排序
Sort sort2 = Sort.by(Sort.Direction.DESC, "id", "age");
// 通过实例化 Sort.Order 来排序多个字段
List<Sort.Order> orders = new ArrayList<>();
Sort.Order order1 = new Sort.Order(Sort.Direction.DESC, "id");
Sort.Order order2 = new Sort.Order(Sort.Direction.DESC, "age");
orders.add(order1);
orders.add(order2);
Sort sort3 = Sort.by(orders);
//可以传不同的 sort1,2,3 去测试效果
return teacherRepositoty.getTeachers(subject, sort1);
}
Sort 是用来用来给字段排序的,可以根据一个字段进行排序,也可以给多个字段设置排序规则,但是个人之见使用Sort 对一个字段排序就好。
Sort 类的实例化可以通过 Sort 的 by 静态方法实例化就好,这里就不一一列举了,参考上述案例就好。
jpa 的分页操作
数据多的时候就需要分页,spring-data-jpa 对分页提供了很好的支持,下面通过一个 demo 来展示如何使用分页:
public interface TeacherRepositoty extends JpaRepository<Teacher,Integer> {
//正常使用,只是多加了一个 Pageable 参数而已
@Query("select t from Teacher t where t.subject = :subject")
Page<Teacher> getPage(@Param("subject") String subject, Pageable pageable);
}
按照正常的查询步骤,多加一个 Pageable 的参数而已,如何获取 Pageable 呢?
Pageable 是一个接口类,它的实现类是 PageRequest ,下面就通过测试代码来研究一下如何来实例化 PageRequest:
@GetMapping("page/{subject}")
public Page<Teacher> getPage(@PathVariable("subject") String subject) {
// 第一种方法实例化 Pageable
Pageable pageable1 = PageRequest.of(1, 2);
//第二种实例化 Pageable
Sort sort = Sort.by(Sort.Direction.ASC, "age");
Pageable pageable2 = PageRequest.of(1, 2, sort);
//第三种实例化 Pageable
Pageable pageable3 = PageRequest.of(1, 2, Sort.Direction.DESC, "age");
//可以传入不同的 Pageable,测试效果
Page page = teacherRepositoty.getPage(subject, pageable3);
System.out.println(page.getTotalElements());
System.out.println(page.getTotalPages());
System.out.println(page.hasNext());
System.out.println(page.hasPrevious());
System.out.println(page.getNumberOfElements());
System.out.println(page.getSize());
return page;
}
PageRequest 一共有三个可以实例化的静态方法:
public static PageRequest of(int page, int size)
public static PageRequest of(int page, int size, Sort sort)
分页的同时还可以针对分页后的结果进行一个排序。public static PageRequest of(int page, int size, Direction direction, String… properties)
直接针对字段进行排序。
jpa 使用 Specification
public interface TeacherRepositoty extends JpaRepository<Teacher,Integer> , JpaSpecificationExecutor {
}
TeacherRepository 除了继承 JpaRepository 以外,还继承了 JpaSpecificationExecutor 接口。
JpaSpecificationExecutor 提供了如下的几个方法供我们使用:
public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> var1);
List<T> findAll(@Nullable Specification<T> var1);
Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
List<T> findAll(@Nullable Specification<T> var1, Sort var2);
long count(@Nullable Specification<T> var1);
}
该接口一共有五个方法,还提供了排序,分页的功能,分析方法的参数我们会发现方法中的参数 Specification 是我们使用的一个门槛,下面来具体分析如何实例化 Specification 。
Specification 是一个函数式接口,里面有一个抽象的方法:
Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);
实现该方法我们需要弄清楚 Predicate
, Root
, CriteriaQuery
和 CriteriaBuilder
四个类的使用规则:
现在有这样的一条 SQL 语句 : select * from teacher where age > 20
Predicate
是用来建立 where 后的查寻条件的相当于上述SQL语句的 age > 20。Root
使用来定位具体的查询字段,比如 root.get("age"), 定位 age 字段。CriteriaBuilder
是用来构建一个字段的范围,相当于 > , =, <,and… 等等。CriteriaQuery
可以用来构建整个 SQL 语句,可以指定SQL 语句中的 select 后的查询字段,也可以拼接 where , groupby 和 having 等复杂语句。
演示案例:
public Page<Teacher> queryTeachers(String name, String telephone, Integer page, Integer size) {
// 分页查询参数
Pageable pageable = PageRequest.of(page, size);
//实例化 Specification 类
Specification specification = ((root, criteriaQuery, criteriaBuilder) -> {
// 查询条件集合
List<Predicate> predicates = new ArrayList<>();
// 逐一构建查询条件
predicates.add(criteriaBuilder.like(root.get("name"), "%" + name + "%"));
predicates.add(criteriaBuilder.equal(root.get("telephone"), telephone));
// 使用 and 连接所有条件
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
});
//使用查询
return teacherRepository.findAll(specification, pageable);
}
使用spring-data-jpa 的 Projection (投影映射)
当我们使用 spring-data-jpa 查询数据的时候,有时候不需要返回所有字段的数据,我们只需要个别字段数据,便可以使用 Projection。
现在的需求是我只需要 Teacher 类对应的表 teacher 中的 name 和 age 的数据,其他数据不需要,步骤如下:
-
定义一个如下的接口:
public interface TeacherProjection { String getName(); Integer getAge(); @Value("#{target.name +' and age is' + target.age}") String getTotal(); }
接口中的方法以 get + 属性名,属性名首字母大写, 例如 getName(), 也可以通过 @Value 注解中使用 target.属性名获取属性,也可以把多个属性值拼接成一个字符串。
-
使用自定义接口
定义好一个接口后,在查询方法中指定返回接口类型的数据即可,参考代码如下:public interface TeacherRepositoty extends JpaRepository<Teacher,Integer>, JpaSpecificationExecutor { // 返回 TeacherProjection 接口类型的数据 @Query("select t from Teacher t ") List<TeacherProjection> getTeacherNameAndAge(); }
测试结果:
@GetMapping("/projection") public List<TeacherProjection> projection() { // 返回指定字段的数据 List<TeacherProjection> projections = teacherRepositoty.getTeacherNameAndAge(); // 打印字段值 projections.forEach(teacherProjection -> { System.out.println(teacherProjection.getAge()); System.out.println(teacherProjection.getName()); System.out.println(teacherProjection.getTotal()); }); return projections; }
运行测试,查看结果我们会发现,我们仅仅只是拿到 name 和 age 的字段值而已。