使用Spring Boot JPA Specification实现使用JSON数据来查询实体数据

Posted c.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Spring Boot JPA Specification实现使用JSON数据来查询实体数据相关的知识,希望对你有一定的参考价值。

文章目录

使用Spring Boot JPA Specification实现使用JSON数据来查询实体数据

需求概要

看标题可能有一点懵,但这篇文章来源于一个需求,这个需求是这样的:我们有一个表的数据需要根据不同的条件进行抽取,诶呀可能有的人说直接配个SQL就完事了,使用SQL去查就好了,确实以前的做法就是直接配置SQL,但是直接配SQL存在一些SQL注入的风险,而且以后考虑做成UI可配置的话,配置SQL确实太难看了。所以我就想能不能通过配置一个JSON数据,通过这个JSON数据我们就可以查到想要的数据。以后我们还可以通过前台的一些配置,筛选字段和条件来构造出这个JSON数据,做到可视化的配置。然后构造出来的这个JSON就可以获取到对应表的数据了。这个获取数据的方式就很像mongo这种NoSQL的方式,通过json数据来查询数据了。而且注意一点是,我们只是支持单表查询,没有多表查询的需求了。

JSON 结构的设计

那我们首先就要设计一个JSON的结构能够支持我们一些普通的查询工作了。


    "conditions": [
        "conditions": [],
        "operation": null,
        "conditionExpression": 
            "type": "STRING", //支持不同的类型
            "column": "status", //对应实体的字段名
            "operateExpression": "=",
            "not": false, //如果not为true,则表示不等于
            "operateValue": ["success"],
            "dateformat": null,
            "dateFormatFunction": null
        
    , 
        "conditions": [],
        "operation": null,
        "conditionExpression": 
            "type": "NUMBER",
            "column": "size",
            "operateExpression": "=",
            "not": false,
            "operateValue": ["40"],
            "dateformat": null,
            "dateFormatFunction": null
        
    , 
        "conditions": [],
        "operation": null,
        "conditionExpression": 
            "type": "STRING",
            "column": "time",
            "operateExpression": "=",
            "not": false,
            "operateValue": ["2021"],
            "dateformat": "yyyy-MM-DD HH:mm:ss",
            "dateFormatFunction": 
                "dateFormat": "%Y"
            
        
    ],
    "operation": "AND", // conditions中的条件都是用AND来拼接
    "conditionExpression": null

上面这个json出来的查询条件其实就是(status = 'success' and size = '40' and date_format(time,'%Y') = '2021')

可以看到还能够支持日期的一些date_format方法

json结构出来了,我们就可以开始设计出来DTO对象

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class ConditionDTO implements Serializable 

  private static final long serialVersionUID = -5051343103773843259L;

  @Builder.Default
  private List<ConditionDTO> conditions = new ArrayList<>();

  private OperationEnum operation;

  private ConditionExpressionDTO conditionExpression;


public enum OperationEnum 
  AND, OR


@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class ConditionExpressionDTO implements Serializable 

  private static final long serialVersionUID = 7848696546935190452L;

  private ColumnType type;

  private String column;

  @Convert(converter = OperateExpressionEnum.Converter.class)
  private OperateExpressionEnum operateExpression;

  private boolean not;

  @Builder.Default
  private List<String> operateValue = new ArrayList<>();

  private String dateformat;

  private InternalDateFormatFunction dateFormatFunction;



@Getter
public enum ColumnType implements ConvertType 
  STRING(String.class) 
    @Override
    public String convert(String value) 
      return value;
    
  ,
  BOOLEAN(Boolean.class) 
    @Override
    public Boolean convert(String value)  //为了把字符串转换成具体的类型
      return Boolean.valueOf(value);
    
  ,
  NUMBER(Number.class) 
    @Override
    public Number convert(String value) 
      return new BigDecimal(value);
    
  ,
  DATE(LocalDate.class) 
    @Override
    public LocalDate convert(String value) 
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateTimeUtils.DATE_FORMAT);
      return LocalDate.parse(value, formatter);
    

    @Override
    public LocalDate convert(String value, String format) 
      if (StringUtils.isBlank(format)) 
        return this.convert(value);
      
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
      return LocalDate.parse(value, formatter);
    
  ,
  DATETIME(LocalDateTime.class) 
    @Override
    public LocalDateTime convert(String value) 
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateTimeUtils.DATE_TIME_FORMAT);
      return LocalDateTime.parse(value, formatter);
    

    @Override
    public LocalDateTime convert(String value, String format) 
      if (StringUtils.isBlank(format)) 
        return this.convert(value);
      
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
      return LocalDateTime.parse(value, formatter);
    
  ;

  private final Class<?> type;

  ColumnType(Class<?> clazz) 
    this.type = clazz;
  


public interface ConvertType 

  Object convert(String value);

  default Object convert(String value, String format) 
    return this.convert(value);
  


@Getter
public enum OperateExpressionEnum implements BaseEnum<String> 
  GT(">", List.of(ColumnType.values())),
  GE(">=", List.of(ColumnType.values())),
  LT("<", List.of(ColumnType.values())),
  LE("<=", List.of(ColumnType.values())),
  EQUALS("=", List.of(ColumnType.values())),
  EMPTY("empty", List.of(ColumnType.values())),
  LIKE("like", List.of(ColumnType.STRING)),
  BETWEEN("between", List.of(ColumnType.DATE, ColumnType.DATETIME)),
  IN("in", List.of(ColumnType.values()));

  @JsonValue
  private final String value;
  private final List<ColumnType> supportTypes; //每个操作,支持的数据类型

  OperateExpressionEnum(String value, List<ColumnType> supportTypes) 
    this.value = value;
    this.supportTypes = supportTypes;
  

  public static class Converter extends BaseEnumConverter<OperateExpressionEnum, String> 

  


  public static OperateExpressionEnum getOperateExpressionEnumByValue(String value) 
    for (OperateExpressionEnum operateExpressionEnum : OperateExpressionEnum.values()) 
      if (StringUtils.equalsIgnoreCase(value, operateExpressionEnum.getValue())) 
        return operateExpressionEnum;
      
    
    return null;
  


