深入探索Spring Data JPA, 从Repository 到 Specifications 和 Querydsl

数据访问层,所谓的CRUD是后端程序员的必修课程,Spring Data JPA 可以让我们来简化CRUD过程,本文由简入深,从JPA的基本用法,到各种高级用法。

Repository#

Spring Data JPA 可以用来简化data access的实现,借助JPA我们可以快速的实现一些简单的查询,分页,排序不在话下。

Copy
public interface MovieRepository extends JpaRepository<Movie, Long> { List<Movie> findByTitle(String title, Sort sort); Page<Movie> findByYear(Int year, Pageable pageable); }

JPA会根据方法命名,通过JPA 查询生成器自动生成SQL,cool!

Criteria API#

但是,简单并非万能,有时候也需要面对一些复杂的查询,不能享受JPA 查询生成器带来的便利。JPQ 提供了Criteria API

Criteria API 可以通过编程方式动态构建查询,强类型检查可以避免错误。核心原理就是构造一个Predicate

Copy
LocalDate today = new LocalDate(); CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery<Movie> query = builder.createQuery(Movie.class); Root<Movie> root = query.from(Movie.class); Predicate isComedy = builder.equal(root.get(Movie.genre), Genre.Comedy); Predicate isReallyOld = builder.lessThan(root.get(Movie.createdAt), today.minusYears(25)); query.where(builder.and(isComedy, isReallyOld)); em.createQuery(query.select(root)).getResultList();

Predicate 可以很好的满足一些复杂的查询,但是他的问题在于不便于复用,因为你需要先构建CriteriaBuilder, CriteriaQuery, Root. 同时代码可读性也比较一般。

Specifications#

能不能定义可复用的Predicate呢? JPA 提供Specification 接口来解决这个问题。

先来看这个接口定义:

Copy
public interface Specification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb); }

上文不是说需要先构建CriteriaBuilder, CriteriaQuery, Root吗,那么Specification接口就是给你提供这个三个参数,让你自己构建Predicate,想什么来什么。

我们用Specifications来改写代码,先定义Specification

Copy
public MovieSpecifications { public static Specification<Movie> isComedy() { return (root, query, cb) -> { return cb.equal(root.get(Movie_.genre), Genre.Comedy); }; } public static Specification<Movie> isReallyOld() { return (root, query, cb) -> { return cb.lessThan(root.get(Movie_.createdAt), new LocalDate.now().minusYears(25)); }; } }

然后改写MovieRepository ,为了让Repository可以运行Specification ,我们需要让其继承JpaSpecificationExecutor 接口。

