简介

CRUD-Boy,这是一个很尴尬的名词。Create、Retrieve、Update、Delete,像是四件法器,人人都喜欢提高并发、高可用,与之陪衬的便是缺乏艺术感和想象力的CRUD,可真要做一个工程应用还是得靠这玩意儿打天下。但它又是像是一柄达摩克利斯之剑,机械地使用它,也许真的会消磨掉人的热忱,甚至要命。

千万蝼蚁筑成大厦,CRUD连接你我。不会不行,只会也不行。总有那么段时光是CRUD陪你度过的,那只好尽量优雅一些吧,起码写完看起来还舒服一点。虽说是CRUD,但是本篇主要介绍R、查询为主,其他的皆可触类旁通。

简史

SQL是CRUD的祖宗,数据库是万恶之源。从手写SQL通过JDBC操作开始,逐步诞生了ORM(Object Relational Mapping)框架。首先解决的是返回值的处理,面向对象这一套东西就天生与数据返回的零散字段不和,得把字段装起来变成一个对象。返回值的处理够舒服了,接下来就是SQL的生成,一派MyBatis,一派Hibernate,Hibernate这一派被官方选中然后就出了个JPA(Java Persistent API),两派各有千秋。MyBatis这一派更多的是扩展、插件,JPA这一派更多是封装、整合、适配。好像也有拿MyBatis去实现JPA的,不过感觉理念上就不太合得上,目前为止好像也没闹出太大的动静。

这篇主要说JPA这一派的查询SQL构建。

Hibernate

要说JPA还是离不开Hibernate,毕竟源头就是Hibernate。作为一款全自动框架,可以感觉到Hibernate是希望完全摒弃使用SQL, Hibernate通过Entity、ManyToOne、OneToMany、ManyToMany等概念将表和类、表与表之间的关系和类与类之间的关系进行相互对应描述,并且把返回值封装后的实体全部管理了起来,形成了一套生命周期体系。

这种管理构成了Hibernate的一级缓存,在同一个Session下,可以不重复获取相同数据,并且能够延迟操作数据库的时间,在事务提交时统一刷新处理提高效率。并且Hibernate可以感知到数据的更改,与数据库进行自动同步。

优点是完全面向对象,描述问题和业务场景非常直接。

缺点也很明显,极容易产生大量的SQL语句、自动更新对象让人觉得有些“自作主张”(没让你存你怎么自己就存上了)、SQL优化不方便、复杂查询不方便、更新语句臃肿。

优点有点短,不过份量很重。缺点不是想改就改的缺点,为了实现优点,缺点也是不得已而为之。有些缺点确实挺致命的,导致应用的流行程度不如MyBatis高,但也在逐步改善。

JPA

JPA是一套持久化的规范,作为一套标准的接口,可以有各种各样的实现,不过主要还是以Hibernate为主,毕竟JPA的主导者就是Hibernate的作者,本身的设计也大量借鉴了Hibernate。JPA的EntityManager基本上可以认为是和Hibernate的Session相互对应。个人觉得相较于Hibernate的接口,JPA的设计确实更加舒服一些。

首先列出示例代码会用到的实体类,接下来介绍本篇的3个主要内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Entity
@Table(name = "Gift")
@Data
@NoArgsConstructor
public class Gift implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

private String description;

private Double price;

private Double cost;

@ManyToOne
@JoinColumn(name = "buyer_id")
private Person buyer;

@Version
private Integer version;

@UpdateTimestamp
private Timestamp updateTime;

}

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Person implements Serializable {

@Id
@GeneratedValue
private Long id;

private String personName;

public Person(String personName) {
this.personName = personName;
}
}

实体类中有使用到Lombok注解,如果不清楚可以先了解一下,很不错的一个代码工具。

Criteria API

JPA在JDK1.5时引入,JDK1.6时JPA更新到2.0版本,带来了许多新的特性和增强,对于本篇来说最重要的就是其中的Criteria API。说到这个还是要先提一下JPQL。和JPA类似,JPQL前身是HQL(Hibernate Query Language),目的为了全面适应面向对象和应对一些当时无法解决的复杂查询。JPQL和SQL非常类似,一句话概括来说SQL是from Table而JPQL是from Object

JPQL无法解决的问题主要是没有动态调整语句的能力以及保证类型安全能力。而这两点,尤其是第一点,正是Criteria API所能够解决的。Criteria的核心是对查询的描述和解析,通过一系列类和接口完整描述查询的内容后就可以生成语法树翻译成为正确的SQL语句。

接口结构

以下是javax.persistence.criteria包下绝大部分接口的关系图:

