添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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 注解已具备该注解的功能。

    类的属性注解。该注解表明该属性字段是一个主键。该属性必须具备,不可缺少。

    @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生成器来生成主键,如雪花算法生成器:

    @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);
    

    该实体类中的 @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);
    点击查看代码
    
    @GetMapping("/get/{name}/{age}")
    public List<Student> getStudentByNameAndAge(@PathVariable("name") String name,@PathVariable("age") Integer age) {
        return studentRepository.getByNameAndAge(name, age);
    //删除的时候一定要添加事务注解
    @Transactional
    public Long deleteStudentByName(@PathVariable("name") String name) {
        return studentRepository.deleteByName(name);
    点击查看代码
    
    @GetMapping("/count/{name}")
    public Long countStudentByName(@PathVariable("name") String name) {
        return studentRepository.countByName(name);
    
  • 原生的 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 的数据,其他数据不需要,步骤如下:

    定义一个如下的接口:

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

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