spring-data-jpa specification 复杂查询之 zuji-jpa

Zuji-JPA

官方文档

Zuji-JPA 是一个不用写sql的 Spring Data JPA 增强库,在 Spring Data JPA 的基础上简化开发,目的是让开发者不再去书写冗长的SQL,支持 入参定义式零逻辑极简Java动态链式 两种方式来代替SQL。

初衷

由于spring data jpa 在复杂查询这块的短板,我基于specification 功能 开发出了为简化开发而生的Spring Data JPA 增强库 zuji-jpa 框架。

特性

  • 无侵入性,只做增强不做改变;
  • 超轻量,jar包只有60K,更好用的spring-data-jpa增强库;
  • 低功耗,全部基于静态工具类方法实现,程序启动无需加载任何Class;
  • 提供了 入参定义式零逻辑(支持join) 和 极简Java动态链式 两种方式替代sql;
  • 支持动态投影(Dynamic Projections),支持自定义实体类(VO/DTO)查询返回;
  • 单层级的动态条件查询只需定义入参实体类,不用写具体java实现代码,也不用写sql,即可信手拈来;
  • 多层级嵌套复杂的动态条件查询使用超简洁的动态链式编程即可轻松实现;
  • 使用zuji-jpa可以大大简化开发、提高效率,节省更多的时间让你专注于业务;

支持场景

适用于 Java Spring Data JPA 项目,JDK 1.8 及以上,Spring Data JPA 版本无具体要求。

使用之前需要对Spring Data JPA框架有一定的了解和使用,如果还未使用过,请先写个简单的DEMO熟悉一下,具体可参考Spring Data JPA官方文档。

Spring 项目集成

如果你是 Spring 项目,请直接集成zuji-jpa依赖,spring boot和spring mvc项目均支持。

gradle

implementation 'top.spring-data-jpa:zuji-jpa:1.0.1'

maven

<dependency>
  <groupId>top.spring-data-jpa</groupId>
  <artifactId>zuji-jpa</artifactId>
  <version>1.0.1</version>
</dependency>

快速开始

Zuji-JPA查询全部基于Specification进行扩展增强开发,所以使用之前必须要继承JpaSpecificationExecutor接口

public interface BlogRepository extends JpaSpecificationExecutor<Blog> {

}

或者使用Zuji-JPA自带的BaseRepository,BaseRepository继承了JpaSpecificationExecutor和JpaRepository接口。

public interface BlogRepository extends BaseRepository<Blog> {
    
}

1、单层多条件查询

入参定义式零逻辑

入参定义式查询仅支持单层条件查询,支持JOIN,支持equal、like、in、between等这些常用的查询关键字,多层嵌套复杂查询请参考下一节java动态链式查询。

首先要定义查询入参的实体类,如果字段为NULL则不参与条件查询,默认使用的是等于equal,如果是Collection类型的字段,默认使用的是 IN 查询,
也可以使用@QueryOperator 注解里面的 fieldName 字段来定义对应数据库的字段名称。

/**
 *  如果字段为null则不参与条件查询,默认使用的是等于equal,
 *  如果是Collection类型的字段,默认使用的是 IN 查询
 */
@Data
public class ReqBlogQueryVO {
    /**
     *  此注解用于忽略查询
     */
    @QueryIgnore
    private Long id;
    /**
     *  此注解等同于SQL: title like "value%"
     */
    @QueryOperator(Operator.STARTING_WITH)
    private String title;
    /**
     *  属于Collection类型,所以默认使用的是IN查询
     */
    private List<String> author;
    /**
     *  此注解等同于SQL: content like "%value%"
     */
    @QueryOperator(Operator.CONTAINS)
    private String content;
    /**
     *  无注解默认使用的是等于
     */
    private Integer status;
    /**
     *  LEFT_JOIN的方式有两种,下面使用的是第二种
     */
    @JoinColumn(name="createUser")
    @QueryOperator(Operator.CONTAINS)
    private String userName;

   
}

入参定义式查询支持JOIN,LEFT_JOIN的方式有两种,以下任选其一即可。