内容不少,但是核心接口主要是三个:

  • CriteriaBuilder:基本构建器,创建Criteria、Predicate等等
  • Criteria:主要以CriteriaQuery来说,是整个查询描述的主体
  • Expression:表达式的核心接口,以Root和Predicate使用居多

Criteria可以保证类型安全,但并不是强制性的,如果需要保证类型安全则需要编写额外的持久化元模型来构建表达。基本结构形似如下:

1
2
3
4
public class Person_ {
public static volatile SingularAttribute< Person,Long> id;
public static volatile SingularAttribute< Person,String> personName;
}

元模型也可以通过Annotation Processor工具生成规范元模型,这部分不详细描述,有兴趣可以自查。

代码示例

本篇所有的示例都是写在SpringBoot项目下的测试类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@SpringBootTest
@RunWith(SpringRunner.class)
public class QueryTest {

@PersistenceContext
private EntityManager entityManager;

@Test
public void testCriteria(){
// 简单条件查询
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Gift> simleQuery = cb.createQuery(Gift.class);
Root<Gift> simpleRoot = simleQuery.from(Gift.class);
simleQuery.where(cb.and(
cb.greaterThan(simpleRoot.get("cost"), 70),
cb.isNotNull(simpleRoot.get("name"))));
simleQuery.orderBy(cb.asc(simpleRoot.get("cost")));

TypedQuery<Gift> typedQuery = entityManager.createQuery(simleQuery);
List<Gift> result = typedQuery.getResultList();
System.out.println("count: "+result.size());

// join条件查询
CriteriaQuery<Gift> joinQuery = cb.createQuery(Gift.class);
Root<Gift> joinRoot = joinQuery.from(Gift.class);
Join<Gift, Person> buyerJoin = joinRoot.join("buyer");
simleQuery.where(cb.and(
cb.greaterThan(simpleRoot.get("cost"), 70),
/*cb.equal(simpleRoot.get("buyer").get("personName"), "mike")*/
cb.equal(buyerJoin.get("personName"), "Mike")));
simleQuery.orderBy(cb.asc(simpleRoot.get("cost")));

TypedQuery<Gift> typedQuery2 = entityManager.createQuery(simleQuery);
List<Gift> result2 = typedQuery2.getResultList();
System.out.println("count: "+result2.size());

// 查询指定数据
CriteriaQuery<javax.persistence.Tuple> tupleQuery = cb.createTupleQuery();
Root<Gift> specRoot = tupleQuery.from(Gift.class);

tupleQuery.select(cb.tuple(
specRoot.get("id"),
specRoot.get("name"),
specRoot.get("price")));
tupleQuery.where(cb.and(
cb.greaterThan(simpleRoot.get("cost"), 70),
cb.isNotNull(simpleRoot.get("name"))));
tupleQuery.orderBy(cb.asc(simpleRoot.get("cost")));
TypedQuery<javax.persistence.Tuple> typedQuery3 = entityManager.createQuery(tupleQuery);
List<javax.persistence.Tuple> result3 = typedQuery3.getResultList();
System.out.println("count: "+result2.size());


// 复杂查询示例
// 使用case...when进行sum操作
CriteriaQuery<javax.persistence.Tuple> sumQuery = cb.createTupleQuery();
Root<Gift> sumRoot = sumQuery.from(Gift.class);
Expression<Integer> flag = cb.selectCase(sumRoot.get("name"))
.when("金块", 100)
.when("银块", 90)
.when("铜块", 30)
.when("塑料", 10)
.otherwise(0).as(Integer.class);
sumQuery.select(cb.tuple(cb.sum(flag)));
sumQuery.groupBy(sumRoot.get("buyer").get("personName"));
javax.persistence.Tuple singleResult = entityManager.createQuery(sumQuery).getSingleResult();
System.out.println(singleResult.get(0));

// 使用排序函数
CriteriaQuery<Gift> orderQuery = cb.createQuery(Gift.class);
Root<Gift> orderRoot = orderQuery.from(Gift.class);
orderQuery.orderBy(cb.desc(
cb.function("FIELD",null, orderRoot.get("name"),
cb.literal("金块"),
cb.literal("银块"),
cb.literal("铜块"),
cb.literal("塑料"))));
}

}

总的来说,主体(CriteriaQuery)、条件、排序、聚合等内容的创建都由CriteriaBuilder创建,查询的字段一般通过Root获取,排序、组合等通过查询主体操作。处理完一个查询一般需要三个基本对象:CriteriaBuilder、CriteriaQuery、Root。

Querydsl

Querydsl是一个基于各种ORM之上的一个通用查询框架,专注于通过Java API构建类型安全的SQL查询,使用它的API类库可以写出“Java代码的sql“。目前Querydsl支持多种平台,包括:JPA、原生SQL、MongoDB、Lucence和JDO等。

