秒懂SpringBoot之如何使用Spring Data JPA中Specification进行动态查询

本文介绍了JPA(JavaPersistenceAPI)的基本概念,SpringDataJPA的高级抽象以及Specifications在动态查询中的应用。作者详细讲解了如何使用这些工具来操作数据库,包括实体映射、Repository的使用、Specifications的创建和组合,以及Metamodel的运用以提高类型安全。
摘要由CSDN通过智能技术生成

[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007

概述

操作数据库大概是一个web程序最重要的部分了,而Spring Data JPA 正是spring生态中用来解决此问题的利器。今天让我们简单聊聊这个话题。

首先让我们来理清楚一些关键概念:

JPA是什么?

JPA (Java Persistence API) 是 Java 平台上的一种标准化的 ORM(对象-关系映射)规范。JPA 提供了一种面向对象的数据访问模型,允许开发人员将 Java 对象映射到数据库表,从而实现对象关系映射。

JPA是Java平台定义的一种规范,一个抽象层。所有人都可以来实现它,但是我们现在一般时候的都是Hibernate。其实除了Hibernate还有很多种JPA的实现方案,例如EclipseLinkOpenJPABlaze-Persistence 等,感兴趣的小伙伴可以继续探索。

Spring Data JPA 是什么?

Spring Data JPA 是Spring Data 项目的一部分,它提供了对 JPA的更高级别的抽象,旨在简化 JPA 的使用。它利用了 JPA 规范,并提供了一组工具和功能,使得在 Spring 应用程序中使用 JPA 更加容易和便捷。

Spring Data JPA 的主要目标是减少开发人员需要编写的重复代码,同时提供一致的数据访问方式。通过 Spring Data JPA,开发人员可以通过定义接口来声明查询方法,而无需手动编写实现。Spring Data JPA 将根据方法名称自动生成查询,从而简化了数据访问层的开发。

Specifications 是什么?

Specifications是Spring Data JPA的一部分,其是对 JPA中Criteria API 的一种包装。

Specifications 使用场景

当我们需要根据用户输入或者只有在runtime时才能确定的条件来查询数据库时就可以考虑使用。

让我们回想一下如何使用Spring Data JPA来操作数据库呢?如下所示

@Repository
public interface JpaStudentRepository extends JpaRepository<Student, Integer>{
    // 使用JPQL
    @Query("select s from Student s where s.number = ?1")
    Optional<Student> findByNumber(String number);
    //使用方法名称
    Optional<Student> findByName(String name);
}

创建一个接口并继承JpaRepository后,在其内部声明要操作数据库的方法即可。这块又有两种方案,第一种是使用JPQL或者 SQL,第二种就是使用方法名称,这个方法名称不能瞎写,是有一套自己的规则。

我首次接触这玩意儿的时候也比较懵逼,这你妈谁知道咋写啊?不用担心,软件行业都发展到现在这个地步了,你遇到的问题很多人都遇到过了,这不一个特别好用的工具就凌空出世了:Jpa-buddy。这是一个增值付费的IntelliJ IDEA插件,但是其免费功能已经满足我们大部分的日常需求了。

安装插件后,我们只需要在对应的JpaRepository对象一下,然后就会弹出如下的弹窗,然后选择你要使用的方法,点击后会弹出弹窗,选择查询条件后 jpa-buddy就在对应的JpaRepository下帮你生成了查询方法。

在这里插入图片描述

如何使用

那么如何使用Specification呢?

引入依赖

假设你已经完成了数据库相关的依赖和配置

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-jpa</artifactId>
 </dependency>

创建Entity

我们有一张学生表和一张老师表,老师和学生是多对多关系。

@Setter
@Getter
@Entity
@Table(name = "student", schema = "jpa-learn")
public class Student extends AbstractAuditingEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Integer id;

    @Column(name = "stu_name")
    private String name;

    @Column(name = "stu_number")
    private String number;

    @Column(name = "age")
    private Integer age;

  ...

    @ManyToMany(cascade = {CascadeType.ALL})
    @JoinTable(name = "student_teacher_relation",
            joinColumns = @JoinColumn(name = "student_id"),
            inverseJoinColumns = @JoinColumn(name = "teacher_id"))
    private List<Teacher> teachers;

}

创建Repository

创建Repository接口并扩展JpaSpecificationExecutor<T> 接口

@Repository
public interface JpaStudentRepository extends JpaRepository<Student, Integer>, JpaSpecificationExecutor<Student> {

    @Query("select s from Student s where s.number = ?1")
    Optional<Student> findByNumber(String number);
    
    Optional<Student> findByName(String name);
}

当我们extendsJpaSpecificationExecutor<Student> 接口后,就获得了很多以Specification为入参的方法,例如

List<T> findAll(Specification<T> spec);

接下来重头戏来了,我们要如何创建Specification然后传递到这些方法里面去。

创建对应的Specification

Specification是什么呢?你可以简单的理解为一个Specification就是一个查询条件,我们可以组合这些查询条件。

例如Specification1是:查询姓王的学生,Specification2是:年龄18岁的学生。那么Specification1.and(Specification2) 就是查询所有年龄为18岁且姓王的学生。

首先,Specification是一个接口,其只包含一个抽象方法,所以创建一个Specification只需要实现一个方法即可,如下所示:

public interface Specification<T> extends Serializable {
    @Nullable
	Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
	...
}

其中方法中的3个参数的含义如下:

假设有如下SQL:selcte * from student AS s where s.name = 'ss007'

  • root

代表我们的Entity,通过它来获取Entiy中的属性,对应SQL中的s

  • query

代表当前正在创建的查询。对应SQL中的selcte * from student AS s where s.name = 'ss007'

  • criteriaBuilder

代表构建查询条件的builder。对应SQL中的where s.name = 'ss007'

一般情况下我们会在一个工具类里创建很多个Specification,在然后再动态地将其组合在一起完成复杂的动态查询。

@UtilityClass
public class StudentSpecification {
    // 以学生名称查询
    public static Specification<Student> hasNumber(String number) {
        return new Specification<Student>() {
            @Override
            public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
                return criteriaBuilder.equal(root.get(Student_.number), number);
            }
        };
    }
	
	//以学生年龄查询并以年龄排序
    public static Specification<Student> ageBetween(Integer minAge, Integer maxAge) {
        return (root, query, criteriaBuilder) -> {
            Predicate between = criteriaBuilder.between(root.get(Student_.age), minAge, maxAge);
            query.orderBy(criteriaBuilder.desc(root.get(Student_.age)));
            return between;
        };
    }

    //以学生的老师查询
    public static Specification<Student> hasTeacher(String teacher) {
        return (root, query, criteriaBuilder) -> {
            Join<Student, Teacher> stuTeachers = root.join(Student_.teachers);
            return criteriaBuilder.equal(stuTeachers.get(Teacher_.name), teacher);
        };
    }
    
}

上面的代码我们定义了3个Specification,也就是3个查询条件,其含义注释已经说的很清楚了。

使用

我们使用上面创建的Specification来构建一个动态查询的方法,这里顺便实现了分页。

@Slf4j
@Service
public class MyServiceImpl implements MyService {

    @Autowired
    private JpaStudentRepository studentRepository;

    @Override
    public Page<Student> filterStudents(FilterStudentRequest filter, Pageable pageable) {

        Specification<Student> spec = Specification.where(null);

        if (StrUtil.isNotBlank(filter.getName())) {
            spec = spec.and(StudentSpecification.nameLike(filter.getName()));
        }
        
        if (!Objects.isNull(filter.getAgeMin()) && !Objects.isNull(filter.getAgeMax())) {
            spec = spec.and(StudentSpecification.ageBetween(filter.getAgeMin(), filter.getAgeMax()));
        }

        if (StrUtil.isNotBlank(filter.getNumber())) {
            spec = spec.and(StudentSpecification.hasNumber(filter.getNumber()));
        }

        if (StrUtil.isNotBlank(filter.getTeacher())) {
            spec = spec.and(StudentSpecification.hasTeacher(filter.getTeacher()));
        }

        return studentRepository.findAll(spec, pageable);
    }
}

如代码所示,我们可以根据用户的输入条件不断的组合Specification(查询条件),最后通过findAll方法查询返回。

使用Metamodel

细心的同学可能已经发现了在构建Specification时使用了一个Student_的类,这个类哪里来的呢?这个类其实是通过jpamodelgen工具生成的对应Entity的元数据,内如类似下面这样。

@StaticMetamodel(Student.class)
@Generated("org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
public abstract class Student_ extends top.ss007.jpademo.entity.AbstractAuditingEntity_ {
	/**
	 * @see top.ss007.jpademo.entity.Student#number
	 **/
	public static volatile SingularAttribute<Student, String> number;
	...
	public static final String NUMBER = "number";
    ...
}

那为什么要使用Metamodel呢?为了类型安全,如果不使用Metamodel,那么就会再查询硬编码,例如

criteriaBuilder.equal(root.get("number"), number);

一旦number写错了,或者数据表字段重命名了就会会引发运行时错误,当上线后报错了就不好玩了,引入Metamodel后就会将这些问题在编译时暴露出来,保证了类型安全。

那如何使用Metamodel呢?

配置jpamodelgen工具

  • 配置依赖
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
    <version>${jpamodelgen.version}</version>
</dependency>
  • 配置APT
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-compiler-plugin</artifactId>
   <version>${maven.plugin.version}</version>
   <configuration>
       <annotationProcessorPaths>
           <path>
               <groupId>org.hibernate.orm</groupId>
               <artifactId>hibernate-jpamodelgen</artifactId>
               <version>${jpamodelgen.version}</version>
           </path>
       </annotationProcessorPaths>
   </configuration>
</plugin>

编译后就会生产各个Entity对应的Metamodel数据。

总结

没接触Spring Data JPA 的时候觉得MyBatis和MyBatis plus真香,后来工作需要被迫使用了JPA,刚开始觉得变扭,但用熟悉了感觉也不错。人啊,走出自己的舒适区真的很难,所以那些能不断走出自己舒适区的同学真的很厉害,特别是在我们IT行业,这种能力更是弥足珍贵。虽然人善变,但男人有一样特别专一,就是至始至终都喜欢

源码

一如既往,你可以从本文首发获取源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ShuSheng007

亲爱的猿猿,难道你又要白嫖?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值