原创性声明:本文完全为笔者原创,请尊重笔者劳动力。转载务必注明原文地址。
背景
要认识spring-data-jpa
,就要和jpa
(Java Persistence API
:java 持久层API) 区分开来。这是两个不同的东西。jpa
是sun
公司官方的ORM
框架,严格上说,是java ORM
的一种规范,从jpa
的组成基本都是一些接口类就可见一斑,而具体的实现更多的由第三方ORM
框架完成,像Hibernate、TopLink、JDO
等。因此可以说,jpa
是一种规范标准,hibernate
是具体的实现。sun
公司之所以这样干,也是为了统一。
而Spring-data-jpa
又是什么呢?在企业级java EE开发中,Spring
的地位已经难以撼动,它几乎无所不能。而它的强大更多的体现在与第三方框架的整合上,对于持久化这一块,Spring
不仅仅可以很好的整合hibernate
,它自己也开了'自营业务',于是就有了Spring-data-**
等各种用于持久化操作的包,包括Spring-data-jpa、Spring-data-mongodb、Spring-data-template
等。
而此文就是记录其中的Spring-data-jpa
。
在使用持久化工具的时候,一般都有一个对象来操作数据库,在原生的Hibernate中叫做Session,在JPA中叫做EntityManager,在MyBatis中叫做SqlSession,通过这个对象来操作数据库。我们一般按照三层结构来看的话,Service层做业务逻辑处理,Dao层和数据库打交道,在Dao中,就存在着上面的对象。那么ORM框架本身提供的功能有什么呢?答案是基本的CRUD,所有的基础CRUD框架都提供,我们使用起来感觉很方便,很给力,业务逻辑层面的处理ORM是没有提供的,如果使用原生的框架,业务逻辑代码我们一般会自定义,会自己去写SQL语句,然后执行。在这个时候,Spring-data-jpa的威力就体现出来了,ORM提供的能力他都提供,ORM框架没有提供的业务逻辑功能Spring-data-jpa也提供,全方位的解决用户的需求。
开始
- 引入(以maven为例)
org.springframework.data
spring-data-jpa
2.0.0.M4
如果使用的是Spring boot
,那么可以这样引用:
org.springframework.boot
spring-boot-starter-data-jpa
2.例如,我们有一个实体类Article
,如下:
@Entity
@Table(name = "articles")
public class Article extends AbstractAuditingEntity {
@Id
@Column(name = "id", unique = true, nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "content", nullable = false)
private String content;
@Column(name = "if_set_top")
@ColumnComment("是否置顶")
private boolean ifSetTop = false; //默认false
// 省略get/set 方法...
}
3.我们需要创建一个接口去继承Spring-data-jpa
提供的接口:
public interface ArticleRepository extends JpaRepository {
}
通常的,ArticleRepository
的命名遵循约定俗称,其中指定的Article
表明了映射的对象,Long
指的是Article
实体类的主键id
类型。如此,就可以在这个接口类中,声明一些特定的方法,以满足简单的基本CRUD。
基本查询
参见一张从网上down下来的截图:
参见这张截图,可以很简单的写出针对Article
的一些查询,只需要在上面的ArticleRepository
中定义这些方法即可,而不需要任何的实现,这就是Spring-data-jpa
的强大之一。
注意:1. 接口方法名的命名要严格遵循驼峰法则;2.有些方法在
JpaRepository
中就已经声明可以直接调用,例如findAll, findOne,save, saveAndFlush
等。
复杂查询
Spring-data-jpa
的复杂查询体现在强大的动态查询和分页查询上。
4.我们需要让ArticleRepository
接口再继承一个接口: JpaSpecificationExecutor
。如下:
public interface ArticleRepository extends JpaRepository,
JpaSpecificationExecutor {
}
进入JpaSpecificationExecutor
接口可以发现,一些用于动态查询和分页查询的方法:
T findOne(Specification spec);
List findAll(Specification spec);
Page findAll(Specification spec, Pageable pageable);
List findAll(Specification spec, Sort sort);
long count(Specification spec);
5.接下来,我们就可以在ArticleService
中这样去调用ArticleRepository
的方法了:
@Service
public class ArticleService {
/**
* 过滤文章,传入filter,和一个分页对象,filter可以匹配id和title。返回分页查询后的结果
*/
public Page filterArticleByIdOrTitle(String filter, Pageable pageable) {
return articleRepository.findAll(new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {
Predicate predicate = null;
if (filter != null && !filter.trim().equal("")) {
if (StringUtils.isNumeric(filter)) {
predicate = cb.equal(root. get("id"), "%" + filter + "%");
} else {
predicate = cb.like(root.get("title")), "%" + filter + "%");
}
}
return predicate;
}
}, new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), pageable.getSort()));
}
}
观察上面的代码,articleRepository.findAll(Specification spec, Pageable pageable)
,方法传入了两个接口作为参数。我们在new
了这两个参数之后,进一步重载了toPredicate
方法,这个方法是实现过滤查询的核心,它相当于在拼接最终复杂查询的sql,因此,其中可以去实现更为复杂的查询方法,包括但不限于: 排序、 分组、 关联其他表等。
predicate
译文就是"断言"的意思,在sql中代指where
后面的内容。
事实上,我们可以创建一个ArticleSpecification
类,来继承Specification
接口,从而实现toPredicate
方法。这样的做法更加的优雅。
6.创建ArticleSpecification
类,用以继承Specification
,并实现toPredicate
方法。
public class ArticleSpecification extends AbstractSpecification {
private final ArticleCriteria criteria; // 动态查询的条件包装类
public ArticleSpecification(ArticleCriteria criteria) { // 构造方法,用以传入criteria
this.criteria = criteria;
}
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {
// 实现toPredicate
Predicate predicate = cb.conjunction();
List> expressions = predicate.getExpressions();
// 快速搜索: 匹配id或title
if (StringUtils.isNotBlank(criteria.getFilter)) {
if (StringUtils.isNumeric(filter)) {
expressions.add(cb.equal(root. get("id"), "%" + criteria.getFilter()+ "%"));
} else {
expressions.add(cb.like(root.get("title")), "%" + criteria.getFilter() + "%"));
}
}
// 高级搜索: 精确匹配id
if (criteria.getId() != null) {
expressions.add(cb.equal(root. get("id"), "%" + criteria.getFilter()+ "%"));
}
// 高级搜索: 模糊匹配title
if (StringUtils.isNotBlank(criteria.getTitle())) {
expressions.add(cb.like(root.get("title")), "%" + criteria.getFilter() + "%"));
}
return predicate;
}
}
上面这个类中,我们定义了一个属性ArticleCriteria
对象,它是一个条件包装类。如下:
public class ArticleCriteria {
private Long id;
private String title;
private String filter;
private Boolean ifSetTop;
// 省略get/set方法
}
这样在ArticleService
中,我们就可以这样去修改:
@Service
public class ArticleService {
/**
* 过滤文章,传入filter,和一个分页对象,filter可以匹配id和title。返回分页查询后的结果
*/
public Page filterArticleByIdOrTitle(ArticleCriteria criteria, Pageable pageable) {
return articleRepository.findAll(new ArticleSpecification(criteria), pageable);
}
}
到这里,后端基本完成了。前端,我们就可以在页面视图上提供两种查询:简单查询
和高级查询
。
简单查询下,只需提供一个 filter对应的输入框,而高级查询下,提供两个输入框,一个对应id,一个对应title。
7.在客户端,我们只需要构造这样的POST请求即可(以angular为例):
$scope.query = { // 搜索的条件
filter: '',
page: 1,
size: 10
}
$http.post("api/article", {
filter: $scope.query.filter,
page: $scope.query.page - 1,
size: $scope.query.size
}).success(function(){
// todo
})
至于数据如何与视图绑定,这里就不讲了。
angular
页面分页的组件可以使用angular material
的angular-material-data-table
。
补充
如果我们查询的结果进行排序,例如根据id倒序,但是希望ifSetTop为true的排在前面。可以在toPredicate
方法中这样去构造Predicate
:
public class ArticleSpecification extends AbstractSpecification {
private final ArticleCriteria criteria; // 动态查询的条件包装类
public ArticleSpecification(ArticleCriteria criteria) { // 构造方法,用以传入criteria
this.criteria = criteria;
}
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {
// 实现toPredicate
Predicate predicate = cb.conjunction();
List> expressions = predicate.getExpressions();
// 快速搜索: 匹配id或title
if (StringUtils.isNotBlank(criteria.getFilter)) {
if (StringUtils.isNumeric(filter)) {
expressions.add(cb.equal(root. get("id"), "%" + criteria.getFilter()+ "%"));
} else {
expressions.add(cb.like(root.get("title")), "%" + criteria.getFilter() + "%"));
}
}
// 高级搜索: 精确匹配id
if (criteria.getId() != null) {
expressions.add(cb.equal(root. get("id"), "%" + criteria.getFilter()+ "%"));
}
// 高级搜索: 模糊匹配title
if (StringUtils.isNotBlank(criteria.getTitle())) {
expressions.add(cb.like(root.get("title")), "%" + criteria.getFilter() + "%"));
}
// 利用query去排序(也可以做更复杂的逻辑处理)
query.where(predicate);
List orders = new ArrayList<>();
orders.add(cb.desc(root.get("ifSetTop")));
orders.add(cb.desc(root.get("id")));
query.orderBy(orders);
return query.getRestriction();
}
}
涉及多表的复杂查询,例如,Article关联了另一个实体Book,需要根据BookId,过滤Article,或者过滤出不属于任何一本书的Article。则需要进一步熟练的在
toPredicate
方法中,构造predicate
。
在上面的代码中,例如:
root.get("id")
也可以通过JPA的原模型对象来访问:
root.get(Article_.id)
前提是,在这之前,需要引入pom依赖:
org.hibernate
hibernate-jpamodelgen
true
再用maven
重新编译即可生成Entity_
这样的原模型对象。
关于spring-data-jpa的更多内容,推荐这篇博客