数据访问层,所谓的CRUD是后端程序员的必修课程,Spring Data JPA 可以让我们来简化CRUD过程,本文由简入深,从JPA的基本用法,到各种高级用法。
Repository
Spring Data JPA 可以用来简化data access的实现,借助JPA我们可以快速的实现一些简单的查询,分页,排序不在话下。
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
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 接口来解决这个问题。
先来看这个接口定义:
public interface Specification<T> {
Predicate toPredicate(Root<T> root,CriteriaQuery query,CriteriaBuilder cb);
}
上文不是说需要先构建CriteriaBuilder,Root 吗,那么Specification接口就是给你提供这个三个参数,让你自己构建Predicate,想什么来什么。
我们用Specifications来改写代码,先定义Specification
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,cb) -> {
return cb.lessThan(root.get(Movie_.createdAt),new LocalDate.now().minusYears(25));
};
}
}
然后改写MovieRepository ,为了让Repository可以运行Specification ,我们需要让其继承JpaSpecificationExecutor 接口。
public interface MovieRepository extends JpaRepository<Movie,Long>,JpaSpecificationExecutor<Movie> {
// query methods here
}
然后我们就可以愉快的使用定义好的Specification 了。
movieRepository.findAll(MovieSpecifications.isComedy());
movieRepository.findAll(MovieSpecifications.isReallyOld());
在这里,repository 的代理类,会自动准备好CriteriaBuilder,Root ,是不是很爽?
从面向对象编程来讲,MovieSpecifications 并不是很优雅,你可以这样做:
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来连接。
movieRepository.findAll(Specification.where(MovieSpecifications.isComedy())
.and(MovieSpecifications.isReallyOld()));
Specification 构造器
产品定义的业务逻辑,有时候会很复杂,比如我们需要根据条件动态拼接查询,我们可以定义一个SpecificationBuilder。
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;
}
}
使用方法:
final MovieSpecificationsBuilder msb = new MovieSpecificationsBuilder();
// add SearchCriteria by invoking with()
final Specification<Movie> spec = msb.build();
movieRepository.findAll(spec);
Querydsl
Querydsl,动态查询语言,支持JPA。先引入:
<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配置:
<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类:
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstname;
private String lastname;
// … methods omitted
}
在这里生成,会根据表结构生成查询classes,比如QCustomer :
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
public interface CustomerRepository extends JpaRepository<Customer>,QueryDslPredicateExecutor {
// Your query methods here
}
查询代码:
BooleanExpression customerHasBirthday = customer.birthday.eq(today);
BooleanExpression isLongTermCustomer = customer.createdAt.lt(today.minusYears(2));
customerRepository.findAll(customerHasBirthday.and(isLongTermCustomer));
同样的,Queydsl 还有一些类似直接写SQL的骚操作。
简单如:
QCustomer customer = QCustomer.customer;
Customer bob = queryFactory.selectFrom(customer)
.where(customer.firstName.eq("Bob"))
.fetchOne();
多表查询:
QCustomer customer = QCustomer.customer;
QCompany company = QCompany.company;
query.from(customer,company);
多条件
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
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
inner join cat.mate as mate
left outer join cat.kittens as kitten
另外一个例子
queryFactory.selectFrom(cat)
.leftJoin(cat.kittens,kitten)
.on(kitten.bodyWeight.gt(10.0))
.fetch();
JPQL version
select cat from Cat as cat
left join cat.kittens as kitten
on kitten.bodyWeight > 10.0
Ordering
QCustomer customer = QCustomer.customer;
queryFactory.selectFrom(customer)
.orderBy(customer.lastName.asc(),customer.firstName.desc())
.fetch();
Grouping
queryFactory.select(customer.lastName).from(customer)
.groupBy(customer.lastName)
.fetch();
子查询
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可以有效减少代码编写量,提升代码易读性和可维护性。
参考
- https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/
- http://www.querydsl.com/static/querydsl/latest/reference/html/ch02.html#jpa_integration
- https://medium.com/@milan.brankovic/spring-advanced-search-filtering-5ee850f9458c
作者:Jadepeng
出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|