1、@QueryOperator注解指定实体类JOIN别名,如 @QueryOperator(fieldName=“createUser.userName”,value=Operator.CONTAINS)

2、@JoinColumn注解指定JOIN的关联字段名,如 @JoinColumn(name=“createUser”)

由于Spring Data Jpa是基于Hibernate开发的,所以JOIN还是继承了Hibernate面向对象的方式,需要在实体类里面定义好关联关系。

:如果开启了自动建表而又不想在数据库创建外键关联的话需要加上注解
@JoinColumn(foreignKey = @ForeignKey(NO_CONSTRAINT))。

@Data
@Entity
@FieldNameConstants
public class Blog {
    //......其他属性省略
    /**
     * 定义和User表的关联关系
     */
    @ManyToOne
    @JoinColumn(foreignKey = @ForeignKey(NO_CONSTRAINT))
    private User createUser;

}

定义好之后就可以直接使用入参实体类生成查询条件,进行查询。

@RestController
@RequestMapping("/blog")
public class BlogController {

    @Autowired
    private BlogRepository repository;

    @PostMapping("/list")
    public Object list(@RequestBody ReqBlogQueryVO query){
        Specification<Blog> spec = Specifications.conditionOf(query);
        return repository.findAll(spec);
    }
}

等同于如下sql,如果字段值为NULL,则不参与条件查询。

SELECT 
    b.*
FROM
    blog b left outer join user u on b.createUserId = u.id
WHERE
    b.title LIKE 'zuji%'
        AND b.author IN ('azheng1' , 'azheng2')
        AND b.content LIKE '%value%'
        AND b.status = 0
        AND a.user_name = 'azheng'

2、多层嵌套复杂条件查询

java动态链式

此查询类似于mybatis-plus的条件构造器。
以下示例包含:动态条件查询 + or 嵌套条件+ 排序+ 分页

@RestController
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private UserRepository repository;
    
    @GetMapping("/list")
    public Page<User> list(ReqUserListVO params) {
       Specification<User> spec = Specifications.where(e -> {
           if (!params.getUserType().equals(UserType.ALL)) {
               e.eq("userType", params.getUserType());
           }
           if (params.getUserName() != null) {
               e.contains("userName", params.getUserName());
           }
           if (params.getRange() != null) {
               e.eq("assigneeId", AuthHelper.currentUserId());
           }
           e.or(e2 -> {
               e2.eq("status", "1");
               e2.eq("status", "2");
           });
           e.eq("deleted", 0);
       });
       Sort sort = Sort.by("createTime").descending();
       return repository.findAll(spec, params.pageRequest(sort));
    }
}

判断条件也可以放在方法里面,看起来更简洁一点,如下

public Page<User> list(ReqUserListVO params) {
   Specification<User> spec = Specifications.where(e -> {
       e.eq(!params.getUserType().equals(UserType.ALL), "userType", params.getUserType());
       e.contains(params.getUserName() != null, "userName", params.getUserName());
       e.eq(params.getRange() != null, "assigneeId", AuthHelper.currentUserId());
       e.or(e2 -> {
           e2.eq("status", "1");
           e2.eq("status", "2");
       });
       e.eq("deleted", 0);
   });
   Sort sort = Sort.by("createTime").descending();
   return repository.findAll(spec, params.pageRequest(sort));
}

如果没有条件判断也可以写成这样,链式编程

@RestController
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private UserRepository repository;
    
    @GetMapping("/list")
    public Page<User> list(ReqUserListVO params) {
       Specification<User> spec = Specifications.where(e -> {
           e.eq("userType", params.getUserType())
            .contains("userName", params.getUserName())
            .eq("assigneeId", AuthHelper.currentUserId())
            .or(e2 -> e2.eq("status", "1").eq("status", "2"))
            .eq("deleted", 0);
       });
       Sort sort = Sort.by("createTime").descending();
       return repository.findAll(spec, params.pageRequest(sort));
    }
}

等同于sql

SELECT
	* 
FROM
	user
WHERE
	user_type = 'ADMIN' 
	AND user_name like "%admin%" 
	AND assignee_id = 11 
	AND ( status = 1 OR status = 2 ) 
	AND deleted = 0 
