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 提供的 findget 关键字完成常规的查询操作,使用 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, CriteriaQueryCriteriaBuilder 四个类的使用规则:

现在有这样的一条 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 的数据,其他数据不需要,步骤如下:

  1. 定义一个如下的接口:

    public interface TeacherProjection {
    
        String getName();
    
        Integer getAge();
    
        @Value("#{target.name +' and age is' + target.age}")
        String getTotal();
    }
    

    接口中的方法以 get + 属性名,属性名首字母大写, 例如 getName(), 也可以通过 @Value 注解中使用 target.属性名获取属性,也可以把多个属性值拼接成一个字符串。

  2. 使用自定义接口
    定义好一个接口后,在查询方法中指定返回接口类型的数据即可,参考代码如下:

    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 的字段值而已。

详解 spring data jpa ,全方位总结,干货分享

posted @ 2023-01-08 20:24  Fogram  阅读(162)  评论(0编辑  收藏  举报