接口结构

以下是com.querydsl.core.types包下大部分接口以及其他部分核心接口的关系图:

可以看到Querydsl整个API的设计更加集中到Expression这点上,这也是Querydsl能够以流式API书写代码的原因之一。不过这张结构图拿来直接和Criteria比是不太妥当的,首先这仅仅是Querydsl整个包的一部分,其次个人认为Querydsl的这些API更加专注于“interface”这个概念,而Criteria这边因为JPA是一个规范,所以它的API更偏向于对实现类的指导。在实现类上也可以窥得一二,Criteria API的实现类一般就是接口名加上Impl后缀,只实现一个Criteria下的接口(当然具体实现没有这么简单),而Querydsl的实现类一般是组合实现相应的基础接口。以下是两个Querydsl应用于JPA时的核心类JPAQueryEntityPathBase的类图。

应用方法

以结合JPA使用为例,需要引入querydsl-jpa和querydsl-apt以及编译插件com.querydsl.apt.jpa.JPAAnnotationProcessor,使用maven时,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>

<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

<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>

编写完实体类后,运行maven的compile命令,就会在outputDirectory指定的目录下生成对应的查询类型。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@SpringBootTest
@RunWith(SpringRunner.class)
public class QueryTest {

@PersistenceContext
private EntityManager entityManager;

@Test
public void testQuerydsl(){
QGift gift = QGift.gift;
JPAQueryFactory factory = new JPAQueryFactory(entityManager);

// 简单条件查询
List<Gift> simpleResult = factory.selectFrom(gift)
.where(
gift.cost.gt(70),
gift.name.isNotNull()
).fetch();
System.out.println("count: "+simpleResult.size());

// join条件查询
QPerson person = QPerson.person;
List<Gift> joinResult = factory.selectFrom(gift)
.leftJoin(person)
.on(gift.buyer.id.eq(person.id)) // 示范操作,实体类没有使用关联的时候可以这么处理
.where(
gift.cost.gt(70),
person.personName.isNotNull()
).fetch();
System.out.println("count: "+joinResult.size());

// 查询指定数据
List<Tuple> tupleResult = factory.select(
gift.id,
gift.name,
gift.price
).from(gift).where(
gift.cost.gt(70),
gift.name.isNotNull()
).orderBy(gift.cost.asc()).fetch();
System.out.println("count: "+tupleResult.size());


// 复杂查询示例
// 使用case...when进行sum操作
List<Tuple> sumResult = factory.select(gift.buyer.personName,
gift.name
.when("金块").then(100)
.when("银块").then(90)
.when("铜块").then(30)
.when("塑料").then(10)
.otherwise(0)
.sum()
).from(gift).groupBy(gift.buyer).fetch();


// 使用排序函数
StringTemplate orderExp = Expressions.stringTemplate("FIELD({0},{1},{2},{3},{4})",
gift.name,
"金块", "银块", "铜块", "塑料");
List<Gift> orderResult = factory.selectFrom(gift).orderBy(orderExp.desc()).fetch();
}

}

可以感受到Querydsl所构建的查询确实和构建SQL十分相似,所有操作都起始于Factory,元模型的结构能够快速获取字段对应的java属性并且封装了一些常用的方法,通过流式API构建的过程非常舒服。

Spring-Data

Spring Data作为Spring的一个子项目,其目的是提供一个简便、可靠的基于Spring的持久化编程模型,同时保留底层数据存储的特性。为了减少各种持久化工具实现数据访问层所需的样板代码数量,Spring Data提供了一层二次抽象、封装。这个子项目的子项目Spring Data JPA便是Spring Data针对JPA的整合。

Spring Data抽象封装的核心接口是Repository,它将Domain(领域)类或者Domain类的ID类型作为类型参数进行管理,通过进一步的继承处理提供各种功能,例如CrudRepository提供了基本的CRUD功能。

领域驱动设计

Repository这个名称取得非常有意图,它来自于领域驱动设计(Domain Driven Design)的概念,DDD的核心在于提倡使用充血模型,规范出界限上下文建立对应的领域。充血模型简单来说就是统一处理数据和行为,模型结构能够完整地表达业务的一个领域,而不是建立一个数据对象然后使用各种service对象去执行方法。在DDD的概念下,接触到需求第一步就是考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现,造成需求的首肢分离。DDD让你首先考虑的是业务的各个领域,而不是数据。

DDD中的仓储即为Repository,领域模型中的对象自从创建后不会一直留在内存活动,当它不活动时会被持久化到DB中,当需要的时候会重建该对象,仓储即提供相关接口来帮助我们管理对象。

