[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007
概述
操作数据库大概是一个web程序最重要的部分了,而Spring Data JPA 正是spring生态中用来解决此问题的利器。今天让我们简单聊聊这个话题。
首先让我们来理清楚一些关键概念:
JPA是什么?
JPA (Java Persistence API) 是 Java 平台上的一种标准化的 ORM(对象-关系映射)规范。JPA 提供了一种面向对象的数据访问模型,允许开发人员将 Java 对象映射到数据库表,从而实现对象关系映射。
JPA是Java平台定义的一种规范,一个抽象层。所有人都可以来实现它,但是我们现在一般时候的都是Hibernate
。其实除了Hibernate
还有很多种JPA的实现方案,例如EclipseLink
、OpenJPA
、Blaze-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);
}
当我们extends
了JpaSpecificationExecutor<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行业,这种能力更是弥足珍贵。虽然人善变,但男人有一样特别专一:矢志不渝的喜欢18岁漂亮的小姑娘...
源码
一如既往,你可以从GitHub项目springboot-learn获取源码
文章评论
这篇文章写得深入浅出,让我这个小白也看懂了!