@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class InternalDateFormatFunction implements Serializable 

  private static final long serialVersionUID = 6895278799616731945L;
  public static final String FUNCTION_NAME = "date_format";
  public static final Class<?> RETURN_TYPE = ColumnType.STRING.getType();
  private String dateFormat;



使用策略模式执行不同的查询条件

OperateExpressionEnum中我们定义了很多不同的查询条件,等于,大于,小于,in, like等等的操作。 所以我们需要根据不同的查询条件分配不同的策略,然后构造出不同的JPA查询条件。

所以我们需要先定义一个策略工厂,然后我们从工厂中获取到具体的策略

@UtilityClass
public class PredicateStrategyFactory 

  private static final Map<String, PredicateStrategy> strategies = new ConcurrentHashMap<>();

  public static PredicateStrategy getByType(String type) 
    return strategies.get(type);
  

  public static void register(OperateExpressionEnum operateExpression, ColumnType columnType, PredicateStrategy predicateStrategy) 
    Assert.notNull(operateExpression, "Operate expression can't be null");
    Assert.notNull(columnType, "Column type can't be null");
    strategies.put(String.format("%s-%s", operateExpression.name(), columnType.name()), predicateStrategy);
  

然后我们看策略类的接口中有什么功能

public interface PredicateStrategy 

  Predicate getPredicate(Root<?> root, CriteriaBuilder criteriaBuilder, ConditionDTO condition); // 构造出JPA Specification需要的条件,通过这些条件加上and或者or的拼接,我们就可以构造出一个查询条件了,关于JPA Specification的使用不太明白的小伙伴可以先去看看文档

  String getConditionContent(ConditionDTO condition); //用于展示类似于SQL的字符串,因为给的是JSON配置,但是我们不知道JSON配的对不对,所以我们会返回一个类似SQL的字符串给用户展示


构造查询条件

我们要怎么根据构造出查询条件呢,其实就是递归到最后一层下面没有conditions的条件了,然后把这些conditions条件拼起来,通过上层的operation来拼接conditions中的条件,伪代码如下:

//伪代码
Condition condition = getCondition();
		
function getPredicate(Root<Entity> root,CriteriaBuilder criteriaBuilder,Condition condition)
	if(condition.conditions is empty) // 为空
		// 根据conditionExpression 分发不同的策略,最后返回一个Predicate
		return getPredicateByExpression(root,criteriaBuilder,condition)
	else //不为空
			if(condition.operition == 'AND')
				return criteriaBuilder.and(
					condition.conditions.map(c->getPredicate(root,criteriaBuilder,c)).toCollection().toArray(Predicate[]::new)
				)
			else if(ondition.operition == 'OR')
				return criteriaBuilder.or(
					condition.conditions.map(c->getPredicate(root,criteriaBuilder,c)).toCollection().toArray(Predicate[]::new)
				)
			
	
	

主逻辑具体的代码实现

接下来就给出主逻辑的具体实现的代码了

@UtilityClass
public class ConditionUtils 

  public static Predicate findByCondition(Root<?> root, CriteriaBuilder criteriaBuilder, ConditionDTO condition) 
    Predicate predicate = ConditionUtils.getPredicate(root, criteriaBuilder, condition);
    if (Objects.isNull(predicate)) 
      return criteriaBuilder.conjunction();
    
    return predicate;
  


  private static Predicate getPredicate(Root<?> root, CriteriaBuilder criteriaBuilder, ConditionDTO condition) 
    if (CollectionUtils.isEmpty(condition.getConditions())) 
      return getPredicateByExpression(root, criteriaBuilder, condition);
     else 
      if (Objects.equals(condition.getOperation(), OperationEnum.AND)) 
        return criteriaBuilder.and(
            condition.getConditions().stream().map(c -> getPredicate(root, criteriaBuilder, c))
                .filter(Objects::nonNull).collect(Collectors.toList()).toArray(Predicate[]::new)
        );
       else if (Objects.equals(condition.getOperation(), OperationEnum.OR)) 
        return criteriaBuilder.or(
            condition.getConditions().stream().map(c -> getPredicate(root, criteriaBuilder, c))
                .filter(Objects::nonNull).collect(Collectors.toList()).toArray(Predicate[]::new)
        );
      
      return null;
    
  

  private static Predicate getPredicateByExpression(Root<?> root, CriteriaBuilder criteriaBuilder, ConditionDTO condition) 
    if (Objects.isNull(condition.getConditionExpression())) 
      return null;
    
    ConditionAssertUtils.isFalse(Objects.isNull(condition.getConditionExpression().getColumn())
 

以上是关于使用Spring Boot JPA Specification实现使用JSON数据来查询实体数据的主要内容,如果未能解决你的问题,请参考以下文章

spring-boot与spring-data-JPA的简单集成使用

Spring boot JPA

Spring boot JPA

Spring boot JPA

Spring Boot:在Spring Boot中使用Mysql和JPA

Spring------Spring boot data jpa的使用方法