Copy
public interface MovieRepository extends JpaRepository<Movie, Long>, JpaSpecificationExecutor<Movie> { // query methods here }

然后我们就可以愉快的使用定义好的Specification 了。

Copy
movieRepository.findAll(MovieSpecifications.isComedy()); movieRepository.findAll(MovieSpecifications.isReallyOld());

在这里,repository 的代理类,会自动准备好CriteriaBuilder, CriteriaQuery, Root,是不是很爽?

从面向对象编程来讲,MovieSpecifications并不是很优雅,你可以这样做:

Copy
public MovieComedySpecification implements Specification<Movie> { @Override public Predicate toPredicate(Root<Movie> root, CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.equal(root.get(Movie_.genre), Genre.Comedy); }

联合Specifications#

我们可以将多个predicates 合到一起使用,通过and,or来连接。

Copy
movieRepository.findAll(Specification.where(MovieSpecifications.isComedy()) .and(MovieSpecifications.isReallyOld()));

Specification 构造器#

产品定义的业务逻辑,有时候会很复杂,比如我们需要根据条件动态拼接查询,我们可以定义一个SpecificationBuilder。

Copy
public enum SearchOperation { EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE; public static final String[] SIMPLE_OPERATION_SET = { ":", "!", ">", "<", "~" }; public static SearchOperation getSimpleOperation(final char input) { switch (input) { case ':': return EQUALITY; case '!': return NEGATION; case '>': return GREATER_THAN; case '<': return LESS_THAN; case '~': return LIKE; default: return null; } } } public class SearchCriteria { private String key; private Object value; private SearchOperation operation; } public final class MovieSpecificationsBuilder { private final List<SearchCriteria> params; public MovieSpecificationsBuilder() { params = new ArrayList<>(); } public Specification<Movie> build() { // convert each of SearchCriteria params to Specification and construct combined specification based on custom rules } public final MovieSpecificationsBuilder with(final SearchCriteria criteria) { params.add(criteria); return this; } }

使用方法:

Copy
final MovieSpecificationsBuilder msb = new MovieSpecificationsBuilder(); // add SearchCriteria by invoking with() final Specification<Movie> spec = msb.build(); movieRepository.findAll(spec);

Querydsl#

Querydsl, 动态查询语言,支持JPA。先引入:

Copy
<dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>${querydsl.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.6.1</version> </dependency>

Querydsl会根据表结构,生成meta-model,需要引入APT插件

maven配置:

Copy
<project> <build> <plugins> ... <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin> ... </plugins> </build> </project>

假设,我们有下面的Domain类:

Copy
@Entity public class Customer { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstname; private String lastname; // … methods omitted }

在这里生成,会根据表结构生成查询classes,比如QCustomer :

Copy
QCustomer customer = QCustomer.customer; LocalDate today = new LocalDate(); BooleanExpression customerHasBirthday = customer.birthday.eq(today); BooleanExpression isLongTermCustomer = customer.createdAt.lt(today.minusYears(2));

对比Specifications,这里是BooleanExpression,基本上基于生成的代码就可以构造了,更方便快捷。

现在我们到JPA使用,JPA 接口需要继承QueryDslPredicateExecutor

Copy
public interface CustomerRepository extends JpaRepository<Customer>, QueryDslPredicateExecutor { // Your query methods here }

查询代码:

Copy
BooleanExpression customerHasBirthday = customer.birthday.eq(today); BooleanExpression isLongTermCustomer = customer.createdAt.lt(today.minusYears(2)); customerRepository.findAll(customerHasBirthday.and(isLongTermCustomer));

同样的,Queydsl 还有一些类似直接写SQL的骚操作。

简单如:

Copy
QCustomer customer = QCustomer.customer; Customer bob = queryFactory.selectFrom(customer) .where(customer.firstName.eq("Bob")) .fetchOne();

多表查询:#

Copy
QCustomer customer = QCustomer.customer; QCompany company = QCompany.company; query.from(customer, company);

多条件#

Copy
queryFactory.selectFrom(customer) .where(customer.firstName.eq("Bob"), customer.lastName.eq("Wilson")); queryFactory.selectFrom(customer) .where(customer.firstName.eq("Bob").and(customer.lastName.eq("Wilson")));

使用JOIN#

Copy
QCat cat = QCat.cat; QCat mate = new QCat("mate"); QCat kitten = new QCat("kitten"); queryFactory.selectFrom(cat) .innerJoin(cat.mate, mate) .leftJoin(cat.kittens, kitten) .fetch();

对应JPQL

Copy
inner join cat.mate as mate left outer join cat.kittens as kitten

另外一个例子

Copy
queryFactory.selectFrom(cat) .leftJoin(cat.kittens, kitten) .on(kitten.bodyWeight.gt(10.0)) .fetch();

JPQL version

Copy
select cat from Cat as cat left join cat.kittens as kitten on kitten.bodyWeight > 10.0

Ordering#

Copy
QCustomer customer = QCustomer.customer; queryFactory.selectFrom(customer) .orderBy(customer.lastName.asc(), customer.firstName.desc()) .fetch();

Grouping#

Copy
queryFactory.select(customer.lastName).from(customer) .groupBy(customer.lastName) .fetch();

子查询#

Copy
QDepartment department = QDepartment.department; QDepartment d = new QDepartment("d"); queryFactory.selectFrom(department) .where(department.size.eq( JPAExpressions.select(d.size.max()).from(d))) .fetch();

小结#

本文简单介绍了JPA的Repository,以及面向动态查询的Querydsl和Specifications 的用法,使用JPA可以有效减少代码编写量,提升代码易读性和可维护性。

参考#


作者:Jadepeng
出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

关注作者

欢迎关注作者微信公众号, 一起交流软件开发:欢迎关注作者微信公众号

posted @   JadePeng  阅读(5552)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示
CONTENTS