DDD的内容不是一两句可以说清楚的,比较抽象,真正落地也不会很简单。在本篇讨论的范围内,了解其核心思想即可,也有助于理解Spring Data为什么这么做。即便是不了解DDD,使用Spring Data的时候也还是会感觉到非常自然。

代码示例

Spring Data的学习和应用还是应该参照官方文档比较好,虽然看起来比较长,但是内容不算太多,也不是很深的东西,毕竟目标是简化操作。这里给出一些代码,便可有个直观的感受。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@SpringBootTest
@RunWith(SpringRunner.class)
public class QueryTest {

@Autowired
private GiftRepository giftRepository;

@Test
public void testSpringData(){
// 单个查询
Gift gift = giftRepository.findById(1L).orElse(null);

// 条件查询
List<Gift> result = giftRepository.findAll(new Specification<Gift>() {
@Override
public Predicate toPredicate(Root<Gift> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Join<Gift, Person> buyerJoin = root.join("buyer");
Predicate predicate = cb.and(cb.greaterThan(root.get("cost"), 70),
cb.equal(buyerJoin.get("personName"), "Mike"));
query.orderBy(cb.asc(root.get("cost")));
return predicate;
}
});

// 分页查询
PageRequest page = PageRequest.of(0, 10);
Page<Gift> pageResult = giftRepository.findAll(new Specification<Gift>() {
@Override
public Predicate toPredicate(Root<Gift> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
query.orderBy(cb.asc(root.get("cost")));
return cb.and(
cb.greaterThan(root.get("cost"), 70),
cb.isNotNull(root.get("name")));
}
}, page);
}

@Test
public void testSpringDataWithDsl(){
// 结合Querydsl
QGift gift = QGift.gift;
PageRequest page = PageRequest.of(0, 10);
giftRepository.findAll(gift.cost.lt(10).and(gift.name.isNotNull()), page);
}

}

核心优势

说了一堆DDD的内容,实际用下来最直观的感受就是不直接使用EntityManager来统一管理各个实体对象,而是采用对应的Repository来进行操作,整个DAO层由根据Repository生成的代理类进行掌控,数据的处理被划分到各个对应的Repository中。使用ID查询单个对象、使用条件查询、持久化对象等基本高频操作非常方便、直接。

使用Criteria时,查询的条件封装成为Specification,核心方法toPredicate直接将CriteriaBuilderCriteriaQueryRoot三大核心传入,减少重复的模板代码。同时其和Querydsl结合良好,Querydsl生成的条件对象也可以直接交由对应的Repository进行处理。

不过相比于原生Criteria API和Querydsl来说,抛开设计的理念不谈,个人觉得最具优势的还是如下几点:

  • 简单查询封装完备,能够解析方法名直接作为CRUD方法
  • 加锁操作便捷
  • 强力的分页处理能力

注意事项

方便是方便,但Spring-Data只是一层封装,内部核心还是JPA、Hibernate那一套东西,生命周期这些基本概念还是得学,在Spring Data这套API下,事务提交时、使用悲观锁查询时还是会自动更新属性被修改的对象,这点尤其容易被忽略造成想不到的错误。调用save方法依然可能会被延迟执行,到事务提交时再统一处理。

总结

拥有JPA Criteria API之后,基本可以消灭丑陋的SQL拼接,除了from语句的子查询无法处理(毕竟返回值内容不符合面向对象)之外,基本上一般的SQL都是可以构建出来的。个人觉得单论MyBatis使用各式各样的辅助标签编写XML来说,使用Criteria明显更加舒服。Querydsl的封装更是将使用java编写sql的能力发挥到了极致,个人觉得如果愿意, 拿MyBatis的逻辑来处理也不成问题,封装一下对Tuple的映射,就又是一个XML转java代码的样例,顺便连生命周期那一套都给“抹掉”了,不过可行性有待验证。

原生Criteria、Querydsl、SpringData、SpringData结合Querydsl,可选的方式还是比较多的。尽管Querydsl可能写起来更舒服,但是要适应动态特性,必然会在代码中嵌入一些if else for的东西,可能就没这么简洁流畅了,而且最终还是要翻译到Criteria那里去。话又说回来,Querydsl与spring web方案还有个很不错的整合,直接能够把Http参数转化成为Predicate,摘抄一段官方文档的内容:

1
2
3
4
// url param
?firstname=Dave&lastname=Matthews
// resolved to dsl predicate
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
class UserController {

@Autowired UserRepository repository;

@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model,
@QuerydslPredicate(root = User.class) Predicate predicate,
Pageable pageable,
@RequestParam MultiValueMap<String, String> parameters) {

model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}

具体怎么选还是视具体情况而定,不过相信有了这些工具和框架,CRUD能够少一些枯燥繁琐。