Spring Data JPA的前两篇已经写了通过方法名格式自动生成SQL,也简单的提到了@Query
注解。但是往往真正的业务逻辑里面,这些是完全不够用的,涉及到一些稍微复杂一点的查询就会有点问题,如根据一组条件中的某几个条件查询(条件不固定),然后再加上分页、排序,这个时候只是使用之前的方法就有点捉襟见肘啦。
这篇博客的篇幅不会很长,主要是讲两个点,一个是在Spring Data JPA系列的第一篇博客中提到的@Query
注解,一个就是通过Specification
组合动态条件以及Pageable
和Sort
实现分页和排序。
@Query
注解
@Query
注解使用起来很简单,默认的属性是value
,就是当前写的SQL语句,有时会用到nativeQuery
属性,这个属性是用来标记当前的SQL是本地SQL,还是符合JPA语法规范的SQL。这里需要解释一下本地SQL和JPA语法规范的SQL区别。
-
本地SQL,是根据实际使用的数据库类型写的SQL,这种SQL中使用到的一些语法格式不能被JPA解析以及可能不兼容其他数据库,这种SQL称为本地SQL,此时需要将
nativeQuery
属性设置为true
,否则会报错。 -
JPA语法规范的SQL,往往这种SQL本身是不适用于任何数据库的,需要JPA将这种SQL转换成真正当前数据库所需要的SQL语法格式。
注意:JPA很好的一个特性就是用JPA语法规范写的SQL,会根据当前系统使用的数据库类型改变生成的SQL语法,兼容数据库类型的切换,如之前使用的是MySQL,现在换成Oracle,由于不同类型的数据库,SQL语法会有区别,如果使用的是mybatis,就需要手动去改SQL兼容Oracle,而JPA就不用啦,无缝对接。
说明:很大的时候使用JPA感觉都是为了兼容后期可能会有数据库切换的问题,所以在使用JPA的时候,不要去使用本地SQL,这就违背了使用JPA的初衷,让nativeQuery属性保持默认值就可以啦!(切记切记)
举个栗子
根据这个栗子再引出一些常用的东西,代码如下:
//示例1
@Query("select t from Device t where t.deviceSn=:deviceSn and t.deleteFlag=1")
Device findExistDevice(@Param("deviceSn") String deviceSn);
//示例2
@Query("select t from Device t where t.deviceSn=:deviceSn and t.deviceType =:deviceType and t.deleteFlag=1")
Device findExistDevice(@Param("deviceSn") String deviceSn,@Param("deviceType")Integer deviceType);
//示例3
@Query("select t from Device t where t.deviceSn=?1 and t.deviceType = ?2 and t.deleteFlag=1")
Device findDevice(String deviceSn,Integer deviceType);
- 在SQL上使用占位符的两种方式,第一种是使用":“后加变量的名称,第二种是使用”?“后加方法参数的位置。如果使用”:“的话,需要使用
@Param
注解来指定变量名;如果使用”?"就需要注意参数的位置。 - SQL语句中直接用实体类代表表名,因为在实体类中使用了
@Table
注解,将该实体类和表进行了关联。
还有其他有使用SpEL表达式等等就不多说了,基本没什么用,也是一些不常用的东西(可以了解了解去装X)。
@Modifying注解
相信在正常的项目开发中都会涉及到修改数据信息的操作,如逻辑删除、封号、解封、修改用户名、头像等等。在使用JPA的时候,如果@Query
涉及到update
就必须同时加上@Modifying
注解,注明当前方法是修改操作。
如下代码:
@Modifying
@Query("update Device t set t.userName =:userName where t.id =:userId")
User updateUserName(@Param("userId") Long userId,@Param("userName") String userName);
到这里就写完了@Query
的基本用法,很多复杂的用法就不多说了,用不上。
Specification+Pageable+Sort组合复杂SQL
在查询列表数据的时候,这三个基本都是可以用上的,如果不涉及到排序、分页,只是组合动态的查询条件,Specification
就够用了,下面来依次说一下。
Specification动态组合查询
组合条件查询很常见,使用@Query
或者根据方法名自动生成SQL实现都不是很方便,JPA提供了Specification
解决了这个问题。如下代码:
package com.itcrud.jpa;
import com.google.common.collect.Lists;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Predicate;
import java.util.List;
/**
* @Author: IT-CRUD
*/
public class ArticleService {
@Autowired
private ArticleRepository repository;
public void articleList(final ArticleReqDTO reqDTO) {
Specification<Article> spec = (root, query, builder) -> {
List<Predicate> predicates = Lists.newArrayList();
//等于,根据ID精确查询
if (reqDTO.getId() != null) {
predicates.add(builder.equal(root.get("id"), reqDTO.getId()));
}
//模糊,关键字匹配文章摘要+标题
if (StringUtils.isNotBlank(reqDTO.getKeywords())) {
predicates.add(builder.or(
builder.like(root.get("title"), "%" + reqDTO.getKeywords() + "%"),
builder.like(root.get("abstract"), "%" + reqDTO.getKeywords() + "%")
));
}
//in范围,根据文章分类查询
if (CollectionUtils.isNotEmpty(reqDTO.getCategories())) {
CriteriaBuilder.In<Object> builderIn = builder.in(root.get("category"));
for (Integer category : reqDTO.getCategories()) {
builderIn = builderIn.value(category);
}
predicates.add(builderIn);
}
return builder.and(predicates.toArray(new Predicate[predicates.size()]));
};
List<Article> articles = repository.findAll(spec);
}
}
这里具体的Article
实体类、ArticleReqDTO
实体类就不展示出来了,太占地方,意会即可。但是ArticleRepository
还是要看一下,里面不仅要继承JpaRepository
,还要继承另一个接口JpaSpecificationExecutor
。看代码:
//泛型的Article是代表数据库的映射的类,Long代表主键的类型
public interface ArticleRepository extends JpaRepository<Article, Long>, JpaSpecificationExecutor<Article> {
}
上面在ArticleService
类中使用到了findAll
方法,但是在ArticleRepository
类里面没有这个方法,那是因为在JpaRepository
和JpaSpecificationExecutor
里面有很多内置的方法,这里使用的是Specification
,很容易可以理解到使用的都是JpaSpecificationExecutor
内的方法,这个接口里面的方法都很简单。(这些内置的方法也挺好用,方法很多)
组合条件的代码不是很难,就是从写SQL转换成用代码写有一个适应的过程,另外用SQL的话会更直观一点,这个代码写了以后需要在日志里面打印出SQL语句,检查SQL的正确性。代码中builder
内还有很多可用的API,比如大于、小于、等于等操作,就不一一的列举啦。
Pageable+Sort分页排序
分页排序往往都是一起用的,Pageable
的实现类PageResult
本身的构造方法里面就支持自动构建Sort
对象。
看下面的代码:
//省略部分……
public void articleList(final ArticleReqDTO reqDTO) {
Specification<Article> spec = (root, query, builder) -> {
List<Predicate> predicates = Lists.newArrayList();
//省略部分……
return builder.and(predicates.toArray(new Predicate[predicates.size()]));
};
Pageable pageable = new PageRequest(reqDTO.getPageNo() - 1
, reqDTO.getPageSize(), Sort.Direction.ASC, "createTime");//按照createTime升序
Page<Article> pageInfo = repository.findAll(spec, pageable);
}
这里代码的省略部分和上面介绍Specification
的代码一样,可以参考,这里就不写出来啦。重点代码是最后两行。需要注意的就是pageNo
参数,JPA是从0开始,如果外部传入的页码是从1开始,就需要做减1的操作;另外就是这里看似没有涉及到Sort
,但是实际是用到的,看PageResult
构造方法源码如下:
public PageRequest(int page, int size, Direction direction, String... properties) {
//使用后两个参数,构建了Sort对象,排序支持多个字段
this(page, size, new Sort(direction, properties));
}
其他都是常规操作啦。具体查到的数据,已经和分页相关的参数都封装在pageInfo
里面。
还有一种组合是Specification+Sort
对查询的数据按照指定格式排序,Sort
的创建可以参考一下上面的代码,也可以直接去看源码,比较简单,不赘述。
总结
草草的写了三篇关于JPA的博客,写了基本的用法,从起初第一篇的Repository
接口的创建、实体类的注解说明,基本的操作,然后第二篇写了JPA的特性之根据方法自动生成SQL,给出了一些常用的模板。最后就是这一篇,涉及到手动写SQL和一些单表的复杂SQL编写。基本的使用已经完全没有问题了,但是这些都是单表操作,如果涉及多表联查,就会不太好使,为了暂时的方便,在项目开发的过程中都是直接写native SQL,放在@Query
注解里面。虽然个人是极其不推荐的,可是真是JPA对连表查询不友好,操作也比较麻烦。
以后如果很闲的话,可能会更新写JPA连表查的相关博客。之所以不写有下面几个原因:
- 如果只是简单表联查,完全可以拆成多次来查,有时候多次查比连表查更方便快捷,效率上往往单表操作比联表查更高
- 如果涉及到非常复杂的连表查询,即使你会用JPA连表查的操作,你也不会去做,涉及的代码量很大,而且不方便,这个时候不如写native SQL来的快,效率高
- 虽然不推荐写native SQL,但是项目中需要使用到很复杂SQL查询的地方一般都是很少的,对这些复杂查询使用了复杂SQL,以后切换数据库改动量也不大
总结:单表操作直接怼,简单联查拆开搞,复杂SQL不用怕,本地SQL来补充,代码优雅最大化,切库改动也不大。