使用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的简单集成使用