ORDER BY
	create_time DESC 
	LIMIT 0,10

:如果第一层(最外面层)是OR关联查询,调用where方法的时候需要添加一个参数isConjunction,为true的时候为and连接(默认为true),为false的时候为or连接。

Specification<Blog> spec = Specifications.where(false, e -> {
    e.eq(Blog.Fields.status, 0);
    e.eq(Blog.Fields.status, 1);
});
select * from blog where status = 0 or status = 1;

:上述示例中查询的字段名使用的是字符串,字符串是不被检查的,很容易出错。lombok提供了可以生成和属性名一样的的静态字段内部类的注解,实体类上面添加@FieldNameConstants注解即可使用。

//...省略
@FieldNameConstants
public class Blog {
    //...省略
}
public Page<User> list(ReqUserListVO params) {
   Specification<User> spec = Specifications.where(e -> {
       e.eq(User.Fields.userType, params.getUserType())
        .contains(User.Fields.userName, params.getUserName())
        .eq(User.Fields.assigneeId, AuthHelper.currentUserId())
        .or(e2 -> e2.eq(User.Fields.status, "1").eq(User.Fields.status, "2"))
        .eq(User.Fields.deleted, 0);
   });
   Sort sort = Sort.by("createTime").descending();
   return repository.findAll(spec, params.pageRequest(sort));
}

3、两者结合使用

Zuji-Jpa支持将入参定义式JAVA动态链式两者结合在一起使用,可以面对更多复杂的场景。

首先需要定义入参实体类,具体参考入参定义式,定义好之后直接进行查询;

@PostMapping("/list")
public Object list(@RequestBody ReqBlogQueryVO query){
    Specification<Blog> spec = Specifications.conditionOf(query,e -> {
        e.eq(Blog.Fields.deleted, 0);
        // 如上,在此添加需要的查询,参考第二节 java动态链式 查询
    });
    return repository.findAll(spec);
}

4、自定义查询返回

Dynamic Projections(动态投影)

数据库查询大部分时候都要 返回自定义字段( VO/DTO ),Spring Data Jpa 默认支持 泛型 动态投影(Projections) ,投影的类型可以是接口也可以是类。

Repository查询接口定义

interface PersonRepository extends Repository<Person, UUID> {
  <T> Collection<T> findByUsername(String username, Class<T> type);
}

或者

interface PersonRepository extends Repository<Person, UUID> {
  NamesQueryDTO findByUsername(String username, Class<T> type);
}

基于接口的投影,须提供字段对应的get方法接口。

interface NamesQueryDTO {

  String getFirstname();
  String getLastname();
  
}

基于类的投影,定义好所有查询字段,并且提供全参构造器。强烈推荐 使用Lombok的@Value注解 简化代码。

@Value
class NamesOnlyDTO {
	String firstname, lastname;
}

提供工具类转换

动态投影有时候会不生效,这时候就需要自己进行类型转换,zuji-jpa提供了相关工具类。

没有分页的列表

List<Blog> list = repository.findAll(spec);
return EntityUtils.cast(list, RespBlogVO.class);

有分页的列表

Specification<Blog> spec = Specifications.conditionOf(vo, e->{
    e.eq(Blog.Fields.status, 1);
});
PageRequest page = vo.pageRequest(Sort.by(Blog.Fields.id).descending());
Page<Blog> list = repository.findAll(spec, page);
return JpaHelper.castPage(list, page, RespBlogVO.class);

或者

Specification<Blog> spec = Specifications.conditionOf(vo, e->{
    e.eq(Blog.Fields.status, 1);
});
Page<Blog> list = repository.findAll(spec, vo.pageRequest(Sort.by(Blog.Fields.id).descending()));
Page<RespBlogVO> result = JpaHelper.castPage(list, RespBlogVO.class);

单个对象

Blog blog = repository.findOne(id);
RespBlogVO result =  EntityUtils.cast(Blog, RespBlogVO.class);

如果有任何问题或想要更多交流,请加QQ群 758629787

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值