feat: 新增dao-support-lib step2
This commit is contained in:
parent
d28fd4254e
commit
dc2e340ce0
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
.idea/encodings.xml
|
||||
.idea/misc.xml
|
||||
.idea/uiDesigner.xml
|
||||
.idea/.gitignore
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
@ -0,0 +1,105 @@
|
||||
package cn.axzo.foundation.dao.support.data.converter;
|
||||
|
||||
import cn.axzo.foundation.dao.support.data.mysql.MybatisPlusConverterUtils;
|
||||
import cn.axzo.foundation.dao.support.data.page.IPageReq;
|
||||
import cn.axzo.foundation.dao.support.data.page.PageResp;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@UtilityClass
|
||||
public class PageConverter {
|
||||
|
||||
/**
|
||||
* 将bfs page转换为MybatisPlus的IPage
|
||||
* 支持根据entity上的字段来排序
|
||||
*
|
||||
* @param page
|
||||
* @param entityClz
|
||||
* @param <R>
|
||||
* @return
|
||||
*/
|
||||
public static <R> IPage<R> convertToMybatis(IPageReq page, Class<R> entityClz) {
|
||||
int pageSize = Math.min(Optional.ofNullable(page.getPageSize()).orElse(IPageReq.DEFAULT_PAGE_SIZE), IPageReq.MAX_PAGE_SIZE);
|
||||
Integer current = Optional.ofNullable(page.getPage()).orElse(IPageReq.DEFAULT_PAGE_NUMBER);
|
||||
|
||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<R> myBatisPage
|
||||
= new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(current, pageSize);
|
||||
List<OrderItem> orderItems = MybatisPlusConverterUtils.convertOrderItems(page.getSort(), entityClz);
|
||||
myBatisPage.setOrders(orderItems);
|
||||
|
||||
return myBatisPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将MybatisPlus的IPage转换为spring的Page, 用于返回
|
||||
*
|
||||
* @param page
|
||||
* @param <T>
|
||||
* @return
|
||||
*/
|
||||
public static <T> PageResp convertToBfs(IPage<T> page) {
|
||||
List<String> sorts = page.orders().stream()
|
||||
.map(e -> e.getColumn().concat(IPageReq.SORT_DELIMITER).concat(e.isAsc() ? IPageReq.SORT_ASC : IPageReq.SORT_DESC))
|
||||
.collect(Collectors.toList());
|
||||
PageResp result = PageResp.builder()
|
||||
.total(page.getTotal())
|
||||
.current(page.getCurrent())
|
||||
.size(page.getSize())
|
||||
.build();
|
||||
|
||||
result.setTotal(page.getTotal());
|
||||
result.setData(page.getRecords());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 bfs 的 page 请求,读取 mybatis 并将结果转换成 bfs 的 page
|
||||
* mybatisPage(page, p->xxxDao.selectPage(p, query));
|
||||
* XXX 针对排序字段作出优化,通过传入entityClz用于确定排序sql中的真实字段名
|
||||
*/
|
||||
public static <T, R> PageResp<R> mybatisPage(IPageReq page, Function<IPage, IPage> pageLoader, Class<T> entityClz) {
|
||||
final IPage<T> p = PageConverter.convertToMybatis(page, entityClz);
|
||||
IPage<T> iPage = pageLoader.apply(p);
|
||||
return PageConverter.convertToBfs(iPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将所有的数据通过page接口写入到list. 并返回
|
||||
* function中需要参数为新的pageNum, 默认从第一页开始加载. 直到返回的记录行数小于 预期的行数
|
||||
*/
|
||||
public static <T> List<T> drainAll(Function<Integer, PageResp<T>> function) {
|
||||
return drainAll(function, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将所有的数据通过page接口写入到list. 并返回
|
||||
* function中需要参数为新的pageNum, 默认从第一页开始加载. 直到返回的记录行数小于 预期的行数
|
||||
* breaker可以自行决定何时中断,允许为空,为空表示会拉取所有
|
||||
*/
|
||||
public static <T> List<T> drainAll(Function<Integer, PageResp<T>> function, Function<List<T>, Boolean> breaker) {
|
||||
List<T> totalData = Lists.newArrayList();
|
||||
int pageNum = IPageReq.DEFAULT_PAGE_NUMBER;
|
||||
while (true) {
|
||||
PageResp<T> result = function.apply(pageNum);
|
||||
totalData.addAll(result.getData());
|
||||
|
||||
if (result.getData().size() < result.getSize()) {
|
||||
break;
|
||||
}
|
||||
if (breaker != null && BooleanUtils.isTrue(breaker.apply(totalData))) {
|
||||
break;
|
||||
}
|
||||
pageNum += 1;
|
||||
}
|
||||
return totalData;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,276 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql;
|
||||
|
||||
import cn.axzo.foundation.dao.support.data.mysql.type.SetTypeHandler;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 一个简单工具类可以从测试环境中通过intellij工具导入数据成json字符串, 然后运行这个脚本导入到线上环境.
|
||||
* intellij中请使用JSON-groovy格式输出成json
|
||||
* 注意.导出的数据的字段是数据库的列名需要转换.
|
||||
* <pre>
|
||||
* [
|
||||
* {
|
||||
* "id": 2,
|
||||
* "app_ids": "9999",
|
||||
* "name": "集成测试告警",
|
||||
* "subject": "集成测试告警",
|
||||
* "priority": 98,
|
||||
* "recipients": "",
|
||||
* "send_threshold": 1,
|
||||
* "send_interval": 1,
|
||||
* "description": "集成测试告警",
|
||||
* "ext": "",
|
||||
* "status": "ENABLED",
|
||||
* "create_time": "2019-10-25 07:10:43",
|
||||
* "update_time": "2019-11-23 19:51:50"
|
||||
* }
|
||||
* ....
|
||||
* ]
|
||||
* </pre>
|
||||
* sample
|
||||
* <pre>
|
||||
* importHelper = JsonImportHelper.<AlertRuleDao, AlertRule>builder().baseMapper(alertRuleDao)
|
||||
* .bizKeyNames(List.of("name"))
|
||||
* .excludeFields(Set.of())
|
||||
* .clz(AlertRule.class)
|
||||
* .saveOrUpdate(null).build();
|
||||
* </pre>
|
||||
*
|
||||
* @param <M>
|
||||
* @param <T>
|
||||
*/
|
||||
public class JsonImportHelper<M extends BaseMapper<T>, T> {
|
||||
|
||||
private static final Set<String> DEFAULT_EXCLUDE_FIELDS = ImmutableSet.of("", "id", "rowid", "updatedTime", "createTime", "updateTime", "createTime", "modifyTime");
|
||||
private static final String DEFAULT_ID_FIELD_NAME = "id";
|
||||
private static final int MAX_IMPORT_COUNT = 1000;
|
||||
private boolean jsonSmart;
|
||||
|
||||
private String idFieldName;
|
||||
private HashSet<String> excludeFields;
|
||||
private M baseMapper;
|
||||
/**
|
||||
* 业务主键. 比如code, 唯一的名称...能唯一标识这条数据.
|
||||
*/
|
||||
private List<String> bizKeyNames;
|
||||
private Class<T> clz;
|
||||
private BiConsumer<T, Boolean> saveOrUpdate;
|
||||
|
||||
/**
|
||||
* @param baseMapper
|
||||
* @param excludeFields 排除的字段. 比如更新日期,创建日期,id. 默认已经集成.
|
||||
* @param bizKeyNames
|
||||
* @param clz
|
||||
* @param saveOrUpdate
|
||||
*/
|
||||
@Builder
|
||||
public JsonImportHelper(M baseMapper, Set<String> excludeFields, List<String> bizKeyNames, Class<T> clz,
|
||||
BiConsumer<T, Boolean> saveOrUpdate, String idFieldName) {
|
||||
Preconditions.checkArgument(!CollectionUtils.isEmpty(bizKeyNames));
|
||||
Preconditions.checkArgument(baseMapper != null);
|
||||
Preconditions.checkArgument(clz != null);
|
||||
|
||||
this.baseMapper = baseMapper;
|
||||
this.excludeFields = new HashSet<>(DEFAULT_EXCLUDE_FIELDS);
|
||||
if (!CollectionUtils.isEmpty(excludeFields)) {
|
||||
this.excludeFields.addAll(excludeFields);
|
||||
}
|
||||
this.bizKeyNames = bizKeyNames;
|
||||
this.clz = clz;
|
||||
this.saveOrUpdate = saveOrUpdate;
|
||||
this.jsonSmart = true;
|
||||
this.idFieldName = Strings.isNullOrEmpty(idFieldName) ? DEFAULT_ID_FIELD_NAME : idFieldName;
|
||||
}
|
||||
|
||||
public void setJsonSmart(boolean jsonSmart) {
|
||||
this.jsonSmart = jsonSmart;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行导入数据操作.返回需要更新, 查询, 没有变化的数据. 如果limit 是0 只是查看数据结果, 不发生操作.
|
||||
* 一次最多不超过200条数据
|
||||
*
|
||||
* @param rawJson
|
||||
* @param limit 指定需要更新的数据数量. 如果是0, 不会更新数据. 只返回结果
|
||||
* @return 返回更新数据列表.
|
||||
*/
|
||||
public ImportResp run(List<JSONObject> rawJson, int limit) {
|
||||
Preconditions.checkArgument(rawJson.size() <= MAX_IMPORT_COUNT);
|
||||
List<JSONObject> importRows = resolveRows(rawJson);
|
||||
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(clz);
|
||||
Map<String, String> columnMap = tableInfo.getFieldList().stream().collect(Collectors.toMap(e -> e.getColumn(), e -> e.getProperty()));
|
||||
// 主键没有columnMap中,需要显示声明.
|
||||
columnMap.put(tableInfo.getKeyColumn(), tableInfo.getKeyProperty());
|
||||
|
||||
ImmutableMap<String, TableFieldInfo> propertyMap = Maps.uniqueIndex(tableInfo.getFieldList(), TableFieldInfo::getProperty);
|
||||
QueryWrapper<T> query = new QueryWrapper<>();
|
||||
|
||||
//如果 bizKeyNames只有1个, 则转化 sql keyColumn in('xxx','xxx'...)
|
||||
if (bizKeyNames.size() == 1) {
|
||||
String bizKeyName = bizKeyNames.get(0);
|
||||
Set<Object> bizKeyValues = importRows.stream().map(e -> e.get(bizKeyName)).collect(Collectors.toSet());
|
||||
query.in(propertyMap.get(bizKeyName) != null && !bizKeyValues.isEmpty(), propertyMap.get(bizKeyName).getColumn(), bizKeyValues);
|
||||
} else {
|
||||
//需要插入更新的rows. 转化为 sql (keyColumn1 = 'xxx' and keyColumn2 = 'xxx') or (...)
|
||||
importRows.forEach(row -> query.or(subWrapper ->
|
||||
bizKeyNames.forEach(key -> subWrapper.eq(propertyMap.get(key) != null && row.get(key) != null,
|
||||
propertyMap.get(key).getColumn(), row.get(key)))));
|
||||
}
|
||||
|
||||
query.last("limit 2000");
|
||||
// 没有直接使用selectList处理json类型列有问题
|
||||
// baseMapper.selectMaps(query) 返回数据是db 列名, 需要转换
|
||||
List<JSONObject> dbRows = baseMapper.selectMaps(query).stream().map(e -> {
|
||||
final Map<String, Object> p = e.entrySet().stream()
|
||||
.filter(entry -> columnMap.containsKey(entry.getKey().toLowerCase()))
|
||||
.collect(Collectors.toMap(entry -> columnMap.get(entry.getKey().toLowerCase()), entry -> entry.getValue()));
|
||||
return new JSONObject(p);
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
ImportResp<T> diffRes = diff(dbRows, importRows);
|
||||
diffRes.insertRows.stream().limit(limit).forEach(e -> doSaveOrUpdate(e, true));
|
||||
diffRes.updateRows.stream().limit(limit).forEach(e -> doSaveOrUpdate(e, false));
|
||||
return diffRes;
|
||||
}
|
||||
|
||||
private void doSaveOrUpdate(T entity, boolean insert) {
|
||||
if (saveOrUpdate != null) {
|
||||
saveOrUpdate.accept(entity, insert);
|
||||
} else {
|
||||
if (insert) {
|
||||
baseMapper.insert(entity);
|
||||
} else {
|
||||
baseMapper.updateById(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解决数据库的列名到属性名的转换, 并过滤不存在和需要过滤的列
|
||||
*
|
||||
* @param rows
|
||||
* @return
|
||||
*/
|
||||
List<JSONObject> resolveRows(List<JSONObject> rows) {
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(clz);
|
||||
// 兼容客户端上传的数据是表的column名称(下划线), 或者是熟悉的名称(camel)
|
||||
ImmutableMap<String, TableFieldInfo> columnMap = Maps.uniqueIndex(tableInfo.getFieldList(), TableFieldInfo::getColumn);
|
||||
ImmutableMap<String, TableFieldInfo> propertyMap = Maps.uniqueIndex(tableInfo.getFieldList(), TableFieldInfo::getProperty);
|
||||
Map<String, TableFieldInfo> columnPropertyMap = new HashMap<>(columnMap);
|
||||
columnPropertyMap.putAll(propertyMap);
|
||||
|
||||
return rows.stream().map(e -> {
|
||||
Map<String, Object> collect = e.entrySet().stream().map(node -> {
|
||||
TableFieldInfo column = columnPropertyMap.get(node.getKey());
|
||||
if (column == null) {
|
||||
return Pair.of("", "");
|
||||
}
|
||||
Object nodeValue = node.getValue();
|
||||
if (jsonSmart && nodeValue != null && (node.getValue() instanceof String)) {
|
||||
String value = (String) (node.getValue());
|
||||
if (value.startsWith("{") && JSON.isValidObject(value)) {
|
||||
nodeValue = JSONObject.parseObject(value);
|
||||
}
|
||||
if (value.startsWith("[") && JSON.isValidArray(value)) {
|
||||
nodeValue = JSONObject.parseArray(value);
|
||||
}
|
||||
if (column.getTypeHandler() == SetTypeHandler.class) {
|
||||
nodeValue = ImmutableSet.copyOf(Splitter.on(",").omitEmptyStrings().trimResults().splitToList(value));
|
||||
}
|
||||
}
|
||||
return Pair.of(column.getProperty(), nodeValue);
|
||||
})
|
||||
.filter(x -> x.getValue() != null)
|
||||
.filter(x -> !excludeFields.contains(x.getKey()))
|
||||
.collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue()));
|
||||
return new JSONObject(collect);
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
ImportResp<T> diff(List<JSONObject> dbRows, List<JSONObject> importRows) {
|
||||
// 通过逻辑biz key来关联数据库和导入的数据, 得到需要需要创建和更新的数据.
|
||||
ImmutableMap<String, JSONObject> dbRowMap = Maps.uniqueIndex(dbRows, e -> {
|
||||
return bizKeyNames.stream().map(key -> e.getString(key)).collect(Collectors.joining(","));
|
||||
});
|
||||
|
||||
ImmutableMap<String, JSONObject> importRowMap = Maps.uniqueIndex(importRows, e -> {
|
||||
return bizKeyNames.stream().map(key -> e.getString(key)).collect(Collectors.joining(","));
|
||||
});
|
||||
ImportResp res = new ImportResp();
|
||||
for (Map.Entry<String, JSONObject> entry : importRowMap.entrySet()) {
|
||||
JSONObject dbRow = dbRowMap.get(entry.getKey());
|
||||
JSONObject importRow = entry.getValue();
|
||||
if (dbRow != null) {
|
||||
// 关联成功
|
||||
if (isSame(dbRow, importRow)) {
|
||||
res.sameRows.add(JSONObject.toJavaObject(importRow, clz));
|
||||
} else {
|
||||
// update the id by db row's
|
||||
JSONObject updated = new JSONObject(importRow);
|
||||
updated.put(this.idFieldName, dbRow.getOrDefault(this.idFieldName, null));
|
||||
res.updateRows.add(JSONObject.toJavaObject(updated, clz));
|
||||
}
|
||||
} else {
|
||||
// 没有关联上, 说明需要新增.
|
||||
res.insertRows.add(JSONObject.toJavaObject(importRow, clz));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将json对象串成一个字段串, 来比较2个json是否一致.
|
||||
*
|
||||
* @param src
|
||||
* @param target
|
||||
* @return
|
||||
*/
|
||||
boolean isSame(JSONObject src, JSONObject target) {
|
||||
Function<JSONObject, String> mixer = i -> {
|
||||
return i.entrySet().stream().filter(e -> !excludeFields.contains(e.getKey()))
|
||||
.sorted(Comparator.comparing(Map.Entry::getKey))
|
||||
.filter(e -> e.getValue() != null)
|
||||
.filter(e -> !Strings.isNullOrEmpty(e.getValue().toString()))
|
||||
.map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(","));
|
||||
};
|
||||
String srcText = mixer.apply(src);
|
||||
String targetText = mixer.apply(target);
|
||||
boolean res = srcText.equals(targetText);
|
||||
return res;
|
||||
}
|
||||
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public final static class ImportResp<T> {
|
||||
List<T> updateRows = Lists.newArrayList();
|
||||
List<T> insertRows = Lists.newArrayList();
|
||||
List<T> sameRows = Lists.newArrayList();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Maps;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
*通过basempper 读取 mysql 中数据,并缓存LoadingCache中
|
||||
* 提供了 2 中构建方式
|
||||
* <ul>
|
||||
* <li>
|
||||
* <p>IdBuilder</p><br/>
|
||||
* LoadingCache<Long, List<User>> cache = MybatisPlusCache.KeyBuilder.<User, Long>builder()
|
||||
* .expire(Duration.ofMinutes(5)).maxSize(100L).baseMapper(mapper)
|
||||
* .keyFunction(User::gtiId).build().toLoadingCache();
|
||||
* </li>
|
||||
* <li>
|
||||
* <p>AllBuilder</p><br/>
|
||||
* LoadingCache<Long, List<User>> cache = MybatisPlusCache.AllBuilder.<User, Long>builder()
|
||||
* .expire(Duration.ofMinutes(5)).maxSize(100L).baseMapper(mapper)
|
||||
* .queryBuilder(k -> new QueryWrapper<User>())
|
||||
* .entityFilter(e -> false)
|
||||
* .build()
|
||||
* .toLoadingCache();
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class MybatisPlusCacheHelper {
|
||||
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public static class KeyBuilder<T, K extends Serializable> {
|
||||
@NonNull
|
||||
private BaseMapper<T> baseMapper;
|
||||
@NonNull
|
||||
private Long maxSize;
|
||||
@NonNull
|
||||
private Duration expire;
|
||||
@NonNull
|
||||
SFunction<T, K> keyFunction;
|
||||
|
||||
public LoadingCache<K, Optional<T>> toLoadingCache() {
|
||||
return CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(expire)
|
||||
.maximumSize(maxSize)
|
||||
.recordStats()
|
||||
.build(new CacheLoader<K, Optional<T>>() {
|
||||
@Override
|
||||
public Optional<T> load(K key) throws Exception {
|
||||
return Optional.ofNullable(baseMapper.selectOne(Wrappers.<T>lambdaQuery().eq(keyFunction, key)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<K, Optional<T>> loadAll(Iterable<? extends K> keys) throws Exception {
|
||||
LambdaQueryWrapper<T> query = Wrappers.<T>lambdaQuery().in(keyFunction, ImmutableList.copyOf(keys));
|
||||
Map<K, T> rows = baseMapper.selectList(query).stream().collect(Collectors.toMap(keyFunction, e -> e));
|
||||
Map<K, Optional<T>> res = Maps.newHashMapWithExpectedSize(rows.size());
|
||||
for (K key : keys) {
|
||||
res.put(key, Optional.ofNullable(rows.get(key)));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public static class AllBuilder<T, K extends Serializable> {
|
||||
@NonNull
|
||||
private BaseMapper<T> baseMapper;
|
||||
@NonNull
|
||||
private Long maxSize;
|
||||
@NonNull
|
||||
private Duration expire;
|
||||
/**
|
||||
* 查询 QueryWrapper 构建器
|
||||
*/
|
||||
Function<K, Wrapper<T>> queryBuilder;
|
||||
|
||||
/**
|
||||
* 实体过滤器
|
||||
*/
|
||||
Predicate<T> entityFilter;
|
||||
|
||||
public <R> LoadingCache<K, List<R>> toLoadingCache(@NonNull Function<T, R> entityConverter) {
|
||||
return CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(expire)
|
||||
.maximumSize(maxSize)
|
||||
.recordStats()
|
||||
.build(new CacheLoader<K, List<R>>() {
|
||||
@Override
|
||||
public List<R> load(K key) throws Exception {
|
||||
Wrapper<T> query = null;
|
||||
if (queryBuilder != null) {
|
||||
query = queryBuilder.apply(key);
|
||||
}
|
||||
List<T> res = baseMapper.selectList(query);
|
||||
if (entityFilter != null) {
|
||||
res = res.stream().filter(entityFilter).collect(Collectors.toList());
|
||||
}
|
||||
return res.stream().map(e -> entityConverter.apply(e)).collect(Collectors.toList());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LoadingCache<K, List<T>> toLoadingCache() {
|
||||
return toLoadingCache(e->e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql;
|
||||
|
||||
import cn.axzo.foundation.dao.support.data.page.IPageReq;
|
||||
import cn.axzo.foundation.dao.support.data.wrapper.SimpleWrapperConverter;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MybatisPlusConverterUtils {
|
||||
|
||||
private static Map<Class, SimpleWrapperConverter<QueryWrapper>> converters = new ConcurrentHashMap<>(32);
|
||||
|
||||
public static SimpleWrapperConverter<QueryWrapper> getWrapperConverter(Class entityClass) {
|
||||
return converters.computeIfAbsent(entityClass, clazz ->
|
||||
SimpleWrapperConverter.<QueryWrapper>builder()
|
||||
.operatorProcessor(new MybatisPlusOperatorProcessor())
|
||||
.fieldColumnMap(getFieldMapping(clazz))
|
||||
.fieldTypeMap(getFieldTypeMapping(clazz))
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回class中property 对应的 column
|
||||
*
|
||||
* @param clazz
|
||||
* @return
|
||||
*/
|
||||
public static Map<String, String> getFieldMapping(Class clazz) {
|
||||
// XXX: TableInfoHelper.getTableInfo(clazz).getFieldList返回的映射关系是不包含@TableId注解(会导致根据id查询的列找不到。)
|
||||
// 在获取property和column映射关系的时候,需要聚合filedList和@TableId
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz);
|
||||
Map<String, String> fieldMap = tableInfo.getFieldList().stream()
|
||||
.collect(Collectors.toMap(TableFieldInfo::getProperty, TableFieldInfo::getColumn));
|
||||
if (!Strings.isNullOrEmpty(tableInfo.getKeyProperty())) {
|
||||
fieldMap.put(tableInfo.getKeyProperty(), tableInfo.getKeyColumn());
|
||||
}
|
||||
return fieldMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回class中property 对应的 propertyClass
|
||||
*
|
||||
* @param clazz
|
||||
* @return
|
||||
*/
|
||||
public static Map<String, Class<?>> getFieldTypeMapping(Class clazz) {
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz);
|
||||
Map<String, Class<?>> fieldTypeMap = tableInfo.getFieldList().stream()
|
||||
.collect(Collectors.toMap(TableFieldInfo::getProperty, TableFieldInfo::getPropertyType));
|
||||
|
||||
if (!Strings.isNullOrEmpty(tableInfo.getKeyProperty())) {
|
||||
fieldTypeMap.put(tableInfo.getKeyProperty(), tableInfo.getKeyType());
|
||||
}
|
||||
return fieldTypeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 id__ASC 格式转换为 orderItems, 用于排序
|
||||
*
|
||||
* @param sorts id__ASC, createTime__DESC
|
||||
* @param entityClz
|
||||
* @return orderItems[column, sort]
|
||||
*/
|
||||
public static List<OrderItem> convertOrderItems(List<String> sorts, Class entityClz) {
|
||||
if (CollectionUtils.isEmpty(sorts)) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
Map<String, String> fieldColumnMap = entityClz == null ? ImmutableMap.of() : MybatisPlusConverterUtils.getFieldMapping(entityClz);
|
||||
|
||||
List<OrderItem> orderItems = sorts.stream()
|
||||
.map(e -> {
|
||||
String property = StringUtils.substringBefore(e, IPageReq.SORT_DELIMITER);
|
||||
// 尝试把实体类上的字段转换为数据库column
|
||||
if (fieldColumnMap.containsKey(property)) {
|
||||
property = fieldColumnMap.get(property);
|
||||
}
|
||||
String direction = StringUtils.substringAfter(e, IPageReq.SORT_DELIMITER);
|
||||
if (direction != null && IPageReq.SORT_DESC.equals(direction)) {
|
||||
return OrderItem.desc(property);
|
||||
}
|
||||
return OrderItem.asc(property);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return orderItems;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql;
|
||||
|
||||
import cn.axzo.foundation.exception.Axssert;
|
||||
import cn.axzo.foundation.result.ResultCode;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author yuanyi
|
||||
* Created on 2020/9/27.
|
||||
*/
|
||||
public class MybatisPlusHelper {
|
||||
|
||||
private static final Integer LIMIT = 1_000;
|
||||
|
||||
public static <T> List<T> drainAll(BaseMapper<T> baseMapper, QueryWrapper<T> queryWrapper,
|
||||
SFunction<T, Long> idFunction) {
|
||||
return drainAll(baseMapper, queryWrapper::lambda, idFunction, LIMIT);
|
||||
}
|
||||
|
||||
public static <T> List<T> drainAll(BaseMapper<T> baseMapper, QueryWrapper<T> queryWrapper,
|
||||
SFunction<T, Long> idFunction, int limit) {
|
||||
return drainAll(baseMapper, queryWrapper::lambda, idFunction, limit);
|
||||
}
|
||||
|
||||
public static <T> List<T> drainAll(BaseMapper<T> baseMapper, Supplier<LambdaQueryWrapper<T>> wrapperSupplier,
|
||||
SFunction<T, Long> idFunction) {
|
||||
return drainAll(baseMapper, wrapperSupplier, idFunction, LIMIT);
|
||||
}
|
||||
|
||||
public static <T> List<T> drainAll(BaseMapper<T> baseMapper, Supplier<LambdaQueryWrapper<T>> wrapperSupplier,
|
||||
SFunction<T, Long> idFunction, int limit) {
|
||||
LambdaQueryWrapper<T> queryWrapper = wrapperSupplier.get();
|
||||
Preconditions.checkArgument(!StringUtils.containsIgnoreCase(queryWrapper.getSqlSegment(), "order by"),
|
||||
"queryWrapper不能含有order by");
|
||||
|
||||
LambdaQueryWrapper<T> nextQueryWrapper = wrapperSupplier.get();
|
||||
Preconditions.checkArgument(queryWrapper != nextQueryWrapper,
|
||||
"wrapperSupplier需要返回不同的LambdaQueryWrapper实例");
|
||||
|
||||
Function<Long, Wrapper<T>> wrapperFunc = startId -> wrapperSupplier.get()
|
||||
.gt(idFunction, startId)
|
||||
.orderByAsc(idFunction)
|
||||
.last(" limit " + limit);
|
||||
return drainAll(baseMapper, wrapperFunc, idFunction::apply, limit);
|
||||
}
|
||||
|
||||
private static <T> List<T> drainAll(BaseMapper<T> baseMapper, Function<Long, Wrapper<T>> wrapperFunc,
|
||||
Function<T, Long> startIdFunc, int limit) {
|
||||
List<T> totalData = Lists.newArrayList();
|
||||
Long startId = 0L;
|
||||
while (true) {
|
||||
List<T> records = baseMapper.selectList(wrapperFunc.apply(startId));
|
||||
totalData.addAll(records);
|
||||
|
||||
if (records.size() < limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
T lastOne = records.get(records.size() - 1);
|
||||
startId = startIdFunc.apply(lastOne);
|
||||
}
|
||||
return totalData;
|
||||
}
|
||||
|
||||
public static <T> int pageRunAll(BaseMapper<T> baseMapper, QueryWrapper<T> queryWrapper,
|
||||
SFunction<T, Long> idFunction, Consumer<List<T>> consumer) {
|
||||
return pageRunAll(baseMapper, queryWrapper::lambda, idFunction, LIMIT, consumer);
|
||||
}
|
||||
|
||||
public static <T> int pageRunAll(BaseMapper<T> baseMapper, QueryWrapper<T> queryWrapper,
|
||||
SFunction<T, Long> idFunction, int limit, Consumer<List<T>> consumer) {
|
||||
return pageRunAll(baseMapper, queryWrapper::lambda, idFunction, limit, consumer);
|
||||
}
|
||||
|
||||
public static <T> int pageRunAll(BaseMapper<T> baseMapper, Supplier<LambdaQueryWrapper<T>> wrapperSupplier,
|
||||
SFunction<T, Long> idFunction, Consumer<List<T>> consumer) {
|
||||
return pageRunAll(baseMapper, wrapperSupplier, idFunction, LIMIT, consumer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供分页处理能力
|
||||
*
|
||||
* @param consumer 对每一次page查询,进行批处理的consumer
|
||||
* @return 总共处理数量
|
||||
*/
|
||||
public static <T> int pageRunAll(BaseMapper<T> baseMapper, Supplier<LambdaQueryWrapper<T>> wrapperSupplier,
|
||||
SFunction<T, Long> idFunction, int limit, Consumer<List<T>> consumer) {
|
||||
LambdaQueryWrapper<T> queryWrapper = wrapperSupplier.get();
|
||||
Preconditions.checkArgument(!StringUtils.containsIgnoreCase(queryWrapper.getSqlSegment(), "order by"),
|
||||
"queryWrapper不能含有order by");
|
||||
|
||||
LambdaQueryWrapper<T> nextQueryWrapper = wrapperSupplier.get();
|
||||
Preconditions.checkArgument(queryWrapper != nextQueryWrapper,
|
||||
"wrapperSupplier需要返回不同的LambdaQueryWrapper实例");
|
||||
|
||||
Function<Long, Wrapper<T>> wrapperFunc = startId -> wrapperSupplier.get()
|
||||
.gt(idFunction, startId)
|
||||
.orderByAsc(idFunction)
|
||||
.last(" limit " + limit);
|
||||
return pageRunAll(baseMapper, wrapperFunc, idFunction::apply, limit, consumer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 entity 属性是否为null. ignoreFields指定不检查字段, 如id. modifier等
|
||||
*/
|
||||
public static boolean nonNullContent(Object entity, String... ignoreFields) {
|
||||
Axssert.checkNonNull(entity, ResultCode.INVALID_PARAMS, "entity不能为Null");
|
||||
Set<String> allFields = TableInfoHelper.getTableInfo(entity.getClass()).getFieldList().stream()
|
||||
.map(TableFieldInfo::getProperty).collect(Collectors.toSet());
|
||||
Axssert.check(!allFields.isEmpty(), ResultCode.INVALID_PARAMS, "非Mybatis对象");
|
||||
Set<String> ignoreCheckFields = Optional.ofNullable(ignoreFields)
|
||||
.map(e -> Arrays.stream(e).collect(Collectors.toSet())).orElse(ImmutableSet.of());
|
||||
|
||||
boolean changed = ((JSONObject) JSON.toJSON(entity)).entrySet().stream()
|
||||
.filter(e -> allFields.contains(e.getKey()) && !ignoreCheckFields.contains(e.getKey()))
|
||||
.anyMatch(e -> e.getValue() != null);
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static <T> int pageRunAll(BaseMapper<T> baseMapper, Function<Long, Wrapper<T>> wrapperFunc,
|
||||
Function<T, Long> startIdFunc, int limit, Consumer<List<T>> consumer) {
|
||||
Long startId = 0L;
|
||||
int total = 0;
|
||||
while (true) {
|
||||
List<T> records = baseMapper.selectList(wrapperFunc.apply(startId));
|
||||
if (records.isEmpty()) {
|
||||
return total;
|
||||
}
|
||||
consumer.accept(records);
|
||||
total = total + records.size();
|
||||
|
||||
if (records.size() < limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
T lastOne = records.get(records.size() - 1);
|
||||
startId = startIdFunc.apply(lastOne);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,249 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql;
|
||||
|
||||
import cn.axzo.foundation.dao.support.data.wrapper.CriteriaWrapper;
|
||||
import cn.axzo.foundation.dao.support.data.wrapper.Operator;
|
||||
import cn.axzo.foundation.dao.support.data.wrapper.OperatorProcessor;
|
||||
import cn.axzo.foundation.dao.support.data.wrapper.TriConsumer;
|
||||
import cn.axzo.foundation.exception.Axssert;
|
||||
import cn.axzo.foundation.exception.BusinessException;
|
||||
import cn.axzo.foundation.result.ResultCode;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class MybatisPlusOperatorProcessor<T> implements OperatorProcessor<QueryWrapper<T>> {
|
||||
private final static Pattern COLUMN_PATTERN = Pattern.compile("^\\w+$");
|
||||
|
||||
@Override
|
||||
public QueryWrapper<T> assembleAllQueryWrapper(ImmutableListMultimap<String, CriteriaWrapper.QueryField> queryColumnMap, boolean andOperator) {
|
||||
QueryWrapper<T> queryWrapper = Wrappers.query();
|
||||
queryColumnMap.asMap().forEach((column, queryFields) -> {
|
||||
// 获取processor,拼装wrapper
|
||||
if (andOperator) {
|
||||
queryFields.forEach(queryField -> get(queryField.getOperator())
|
||||
.accept(queryWrapper, queryField.getColumnWithPrefix(column), queryField.getValue()));
|
||||
} else {
|
||||
queryFields.forEach(queryField -> {
|
||||
get(queryField.getOperator())
|
||||
.accept(queryWrapper, queryField.getColumnWithPrefix(column), queryField.getValue());
|
||||
queryWrapper.or();
|
||||
});
|
||||
}
|
||||
});
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
public TriConsumer<QueryWrapper<T>, String, Object> get(Operator operator) {
|
||||
switch (operator) {
|
||||
case LIKE:
|
||||
return QueryWrapper::like;
|
||||
case EQ:
|
||||
return QueryWrapper::eq;
|
||||
case LT:
|
||||
return QueryWrapper::lt;
|
||||
case LE:
|
||||
return QueryWrapper::le;
|
||||
case GT:
|
||||
return QueryWrapper::gt;
|
||||
case GE:
|
||||
return QueryWrapper::ge;
|
||||
case NE:
|
||||
return QueryWrapper::ne;
|
||||
case SW:
|
||||
return QueryWrapper::likeRight;
|
||||
case EW:
|
||||
return QueryWrapper::likeLeft;
|
||||
case IN:
|
||||
return this::in;
|
||||
case IS_NULL:
|
||||
return this::isNull;
|
||||
case IS_NOT_NULL:
|
||||
return this::isNotNull;
|
||||
case BETWEEN:
|
||||
return this::between;
|
||||
case ORDER:
|
||||
return this::order;
|
||||
case OR:
|
||||
return this::or;
|
||||
case JSON:
|
||||
return this::json;
|
||||
case JSON_OR:
|
||||
return this::jsonOr;
|
||||
case JSON_QUERY:
|
||||
return this::jsonQuery;
|
||||
case NOT_IN:
|
||||
return this::notIn;
|
||||
case SELECT:
|
||||
return this::select;
|
||||
case FS:
|
||||
case CONTAIN_ALL:
|
||||
case NOT:
|
||||
throw new UnsupportedOperationException("暂不支持的操作符");
|
||||
|
||||
default:
|
||||
throw new BusinessException(ResultCode.INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
private void jsonQuery(QueryWrapper<T> tQueryWrapper, String column, Object value) {
|
||||
List<JSONQuery> valueList = (List) value;
|
||||
if (CollectionUtils.isEmpty(valueList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
valueList.forEach(v -> v.assembleQueryWrapper(tQueryWrapper, this, column));
|
||||
}
|
||||
|
||||
private void or(QueryWrapper<T> queryWrapper, String column, Object value) {
|
||||
List<CriteriaWrapper.QueryField> criterials = (List<CriteriaWrapper.QueryField>) value;
|
||||
queryWrapper.and(e -> {
|
||||
for (CriteriaWrapper.QueryField q : criterials) {
|
||||
get(q.getOperator()).accept(e, q.getFieldWithPrefix(), q.getValue());
|
||||
e.or();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void json(QueryWrapper<T> queryWrapper, String column, Object value) {
|
||||
List<CriteriaWrapper.QueryField> criterials = (List<CriteriaWrapper.QueryField>) value;
|
||||
queryWrapper.and(e -> {
|
||||
for (CriteriaWrapper.QueryField q : criterials) {
|
||||
get(q.getOperator()).accept(e, q.getFieldWithPrefix(), q.getValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void jsonOr(QueryWrapper<T> queryWrapper, String column, Object value) {
|
||||
List<CriteriaWrapper.QueryField> criterials = (List<CriteriaWrapper.QueryField>) value;
|
||||
queryWrapper.and(e -> {
|
||||
for (CriteriaWrapper.QueryField q : criterials) {
|
||||
get(q.getOperator()).accept(e, q.getFieldWithPrefix(), q.getValue());
|
||||
e.or();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void isNull(QueryWrapper<T> queryWrapper, String column, Object value) {
|
||||
queryWrapper.isNull(column);
|
||||
}
|
||||
|
||||
private void isNotNull(QueryWrapper<T> queryWrapper, String column, Object value) {
|
||||
queryWrapper.isNotNull(column);
|
||||
}
|
||||
|
||||
private void between(QueryWrapper queryWrapper, String column, Object value) {
|
||||
List valueList = (List) value;
|
||||
queryWrapper.between(column, valueList.get(0), valueList.get(1));
|
||||
}
|
||||
|
||||
private void in(QueryWrapper<T> queryWrapper, String column, Object v) {
|
||||
Collection values = (Collection) v;
|
||||
boolean isJson = CriteriaWrapper.QueryField.isJsonQueryField(column);
|
||||
if (isJson) {
|
||||
queryWrapper.and(w -> values.forEach(value -> w.or().apply(column + "={0}", value)));
|
||||
} else {
|
||||
queryWrapper.in(column, values);
|
||||
}
|
||||
}
|
||||
|
||||
private void notIn(QueryWrapper<T> queryWrapper, String column, Object v) {
|
||||
Collection values = (Collection) v;
|
||||
boolean isJson = CriteriaWrapper.QueryField.isJsonQueryField(column);
|
||||
if (isJson) {
|
||||
queryWrapper.and(w -> values.forEach(value -> w.apply(column + "!={0}", value)));
|
||||
} else {
|
||||
queryWrapper.notIn(column, values);
|
||||
}
|
||||
}
|
||||
|
||||
private void order(QueryWrapper<T> queryWrapper, String column, Object value) {
|
||||
// 如果value不能转成Direction,会抛异常
|
||||
if (Sort.Direction.fromString(String.valueOf(value)).isDescending()) {
|
||||
queryWrapper.orderByDesc(column);
|
||||
return;
|
||||
}
|
||||
|
||||
queryWrapper.orderByAsc(column);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持通过查询指定column, value传入map. 指定 distinct, sum等删除
|
||||
* eg: "id__SELECT": {"distinct": true, "function": "sum"}
|
||||
* 其他 select 只支持一个属性. 如果查询条件有多个. 只会保留最后一个
|
||||
*/
|
||||
private void select(QueryWrapper<T> queryWrapper, String column, Object v) {
|
||||
JSONObject conditions = JSONObject.parseObject(JSON.toJSONString(v));
|
||||
Boolean isDistinct = BooleanUtils.isTrue(conditions.getBoolean("distinct"));
|
||||
String selectColumn = String.format("%s%s", isDistinct ? "distinct " : StringUtils.EMPTY, column);
|
||||
String function = conditions.getString("function");
|
||||
if (!Strings.isNullOrEmpty(function)) {
|
||||
selectColumn = String.format("%s(%s)", function, selectColumn);
|
||||
}
|
||||
|
||||
//优先使用别名alias, 再使用as 以及当前的colum作为返回. 不推荐使用as
|
||||
String asColumn = StringUtils.firstNonBlank(conditions.getString("alias"),
|
||||
conditions.getString("as"),
|
||||
column);
|
||||
//校验只能是字母,数字,下划线. 避免sql注入
|
||||
boolean isValidColumn = COLUMN_PATTERN.matcher(asColumn).matches();
|
||||
Axssert.check(isValidColumn, ResultCode.INVALID_PARAMS, "别名{}不正确", asColumn);
|
||||
|
||||
queryWrapper.select(String.format("%s as `%s`", selectColumn, asColumn));
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class JSONQuery {
|
||||
private String jsonPath;
|
||||
private Object data;
|
||||
@Builder.Default
|
||||
private Operator operator = Operator.EQ;
|
||||
|
||||
public <T> void assembleQueryWrapper(QueryWrapper<T> queryWrapper, MybatisPlusOperatorProcessor<T> processor, String column) {
|
||||
// 特殊处理Boolean的EQ & NE查询。
|
||||
// 调试发现,json查询中,对Boolean类型的参数,在放到参数列表的时候没有问题,但查询结果不对。通过进一步调试发现,true/false在最终的查询语句中体现出来是 数字1/0
|
||||
// 问题的原因在于,jdbc驱动程序(mysql-connector-java),在PreparedStatement中,设置参数的时候 setBoolean会将 boolean 写为true=1或者false=0(有参数可配置,此为默认行为)
|
||||
// 参考:com.mysql.jdbc.PreparedStatement#setBoolean,/com/mysql/jdbc/PreparedStatement.java:2931
|
||||
if ((operator == Operator.EQ || operator == Operator.NE) && data instanceof Boolean) {
|
||||
String op = operator == Operator.EQ ? " = " : " <> ";
|
||||
queryWrapper.apply(MysqlJsonHelper.buildJsonField(column, jsonPath) + op + data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (operator != Operator.OR) {
|
||||
// json in. 5.7 mysql 不支持in操作,但是本类的 in() 和 notIn() 方法已经对此做了处理,可以直接使用
|
||||
processor.get(operator)
|
||||
.accept(queryWrapper, MysqlJsonHelper.buildJsonField(column, jsonPath), data);
|
||||
return;
|
||||
}
|
||||
// OR ,特殊处理
|
||||
List<JSONQuery> jsonQueries;
|
||||
try {
|
||||
jsonQueries = (List<JSONQuery>) JSONObject.parseArray(JSONObject.toJSONString(data)).toJavaList(JSONQuery.class);
|
||||
Axssert.checkNotEmpty(jsonQueries, "jsonQuery参数格式错误");
|
||||
} catch (Exception e) {
|
||||
throw ResultCode.INVALID_PARAMS.toException("jsonQuery参数格式错误");
|
||||
}
|
||||
queryWrapper.and(wrapper -> jsonQueries.forEach(jsonQuery ->
|
||||
wrapper.or(queryWrapper1 -> jsonQuery.assembleQueryWrapper(queryWrapper1, processor, column))));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 一个 mysql 的 json 操作 helper,方便构建 json 操作sql
|
||||
*/
|
||||
@UtilityClass
|
||||
public class MysqlJsonHelper {
|
||||
|
||||
/**
|
||||
* 构建 json 更新sql。支持嵌套内容的更新。
|
||||
* mysql 的 json_set 需要嵌套的路径中对象存在,否则更新失败。
|
||||
* https://stackoverflow.com/questions/40896920/mysql-json-set-cant-insert-into-column-with-null-value5-7
|
||||
* <p>
|
||||
* String sql = MysqlJsonHelper.buildJsonSetSql("ext",
|
||||
* ImmutableMap.of("test2.test4.test5", "100", "test2.test3", "55", "test2.test4.test6", "999"));
|
||||
* 产生结果:
|
||||
* ext = COALESCE(ext, JSON_OBJECT()),
|
||||
* ext = JSON_SET(ext, '$.test2', IFNULL(ext->'$.test2',JSON_OBJECT())),
|
||||
* ext = JSON_SET(ext, '$.test2.test4', IFNULL(ext->'$.test2.test4',JSON_OBJECT())),
|
||||
* ext = JSON_SET(ext, '$.test2.test4.test5', '100'),
|
||||
* ext = JSON_SET(ext, '$.test2.test3', '55'),
|
||||
* ext = JSON_SET(ext, '$.test2.test4.test6', '999')
|
||||
*
|
||||
* @return json_set sql 语句
|
||||
*/
|
||||
public String buildJsonSetSql(@NonNull String colName, @NonNull Map<String, Object> values) {
|
||||
Preconditions.checkArgument(!values.isEmpty());
|
||||
|
||||
return values.entrySet().stream().flatMap(e -> {
|
||||
List<String> nodes = Splitter.on(".").splitToList(e.getKey());
|
||||
List<String> sqls = Lists.newArrayList();
|
||||
String jsonPath = "$";
|
||||
// json_set 不支持上级节点是null,这里需要产生 sql 来初始化上级节点。
|
||||
sqls.add(String.format("%s = COALESCE(%s, JSON_OBJECT())", colName, colName));
|
||||
for (int i = 0; i < nodes.size() - 1; i++) {
|
||||
jsonPath = Joiner.on(".").join(jsonPath, nodes.get(i));
|
||||
String sql = String.format("%s = JSON_SET(%s, '%s', IFNULL(%s,JSON_OBJECT()))", colName, colName, jsonPath, buildJsonField(colName, jsonPath));
|
||||
sqls.add(sql);
|
||||
}
|
||||
String sqlValue = String.format("%s = JSON_SET(%s, '%s', %s)", colName, colName, "$." + e.getKey(),
|
||||
(e.getValue() instanceof String) ? "'" + e.getValue() + "'" : e.getValue());
|
||||
sqls.add(sqlValue);
|
||||
return sqls.stream();
|
||||
}).distinct().collect(Collectors.joining(",\n"));
|
||||
}
|
||||
|
||||
public String buildJsonField(@NonNull String colName, @NonNull String path) {
|
||||
Preconditions.checkArgument(StringUtils.isNotBlank(colName));
|
||||
Preconditions.checkArgument(StringUtils.isNotBlank(path));
|
||||
|
||||
return String.format("%s->'%s'", colName, (path.startsWith("$") ? path : "$." + path));
|
||||
}
|
||||
|
||||
/**
|
||||
* mysql 暂不支持JSON IN 查询
|
||||
* 这里把 in 转换成 or 查询。
|
||||
* //https://dev.mysql.com/doc/refman/5.7/en/json.html#json-comparison
|
||||
*/
|
||||
public <T> QueryWrapper<T> buildJsonIn(@NonNull QueryWrapper<T> wrapper, @NonNull String colName, @NonNull String path, @NonNull Collection values) {
|
||||
Preconditions.checkArgument(StringUtils.isNotBlank(colName));
|
||||
Preconditions.checkArgument(StringUtils.isNotBlank(path));
|
||||
|
||||
wrapper.and(w -> values.forEach(value -> {
|
||||
w.or().apply(buildJsonField(colName, path) + "={0}", value);
|
||||
}));
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* mysql 暂不支持JSON ARRAY 的 IN 查询
|
||||
* 这里把 in 转换成 or 查询。
|
||||
* //https://dev.mysql.com/doc/refman/5.7/en/json.html#json-comparison
|
||||
*/
|
||||
public <T> QueryWrapper<T> buildJsonContains(@NonNull QueryWrapper<T> wrapper, @NonNull String colName, @NonNull String path, @NonNull Collection values) {
|
||||
Preconditions.checkArgument(StringUtils.isNotBlank(colName));
|
||||
Preconditions.checkArgument(StringUtils.isNotBlank(path));
|
||||
|
||||
String normalizedPath = (path.startsWith("$") ? path : "$." + path);
|
||||
wrapper.and(w -> values.forEach(value -> {
|
||||
String sql = String.format("JSON_CONTAINS(%s, '%s', '%s')", colName,
|
||||
// XXX Long类型可能会被序列化为带引号的字符串(为了兼容前端),因此Long类型不使用json来序列化
|
||||
(value instanceof Long) ? value : JSONObject.toJSONString(value), normalizedPath);
|
||||
w.or().apply(sql);
|
||||
}));
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql;
|
||||
|
||||
import cn.axzo.foundation.dao.support.data.wrapper.CriteriaWrapper;
|
||||
import cn.axzo.foundation.dao.support.data.wrapper.SimpleWrapperConverter;
|
||||
import cn.axzo.foundation.exception.Axssert;
|
||||
import cn.axzo.foundation.result.ResultCode;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class QueryWrapperHelper {
|
||||
|
||||
public static <T> QueryWrapper<T> fromBean(Object bean, Class<T> clazz) {
|
||||
SimpleWrapperConverter<QueryWrapper> converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
|
||||
return converter.toWrapper(CriteriaWrapper.fromBean(bean));
|
||||
}
|
||||
|
||||
public static <T> QueryWrapper<T> fromMap(Map<String, Object> map, Class<T> clazz) {
|
||||
SimpleWrapperConverter<QueryWrapper> converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
|
||||
return converter.toWrapper(CriteriaWrapper.builder().queryField(map).build());
|
||||
}
|
||||
|
||||
public static <T> QueryWrapper<T> from(Object bean, Map<String, Object> map, Class<T> clazz) {
|
||||
SimpleWrapperConverter<QueryWrapper> converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
|
||||
Axssert.check(bean != null || !CollectionUtils.isEmpty(map), ResultCode.INVALID_PARAMS, "查询条件bean/map不能都为空");
|
||||
List<CriteriaWrapper> wrappers = Lists.newArrayList();
|
||||
if (bean != null) {
|
||||
wrappers.add(CriteriaWrapper.fromBean(bean));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(map)) {
|
||||
wrappers.add(CriteriaWrapper.builder().queryField(map).build());
|
||||
}
|
||||
return converter.toWrapper(wrappers);
|
||||
}
|
||||
|
||||
public static <T> QueryWrapper<T> fromBean(Object bean, Class<T> clazz,
|
||||
Function<CriteriaWrapper.QueryField, List<CriteriaWrapper.QueryField>> fieldConverter) {
|
||||
SimpleWrapperConverter<QueryWrapper> converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
|
||||
return converter.toWrapper(CriteriaWrapper.fromBean(bean, true, fieldConverter));
|
||||
}
|
||||
|
||||
public static <T> QueryWrapper<T> fromBean(Object bean, Class<T> clazz, Class... others) {
|
||||
SimpleWrapperConverter<QueryWrapper> converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
|
||||
return converter.toWrapper(CriteriaWrapper.fromBean(bean), Arrays.stream(others)
|
||||
.map(MybatisPlusConverterUtils::getWrapperConverter).toArray(SimpleWrapperConverter[]::new));
|
||||
}
|
||||
|
||||
public static <T> QueryWrapper<T> fromMap(Map<String, Object> map, Class<T> clazz, Class... others) {
|
||||
SimpleWrapperConverter<QueryWrapper> converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
|
||||
return converter.toWrapper(CriteriaWrapper.builder().queryField(map).build(), Arrays.stream(others)
|
||||
.map(MybatisPlusConverterUtils::getWrapperConverter).toArray(SimpleWrapperConverter[]::new));
|
||||
}
|
||||
|
||||
public static <T> QueryWrapper<T> fromBean(Object bean, Class<T> clazz,
|
||||
Function<CriteriaWrapper.QueryField,
|
||||
List<CriteriaWrapper.QueryField>> fieldConverter,
|
||||
Class... others) {
|
||||
SimpleWrapperConverter<QueryWrapper> converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
|
||||
return converter.toWrapper(CriteriaWrapper.fromBean(bean, true, fieldConverter), Arrays.stream(others)
|
||||
.map(MybatisPlusConverterUtils::getWrapperConverter).toArray(SimpleWrapperConverter[]::new));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql.plugins;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||
import com.google.common.base.Strings;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlCommandType;
|
||||
import org.apache.ibatis.plugin.*;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.apache.ibatis.reflection.SystemMetaObject;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
|
||||
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})})
|
||||
@Slf4j
|
||||
@Getter
|
||||
public class LimitInterceptor implements Interceptor {
|
||||
private Consumer<LimitContext> overflowConsumer;
|
||||
|
||||
private final static Pattern pattern = Pattern.compile(".+\\s+limit\\s+.+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
||||
//找一个距离1024最近的质数
|
||||
final static int DEFAULT_QUERY_LIMIT_SIZE = 1021;
|
||||
|
||||
@Builder
|
||||
public LimitInterceptor(Consumer<LimitContext> overflowConsumer) {
|
||||
this.overflowConsumer = overflowConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
|
||||
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
|
||||
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
|
||||
// 非查询,不处理
|
||||
if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
// sql语句中有limit的,不处理
|
||||
if (containsLimitSegment(metaObject)) {
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
//如果拦截query方法, 则检查查询结果是否击穿数据库
|
||||
//如果拦截prepare方法, 则检查是否需要增加limit
|
||||
if ("query".equals(invocation.getMethod().getName())) {
|
||||
return doAlertIntercept(invocation, metaObject);
|
||||
} else if ("prepare".equals(invocation.getMethod().getName())) {
|
||||
return doLimitIntercept(invocation, metaObject);
|
||||
} else {
|
||||
return invocation.proceed();
|
||||
}
|
||||
}
|
||||
|
||||
private Object doLimitIntercept(Invocation invocation, MetaObject metaObject) throws Throwable {
|
||||
RowBounds rowBounds = (RowBounds) metaObject.getValue("delegate.rowBounds");
|
||||
// 有RowBounds,且非默认RowBounds,不处理
|
||||
if (Objects.nonNull(rowBounds) && rowBounds != RowBounds.DEFAULT) {
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
metaObject.setValue("delegate.rowBounds", new RowBounds(0, DEFAULT_QUERY_LIMIT_SIZE));
|
||||
metaObject.setValue("delegate.resultSetHandler.rowBounds", new RowBounds(0, DEFAULT_QUERY_LIMIT_SIZE));
|
||||
return invocation.proceed();
|
||||
}
|
||||
|
||||
private Object doAlertIntercept(Invocation invocation, MetaObject metaObject) throws Throwable {
|
||||
Object result = invocation.proceed();
|
||||
if (result instanceof List) {
|
||||
int size = ((List) result).size();
|
||||
if (size == DEFAULT_QUERY_LIMIT_SIZE && Objects.nonNull(overflowConsumer)) {
|
||||
BoundSql boundSql = (BoundSql) metaObject.getValue("boundSql");
|
||||
overflowConsumer.accept(LimitContext.builder()
|
||||
.limitSize(DEFAULT_QUERY_LIMIT_SIZE)
|
||||
.sql(boundSql.getSql())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean containsLimitSegment(MetaObject metaObject) {
|
||||
BoundSql boundSql = (BoundSql) metaObject.getValue("boundSql");
|
||||
if (boundSql == null || Strings.isNullOrEmpty(boundSql.getSql())) {
|
||||
return false;
|
||||
}
|
||||
return pattern.matcher(boundSql.getSql()).matches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object plugin(Object target) {
|
||||
return target instanceof StatementHandler ? Plugin.wrap(target, this) : target;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public static class LimitContext {
|
||||
String sql;
|
||||
Integer limitSize;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql.plugins;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.mysql.cj.xdevapi.PreparableStatement;
|
||||
import com.zaxxer.hikari.pool.HikariProxyPreparedStatement;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.plugin.*;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.apache.ibatis.reflection.SystemMetaObject;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 满查询告警的拦截器. 如果sql执行时间超过限制. 则调用Consumer触发告警或日志
|
||||
*/
|
||||
@Intercepts({
|
||||
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
|
||||
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
|
||||
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
|
||||
})
|
||||
@Slf4j
|
||||
@Getter
|
||||
public class SlowQueryInterceptor implements Interceptor {
|
||||
private static final long DEFAULT_ALERT_THRESHOLD_MILLS = TimeUnit.SECONDS.toMillis(10);
|
||||
private Supplier<Long> alertThresholdSupplier;
|
||||
private Consumer<SlowQueryContext> slowQueryConsumer;
|
||||
|
||||
@Builder
|
||||
public SlowQueryInterceptor(Supplier<Long> alertThresholdSupplier, Consumer<SlowQueryContext> slowQueryConsumer) {
|
||||
this.slowQueryConsumer = slowQueryConsumer;
|
||||
this.alertThresholdSupplier = Optional.ofNullable(alertThresholdSupplier)
|
||||
.orElse(() -> DEFAULT_ALERT_THRESHOLD_MILLS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
Stopwatch stopwatch = Stopwatch.createStarted();
|
||||
Object proceed = invocation.proceed();
|
||||
long timeCost = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
Long threshold = alertThresholdSupplier.get();
|
||||
if (slowQueryConsumer != null && timeCost > threshold) {
|
||||
String sql = getSql(invocation);
|
||||
SlowQueryContext context = SlowQueryContext.builder()
|
||||
.sql(sql)
|
||||
.threshold(threshold)
|
||||
.timeCost(timeCost)
|
||||
.build();
|
||||
|
||||
log.warn("sql slow query, timeCost = {}, sql = {}", context.getTimeCost(), context.getSql());
|
||||
slowQueryConsumer.accept(context);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
log.warn("push slow query alert error, timeCost = {}", timeCost, exception);
|
||||
//ignore
|
||||
}
|
||||
return proceed;
|
||||
}
|
||||
|
||||
private String getSql(Invocation invocation) throws SQLException {
|
||||
Statement statement;
|
||||
Object firstArg = invocation.getArgs()[0];
|
||||
if (Proxy.isProxyClass(firstArg.getClass())) {
|
||||
statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
|
||||
} else {
|
||||
statement = (Statement) firstArg;
|
||||
}
|
||||
|
||||
//优先输出带参数的sql
|
||||
if (HikariProxyPreparedStatement.class.isAssignableFrom(statement.getClass())) {
|
||||
HikariProxyPreparedStatement preparedStatement = (HikariProxyPreparedStatement) statement;
|
||||
return preparedStatement.toString();
|
||||
}
|
||||
|
||||
//如果不是HikariProxyPreparedStatement的. 直接输出boundSql
|
||||
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
|
||||
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
|
||||
BoundSql boundSql = (BoundSql) metaObject.getValue("boundSql");
|
||||
return boundSql.getSql();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object plugin(Object target) {
|
||||
return target instanceof StatementHandler ? Plugin.wrap(target, this) : target;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public static class SlowQueryContext {
|
||||
@JSONField(ordinal = 20)
|
||||
String sql;
|
||||
@JSONField(ordinal = 10)
|
||||
Long timeCost;
|
||||
@JSONField(ordinal = 11)
|
||||
Long threshold;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql.type;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.serializer.SerializerFeature;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
@MappedTypes({List.class})
|
||||
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||
public abstract class BaseListTypeHandler<T> extends BaseTypeHandler<List<T>> {
|
||||
|
||||
private Class<T> type = getGenericType();
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement preparedStatement, int i,
|
||||
List<T> list, JdbcType jdbcType) throws SQLException {
|
||||
preparedStatement.setString(i, JSONArray.toJSONString(list, SerializerFeature.WriteMapNullValue,
|
||||
SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> getNullableResult(ResultSet resultSet, String s) throws SQLException {
|
||||
return JSONArray.parseArray(resultSet.getString(s), type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> getNullableResult(ResultSet resultSet, int i) throws SQLException {
|
||||
return JSONArray.parseArray(resultSet.getString(i), type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
|
||||
return JSONArray.parseArray(callableStatement.getString(i), type);
|
||||
}
|
||||
|
||||
private Class<T> getGenericType() {
|
||||
Type t = getClass().getGenericSuperclass();
|
||||
Type[] params = ((ParameterizedType) t).getActualTypeArguments();
|
||||
return (Class<T>) params[0];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql.type;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.serializer.SerializerFeature;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Set;
|
||||
|
||||
@MappedTypes({Set.class})
|
||||
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||
public abstract class BaseSetTypeHandler<T> extends BaseTypeHandler<Set<T>> {
|
||||
|
||||
private Class<T> type = getGenericType();
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement preparedStatement, int i,
|
||||
Set<T> set, JdbcType jdbcType) throws SQLException {
|
||||
preparedStatement.setString(i, JSONArray.toJSONString(Sets.newLinkedHashSet(set), SerializerFeature.WriteMapNullValue,
|
||||
SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<T> getNullableResult(ResultSet resultSet, String s) throws SQLException {
|
||||
return toLinkedHashSet(resultSet.getString(s));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<T> getNullableResult(ResultSet resultSet, int i) throws SQLException {
|
||||
return toLinkedHashSet(resultSet.getString(i));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<T> getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
|
||||
return toLinkedHashSet(callableStatement.getString(i));
|
||||
}
|
||||
|
||||
private Class<T> getGenericType() {
|
||||
Type t = getClass().getGenericSuperclass();
|
||||
Type[] params = ((ParameterizedType) t).getActualTypeArguments();
|
||||
return (Class<T>) params[0];
|
||||
}
|
||||
|
||||
private Set<T> toLinkedHashSet(String dbValue) {
|
||||
if (Strings.isNullOrEmpty(dbValue)) {
|
||||
return ImmutableSet.of();
|
||||
}
|
||||
return Sets.newLinkedHashSet(JSONArray.parseArray(dbValue, type));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql.type;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 1. 用于将数据库中的, 逗号分割的String, 转换为LinkedHashSet.
|
||||
* 2. 存储时将LinkedHashSet直接存String, 多个使用逗号分割
|
||||
*/
|
||||
@MappedJdbcTypes({JdbcType.VARCHAR})
|
||||
public class LinkedHashSetTypeHandler extends BaseTypeHandler<LinkedHashSet<String>> {
|
||||
|
||||
private static final String DELIMITER = ",";
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, LinkedHashSet<String> parameter, JdbcType jdbcType) throws SQLException {
|
||||
String value = Joiner.on(DELIMITER).join(Optional.ofNullable(parameter).orElseGet(Sets::newLinkedHashSet));
|
||||
ps.setString(i, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinkedHashSet<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
return getSet(rs.getString(columnName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinkedHashSet<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
return getSet(rs.getString(columnIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinkedHashSet<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
return getSet(cs.getString(columnIndex));
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> getSet(String dbValue) {
|
||||
return Sets.newLinkedHashSet(Splitter.on(DELIMITER)
|
||||
.omitEmptyStrings()
|
||||
.trimResults()
|
||||
.splitToList(Strings.nullToEmpty(dbValue)));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package cn.axzo.foundation.dao.support.data.mysql.type;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 1. 用于将数据库中的, 逗号分割的String, 转换为Set.
|
||||
* 2. 存储时将Set直接存String, 多个使用逗号分割
|
||||
*/
|
||||
@MappedJdbcTypes({JdbcType.VARCHAR})
|
||||
public class SetTypeHandler extends BaseTypeHandler<Set<String>> {
|
||||
|
||||
private static final String DELIMITER = ",";
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, Set<String> parameter, JdbcType jdbcType) throws SQLException {
|
||||
String value = Sets.newLinkedHashSet(Optional.ofNullable(parameter).orElse(Collections.emptySet()))
|
||||
.stream()
|
||||
.collect(Collectors.joining(DELIMITER));
|
||||
|
||||
ps.setString(i, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
return getSet(rs.getString(columnName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
return getSet(rs.getString(columnIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
return getSet(cs.getString(columnIndex));
|
||||
}
|
||||
|
||||
private Set<String> getSet(String dbValue) {
|
||||
return Splitter.on(",")
|
||||
.omitEmptyStrings()
|
||||
.trimResults()
|
||||
.splitToList(Optional.of(dbValue).orElse(StringUtils.EMPTY))
|
||||
.stream()
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package cn.axzo.foundation.dao.support.data.page;
|
||||
|
||||
import cn.axzo.foundation.enums.OrderEnum;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IPageReq {
|
||||
Integer DEFAULT_PAGE_NUMBER = 1;
|
||||
Integer DEFAULT_PAGE_SIZE = 20;
|
||||
Integer MAX_PAGE_SIZE = 1000;
|
||||
|
||||
String SORT_DELIMITER = "__";
|
||||
String SORT_DESC = OrderEnum.DESC.name();
|
||||
String SORT_ASC = OrderEnum.ASC.name();
|
||||
|
||||
default Integer getPage() {
|
||||
return DEFAULT_PAGE_NUMBER;
|
||||
}
|
||||
|
||||
default Integer getPageSize() {
|
||||
return DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
|
||||
default List<String> getSort() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,201 @@
|
||||
package cn.axzo.foundation.dao.support.data.utils;
|
||||
|
||||
import cn.axzo.foundation.enums.AppEnvEnum;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import lombok.NonNull;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 提供通用的 bit 位操作的接口。需要支持 bit 位的枚举实现该接口。参考如下:
|
||||
* <pre>
|
||||
* @Getter
|
||||
* @AllArgsConstructor
|
||||
* enum TestMask implements BitMask {
|
||||
* TEST1(1<<0),
|
||||
* TEST2(1<<1)
|
||||
* ;
|
||||
*
|
||||
* long mask;
|
||||
* @Override
|
||||
* public long getMask() {
|
||||
* return mask;
|
||||
* }
|
||||
* }
|
||||
* // feature 是存在 DB 中的值
|
||||
* long l = TestMask.TEST1.enableBit(feature);
|
||||
*
|
||||
* Set<BitMask> bitMasks = BitMask.enabledBitMasks(TestMask.values(), feature);
|
||||
* </pre>
|
||||
*/
|
||||
public interface BitMask {
|
||||
long getMask();
|
||||
|
||||
String name();
|
||||
|
||||
default long enableBit(Long value) {
|
||||
if (value == null) {
|
||||
return getMask();
|
||||
}
|
||||
return value | getMask();
|
||||
}
|
||||
|
||||
static long enableBits(Long value, BitMask... masks) {
|
||||
for (final BitMask mask : masks) {
|
||||
value = mask.enableBit(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static long enableBits(Long value, @NonNull Collection<? extends BitMask> masks) {
|
||||
return enableBits(value, masks.toArray(new BitMask[0]));
|
||||
}
|
||||
|
||||
default long disableBit(Long value) {
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
return value & ~getMask();
|
||||
}
|
||||
|
||||
static long disableBits(Long value, BitMask... masks) {
|
||||
for (final BitMask mask : masks) {
|
||||
value = mask.disableBit(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static long disableBits(Long value, @NonNull Collection<? extends BitMask> masks) {
|
||||
return disableBits(value, masks.toArray(new BitMask[0]));
|
||||
}
|
||||
|
||||
default long toggleBit(Long value, boolean enabled) {
|
||||
if (enabled) {
|
||||
return enableBit(value);
|
||||
} else {
|
||||
return disableBit(value);
|
||||
}
|
||||
}
|
||||
|
||||
static long toggle(Long value, BitMask k1, Boolean v1) {
|
||||
return toggle(value, ImmutableList.of(Pair.of(k1, v1)));
|
||||
}
|
||||
|
||||
static long toggle(Long value, BitMask k1, Boolean v1, BitMask k2, Boolean v2) {
|
||||
return toggle(value, ImmutableList.of(Pair.of(k1, v1), Pair.of(k2, v2)));
|
||||
}
|
||||
|
||||
static long toggle(Long value, BitMask k1, Boolean v1, BitMask k2, Boolean v2, BitMask k3, Boolean v3) {
|
||||
return toggle(value, ImmutableList.of(Pair.of(k1, v1), Pair.of(k2, v2), Pair.of(k3, v3)));
|
||||
}
|
||||
|
||||
static long toggle(Long value, BitMask k1, Boolean v1, BitMask k2, Boolean v2, BitMask k3, Boolean v3, BitMask k4, Boolean v4) {
|
||||
return toggle(value, ImmutableList.of(Pair.of(k1, v1), Pair.of(k2, v2), Pair.of(k3, v3), Pair.of(k4, v4)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据初始值以及 kv来返回最终的bitMask. usage : toggle(0L, Enum1, false, Enum2, true, Enum3, true)
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
static long toggle(Long value, BitMask k1, Boolean v1, BitMask k2, Boolean v2, BitMask k3, Boolean v3, BitMask k4, Boolean v4, BitMask k5, Boolean v5) {
|
||||
return toggle(value, ImmutableList.of(Pair.of(k1, v1), Pair.of(k2, v2), Pair.of(k3, v3), Pair.of(k4, v4), Pair.of(k5, v5)));
|
||||
}
|
||||
|
||||
static long toggle(Long value, List<Pair<BitMask, Boolean>> entries) {
|
||||
List<Pair<BitMask, Boolean>> entryList = entries.stream()
|
||||
.filter(p -> p.getKey() != null && p.getValue() != null)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long result = Optional.ofNullable(value).orElse(0L);
|
||||
for (Pair<BitMask, Boolean> entry : entryList) {
|
||||
result = entry.getValue() ? enableBits(result, entry.getKey()) : disableBits(result, entry.getKey());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
default boolean isEnabled(Long value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
return (value & getMask()) > 0;
|
||||
}
|
||||
|
||||
default boolean isDisabled(Long value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
return (value & getMask()) == 0;
|
||||
}
|
||||
|
||||
|
||||
default String buildSql(AppEnvEnum env, String columnName, boolean enabled) {
|
||||
return buildSql(env, columnName, enabled, this);
|
||||
}
|
||||
|
||||
default String buildConditionSql(AppEnvEnum env, String columnName, boolean enabled) {
|
||||
return buildConditionSql(env, columnName, enabled, this);
|
||||
}
|
||||
|
||||
static String buildSql(AppEnvEnum env, String columnName, boolean enabled, BitMask... masks) {
|
||||
if (env == AppEnvEnum.unittest) {
|
||||
long mask = Arrays.stream(masks).map(BitMask::getMask).reduce((x, y) -> x | y).orElse(0L);
|
||||
if (enabled) {
|
||||
return String.format("%s = bitor(%s, %s)", columnName, columnName, mask);
|
||||
} else {
|
||||
return String.format("%s = bitand(%s, bitnot(%s))", columnName, columnName, mask);
|
||||
}
|
||||
} else {
|
||||
long mask = Arrays.stream(masks).map(BitMask::getMask).reduce((x, y) -> x | y).orElse(0L);
|
||||
if (enabled) {
|
||||
return String.format("%s = (%s | %s)", columnName, columnName, mask);
|
||||
} else {
|
||||
return String.format("%s = (%s & ~%s)", columnName, columnName, mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String buildConditionSql(AppEnvEnum env, String columnName, boolean enabled, BitMask... masks) {
|
||||
if (env == AppEnvEnum.unittest) {
|
||||
long mask = Arrays.stream(masks).map(BitMask::getMask).reduce((x, y) -> x | y).orElse(0L);
|
||||
if (enabled) {
|
||||
return String.format("(bitand(%s, %s) > 0)", columnName, mask);
|
||||
} else {
|
||||
return String.format("(bitand(%s, %s) = 0)", columnName, mask);
|
||||
}
|
||||
} else {
|
||||
long mask = Arrays.stream(masks).map(BitMask::getMask).reduce((x, y) -> x | y).orElse(0L);
|
||||
if (enabled) {
|
||||
return String.format("((%s & %s) > 0)", columnName, mask);
|
||||
} else {
|
||||
return String.format("((%s & %s) = 0)", columnName, mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String buildConditionSqlAllMatch(AppEnvEnum env, String columnName, boolean enabled, BitMask... masks) {
|
||||
long mask = Arrays.stream(masks).map(BitMask::getMask).reduce((x, y) -> x | y).orElse(0L);
|
||||
if (env != AppEnvEnum.unittest) {
|
||||
if (enabled) {
|
||||
return String.format("((%s & %s) = %s)", columnName, mask, mask);
|
||||
} else {
|
||||
return String.format("((%s & %s) = 0)", columnName, mask);
|
||||
}
|
||||
} else {
|
||||
if (enabled) {
|
||||
return String.format("(bitand(%s, %s) = %s)", columnName, mask, mask);
|
||||
} else {
|
||||
return String.format("(bitand(%s, %s) = 0)", columnName, mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static <T extends BitMask> Set<T> getEnabledBitMasks(T[] masks, Long value) {
|
||||
if (value == null || value == 0) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return Arrays.stream(masks).filter(e -> e.isEnabled(value)).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,319 @@
|
||||
package cn.axzo.foundation.dao.support.data.utils;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import cn.axzo.foundation.dao.support.data.mysql.MybatisPlusConverterUtils;
|
||||
import cn.axzo.foundation.dao.support.data.wrapper.SimpleWrapperConverter;
|
||||
import com.google.common.base.Preconditions;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.springframework.cglib.beans.BeanMap;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 提供修复数据的工具类, 支持mysql. 修复方式为通过指定起始id批量查询并修复
|
||||
*
|
||||
* @param mapper {@link BaseMapper}对象
|
||||
* @param clz 实体类
|
||||
* @param updater 用于更新数据库中查询出来的记录
|
||||
* @param queryWrapper 可选. 查询数据库时指定的查询条件. 注意因为需要clone该wrapper, 要求其中的查询参数是serializable的,否则可能出错
|
||||
* @param selectFields 可选. 查询数据库时指定返回的字段
|
||||
* @param breaker 可选. 修复下一批次数据时检查breaker是否需要提前终止修复
|
||||
* @param transactionTemplate 可选. 当指定transactionTemplate时,将在事务中修复每批次数据
|
||||
*/
|
||||
@Slf4j
|
||||
public class RepairDataHelper<T> {
|
||||
|
||||
private RepositoryWrapper<T> repositoryWrapper;
|
||||
private Function<List<T>, List<T>> updater;
|
||||
private Supplier<Boolean> breaker;
|
||||
private TransactionTemplate transactionTemplate;
|
||||
|
||||
@Builder
|
||||
public RepairDataHelper(BaseMapper<T> mapper, Class<T> clz, Function<List<T>, List<T>> updater,
|
||||
QueryWrapper<T> queryWrapper, Set<String> selectFields, Supplier<Boolean> breaker,
|
||||
TransactionTemplate transactionTemplate) {
|
||||
Preconditions.checkArgument(mapper != null, "mapper不能为空");
|
||||
Preconditions.checkArgument(clz != null, "clz不能为空");
|
||||
Preconditions.checkArgument(updater != null, "updater不能为空");
|
||||
|
||||
if (queryWrapper == null) {
|
||||
queryWrapper = Wrappers.query();
|
||||
}
|
||||
repositoryWrapper = new MysqlRepositoryWrapper(mapper, clz, queryWrapper, selectFields);
|
||||
this.updater = updater;
|
||||
this.breaker = breaker;
|
||||
this.transactionTemplate = transactionTemplate;
|
||||
|
||||
log.info("---Repair Data Helper--- created with repository: {}, queryWrapper: {}, selectFields: {}",
|
||||
mapper, queryWrapper, selectFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据修复
|
||||
*
|
||||
* @param req startId 从指定的起始id开始修复数据. 默认从第一条记录开始
|
||||
* batchSize 一个批次处理的数据量. 默认一个批次仅处理20条数据
|
||||
* limit 必填项, 限制处理数据总条数.
|
||||
* onlyPrint true表示仅打印需要修复的数据, 不做修复动作. 默认不执行修复动作
|
||||
* @return 总的修复记录数
|
||||
*/
|
||||
public int repair(RepairReq req) {
|
||||
req = Optional.ofNullable(req).orElse(RepairReq.DEFAULT);
|
||||
Preconditions.checkArgument(req.getBatchSize() > 0, "batchCount必须大于0");
|
||||
Preconditions.checkArgument(req.getLimit() > 0, "limit必须大于0");
|
||||
|
||||
log.info("---Repair Data Helper--- repair with req: {}", req);
|
||||
|
||||
int batchIndex = 0;
|
||||
int totalProcessed = 0;
|
||||
int totalRepaired = 0;
|
||||
Serializable startId = req.getStartId();
|
||||
|
||||
while (totalProcessed < req.getLimit()) {
|
||||
boolean breakOut = breaker != null && BooleanUtils.isTrue(breaker.get());
|
||||
if (breakOut) {
|
||||
log.info("[{}]---Repair Data Helper--- break out, startId: {}", batchIndex, startId);
|
||||
break;
|
||||
}
|
||||
|
||||
log.info("[{}]---Repair Data Helper--- start process, startId: {}", batchIndex, startId);
|
||||
int batchSize = Math.min(req.getBatchSize(), req.getLimit() - totalProcessed);
|
||||
BatchRepairReq batchRepairReq = BatchRepairReq.builder()
|
||||
.batchIndex(batchIndex)
|
||||
.batchSize(batchSize)
|
||||
.startId(startId)
|
||||
.onlyPrint(req.getOnlyPrint())
|
||||
.build();
|
||||
BatchRepairResult batchRepairResult = Optional.ofNullable(transactionTemplate)
|
||||
.map(t -> t.execute(s -> batchRepair(batchRepairReq)))
|
||||
.orElseGet(() -> batchRepair(batchRepairReq));
|
||||
if (batchRepairResult.isBreakOut()) {
|
||||
break;
|
||||
}
|
||||
totalProcessed += batchRepairResult.getProcessedCount();
|
||||
totalRepaired += batchRepairResult.getRepairedCount();
|
||||
startId = batchRepairResult.getNextId();
|
||||
batchIndex += 1;
|
||||
}
|
||||
|
||||
log.info("---Repair Data Helper--- repair finished, req: {}, processed: {}, repaired: {}, ignored: {}",
|
||||
req, totalProcessed, totalRepaired, totalProcessed - totalRepaired);
|
||||
|
||||
return totalRepaired;
|
||||
}
|
||||
|
||||
private BatchRepairResult batchRepair(BatchRepairReq req) {
|
||||
int batchIndex = req.getBatchIndex();
|
||||
int batchSize = req.getBatchSize();
|
||||
Serializable startId = req.getStartId();
|
||||
boolean onlyPrint = req.isOnlyPrint();
|
||||
boolean includeStartId = batchIndex == 0;
|
||||
|
||||
List<T> selectedRecords = repositoryWrapper.selectByStartId(startId, batchSize, includeStartId);
|
||||
log.info("[{}]---Repair Data Helper--- selected records count: {}", batchIndex, selectedRecords.size());
|
||||
|
||||
if (CollectionUtils.isEmpty(selectedRecords)) {
|
||||
return BatchRepairResult.builder().breakOut(true).build();
|
||||
}
|
||||
|
||||
List<T> updateRecords = updater.apply(selectedRecords);
|
||||
log.info("[{}]---Repair Data Helper--- update records count: {}", batchIndex, updateRecords.size());
|
||||
|
||||
List<Object> updateIds = updateRecords.stream()
|
||||
.map(r -> getIdValue(r, repositoryWrapper.getIdFieldName()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
int updatedCount = 0;
|
||||
if (onlyPrint || CollectionUtils.isEmpty(updateIds)) {
|
||||
log.info("[{}]---Repair Data Helper--- to be updated ids: {}", batchIndex, updateIds);
|
||||
} else {
|
||||
if (needBatchUpdate(updateRecords)) {
|
||||
updatedCount = repositoryWrapper.batchUpdate(updateRecords);
|
||||
} else {
|
||||
// 所有待更新的数据都是相同的,可以通过条件更新一次性更新
|
||||
updatedCount = repositoryWrapper.updateByIds(updateRecords.get(0), updateIds);
|
||||
}
|
||||
|
||||
log.info("[{}]---Repair Data Helper--- updated records count: {}", batchIndex, updatedCount);
|
||||
}
|
||||
|
||||
Serializable nextId = getIdValue(selectedRecords.get(selectedRecords.size() - 1),
|
||||
repositoryWrapper.getIdFieldName());
|
||||
return BatchRepairResult.builder()
|
||||
.breakOut(false)
|
||||
.processedCount(selectedRecords.size())
|
||||
.repairedCount(updatedCount)
|
||||
.nextId(nextId)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
private static class BatchRepairReq {
|
||||
private int batchIndex;
|
||||
private int batchSize;
|
||||
private Serializable startId;
|
||||
private boolean onlyPrint;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
private static class BatchRepairResult {
|
||||
private boolean breakOut;
|
||||
private int processedCount;
|
||||
private int repairedCount;
|
||||
private Serializable nextId;
|
||||
}
|
||||
|
||||
private interface RepositoryWrapper<T> {
|
||||
String getIdFieldName();
|
||||
|
||||
List<T> selectByStartId(Serializable startId, int count, boolean includeStartId);
|
||||
|
||||
int updateByIds(T updateEntity, List<Object> ids);
|
||||
|
||||
int batchUpdate(List<T> updateEntities);
|
||||
}
|
||||
|
||||
private class MysqlRepositoryWrapper implements RepositoryWrapper<T> {
|
||||
private QueryWrapper<T> queryWrapper;
|
||||
private Set<String> selectFields;
|
||||
|
||||
private BaseMapper<T> baseMapper;
|
||||
private String idFieldName;
|
||||
private String idColumnName;
|
||||
|
||||
private SimpleWrapperConverter<QueryWrapper> converter;
|
||||
|
||||
private MysqlRepositoryWrapper(BaseMapper<T> baseMapper, Class<T> entityClass,
|
||||
QueryWrapper<T> queryWrapper, Set<String> selectFields) {
|
||||
try {
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
|
||||
idFieldName = tableInfo.getKeyProperty();
|
||||
idColumnName = tableInfo.getKeyColumn();
|
||||
converter = MybatisPlusConverterUtils.getWrapperConverter(entityClass);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
Preconditions.checkArgument(baseMapper != null, "repository找不到baseMapper");
|
||||
Preconditions.checkArgument(entityClass != null, "repository找不到entityClass");
|
||||
Preconditions.checkArgument(idFieldName != null, "repository找不到idFieldName");
|
||||
Preconditions.checkArgument(idColumnName != null, "repository找不到idColumnName");
|
||||
Preconditions.checkArgument(converter != null, "repository找不到converter");
|
||||
|
||||
this.baseMapper = baseMapper;
|
||||
this.queryWrapper = queryWrapper;
|
||||
this.selectFields = selectFields;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdFieldName() {
|
||||
return idFieldName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> selectByStartId(Serializable startId, int count, boolean includeStartId) {
|
||||
// 直接操作queryWrapper将带来副作用,需要将它clone出来
|
||||
QueryWrapper<T> clonedWrapper = queryWrapper.clone();
|
||||
|
||||
if (startId != null) {
|
||||
if (includeStartId) {
|
||||
clonedWrapper.ge(idColumnName, startId);
|
||||
} else {
|
||||
clonedWrapper.gt(idColumnName, startId);
|
||||
}
|
||||
}
|
||||
|
||||
clonedWrapper.orderByAsc(idColumnName).last("LIMIT " + count);
|
||||
|
||||
if (selectFields != null) {
|
||||
Set<String> columns = selectFields.stream()
|
||||
.map(converter::getColumnNotNull)
|
||||
.collect(Collectors.toSet());
|
||||
columns.add(idColumnName);
|
||||
// 注意wrapper的select()方法不支持重复调用,因此需要将所有查询字段放在一个数组里传入
|
||||
clonedWrapper.select(columns.toArray(new String[0]));
|
||||
}
|
||||
|
||||
return baseMapper.selectList(clonedWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateByIds(T updateEntity, List<Object> ids) {
|
||||
UpdateWrapper<T> updateWrapper = Wrappers.<T>update().in(idColumnName, ids);
|
||||
return baseMapper.update(updateEntity, updateWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int batchUpdate(List<T> updateEntities) {
|
||||
return updateEntities.stream().mapToInt(baseMapper::updateById).sum();
|
||||
}
|
||||
}
|
||||
|
||||
private Serializable getIdValue(Object entity, String idFieldName) {
|
||||
Map<String, Object> entityFields = BeanMap.create(entity);
|
||||
Serializable value = (Serializable) entityFields.get(idFieldName);
|
||||
Preconditions.checkState(value != null, "记录的id不能为空");
|
||||
return value;
|
||||
}
|
||||
|
||||
private boolean needBatchUpdate(List<T> records) {
|
||||
return records.stream()
|
||||
.map(record -> ((Map<String, Object>) BeanMap.create(record)).entrySet().stream()
|
||||
.filter(e -> !repositoryWrapper.getIdFieldName().equals(e.getKey()))
|
||||
.collect(Collectors.toList()))
|
||||
.distinct()
|
||||
.count() > 1;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class RepairReq {
|
||||
private String startId;
|
||||
private Integer batchSize;
|
||||
private Integer limit;
|
||||
private Boolean onlyPrint;
|
||||
|
||||
private static final RepairReq DEFAULT = RepairReq.builder()
|
||||
.batchSize(20)
|
||||
.limit(1)
|
||||
.onlyPrint(true)
|
||||
.build();
|
||||
|
||||
public Integer getBatchSize() {
|
||||
return Optional.ofNullable(batchSize).orElse(DEFAULT.batchSize);
|
||||
}
|
||||
|
||||
public Integer getLimit() {
|
||||
return Optional.ofNullable(limit).orElse(DEFAULT.limit);
|
||||
}
|
||||
|
||||
public Boolean getOnlyPrint() {
|
||||
return Optional.ofNullable(onlyPrint).orElse(DEFAULT.onlyPrint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package cn.axzo.foundation.dao.support.data.wrapper;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.FIELD})
|
||||
public @interface CriteriaField {
|
||||
/**
|
||||
* 对应Mysql Entity的字段名或者"Field__Operator"格式
|
||||
* @return String
|
||||
*/
|
||||
String field() default "";
|
||||
|
||||
Operator operator() default Operator.EQ;
|
||||
|
||||
/**
|
||||
* 默认为true,当value为空集合或空map的时候,自动过滤该查询条件.
|
||||
* 主要是考虑到调用方在用fastjson序列化的时候,会将null集合序列化为空集合
|
||||
* 所以默认将空集合过滤掉
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
boolean filterEmpty() default true;
|
||||
|
||||
/**
|
||||
* 默认为true,当value为null的时候,自动过滤该查询条件.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
boolean filterNull() default true;
|
||||
|
||||
/**
|
||||
* 默认为true,当value为""的时候,自动过滤该查询条件.
|
||||
* @return
|
||||
*/
|
||||
boolean filterBlank() default true;
|
||||
|
||||
/**
|
||||
* 是否忽略该字段的查询条件
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean ignore() default false;
|
||||
|
||||
/**
|
||||
* 字段名前缀, 在放入sql时允许指定一个前缀。如前缀'a',则转换后的字段为a.id
|
||||
* 用于联表查询,比如:
|
||||
* @Select("select bill.* from bms_plus_pay_bill as bill, bms_plus_pay_audit as audit ${ew.customSqlSegment} " +
|
||||
* "group by bill.id")
|
||||
* IPage<PayBill> pageWithQueryPayAudit(IPage<PayBill> page, @Param(Constants.WRAPPER) Wrapper wrapper);
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String prefix() default "";
|
||||
}
|
||||
@ -0,0 +1,408 @@
|
||||
package cn.axzo.foundation.dao.support.data.wrapper;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.springframework.cglib.beans.BeanMap;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class CriteriaWrapper {
|
||||
|
||||
private static final String DELIMITER = "__";
|
||||
|
||||
/**
|
||||
* 使用ConcurrentMap,HashMap线程不安全
|
||||
*/
|
||||
private static ConcurrentMap<Class, Map<String, CriteriaField>> criteriaFieldAnnotations = Maps.newConcurrentMap();
|
||||
|
||||
@Getter
|
||||
private Boolean andOperator = true;
|
||||
|
||||
@Getter
|
||||
private ImmutableListMultimap<String, QueryField> queryFieldMap;
|
||||
|
||||
public CriteriaWrapper() {
|
||||
queryFieldMap = ImmutableListMultimap.of();
|
||||
}
|
||||
|
||||
public CriteriaWrapper(ImmutableListMultimap<String, QueryField> queryFieldMap, boolean andOperator) {
|
||||
this.queryFieldMap = queryFieldMap;
|
||||
this.andOperator = andOperator;
|
||||
}
|
||||
|
||||
public static CriteriaWrapperBuilder builder() {
|
||||
return new CriteriaWrapperBuilder();
|
||||
}
|
||||
|
||||
public static class CriteriaWrapperBuilder {
|
||||
private List<QueryField> queryFields = Lists.newArrayList();
|
||||
private boolean andOperator = true;
|
||||
|
||||
/**
|
||||
* @param key java bean的字段名或者"Field__Operator"格式
|
||||
* @param value 除了IS_NULL, IS_NOT_NULL允许null值外,其他情况如果为null该查询条件会被过滤
|
||||
* @return CriteriaWrapperBuilder
|
||||
*/
|
||||
public CriteriaWrapperBuilder queryField(String key, Object value) {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(key));
|
||||
return queryField(key, QueryField.getDefaultOperator(key, value), value);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key java bean的字段名或者"Field__Operator"格式
|
||||
* @param defaultOperator Operator
|
||||
* @param value 除了IS_NULL, IS_NOT_NULL允许null值外,其他情况如果为null该查询条件会被过滤
|
||||
* @return CriteriaWrapperBuilder
|
||||
*/
|
||||
public CriteriaWrapperBuilder queryField(String key, Operator defaultOperator, Object value) {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(key));
|
||||
|
||||
QueryField queryField = QueryField.buildWithNullFilter(key, defaultOperator, value);
|
||||
if (queryField != null) {
|
||||
this.queryFields.add(queryField);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param condition false的时候,该查询条件会被过滤
|
||||
* @param key java bean的字段名或者"Field__Operator"格式
|
||||
* @param value 如果为null,该查询条件会被过滤
|
||||
* @return CriteriaWrapperBuilder
|
||||
*/
|
||||
public CriteriaWrapperBuilder queryField(boolean condition, String key, Object value) {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(key));
|
||||
return queryField(condition, key, QueryField.getDefaultOperator(key, value), value);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param condition false的时候,该查询条件会被过滤
|
||||
* @param key java bean的字段名或者"Field__Operator"格式
|
||||
* @param defaultOperator 默认的查询Operator
|
||||
* @param value 如果为null,该查询条件会被过滤
|
||||
* @return CriteriaWrapperBuilder
|
||||
*/
|
||||
public CriteriaWrapperBuilder queryField(boolean condition, String key, Operator defaultOperator, Object value) {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(key));
|
||||
return queryField(condition, key, defaultOperator, value, StringUtils.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param condition false的时候,该查询条件会被过滤
|
||||
* @param key java bean的字段名或者"Field__Operator"格式
|
||||
* @param defaultOperator 默认的查询Operator
|
||||
* @param value 如果为null,该查询条件会被过滤
|
||||
* @param prefix 字段名添加一个前缀
|
||||
* @return CriteriaWrapperBuilder
|
||||
*/
|
||||
public CriteriaWrapperBuilder queryField(boolean condition, String key, Operator defaultOperator,
|
||||
Object value, String prefix) {
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(key));
|
||||
|
||||
if (!condition) {
|
||||
return this;
|
||||
}
|
||||
|
||||
queryFields.add(QueryField.build(key, defaultOperator, value, prefix));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param params {key, value}, key支持"Field__Operator"格式;value如果为null,该查询条件会被过滤
|
||||
* @return CriteriaWrapperBuilder
|
||||
*/
|
||||
public CriteriaWrapperBuilder queryField(Map<String, Object> params) {
|
||||
return queryField(params, null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param params {key, value}, key支持"Field__Operator"格式;value如果为null,该查询条件会被过滤
|
||||
* @param converter 对querfield进行转换. 比如查询条件, 查询值.
|
||||
* @return CriteriaWrapperBuilder
|
||||
*/
|
||||
public CriteriaWrapperBuilder queryField(Map<String, Object> params, Function<QueryField, QueryField> converter) {
|
||||
List<QueryField> queryFields = params.entrySet().stream()
|
||||
.map(entry -> QueryField.build(entry.getKey(), entry.getValue()))
|
||||
.map(queryField -> converter == null ? queryField : converter.apply(queryField))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(queryField -> queryField.getValue() != null || queryField.getOperator().allowNullValue())
|
||||
.collect(Collectors.toList());
|
||||
this.queryFields.addAll(queryFields);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CriteriaWrapperBuilder queryFields(List<QueryField> queryFields) {
|
||||
this.queryFields.addAll(queryFields);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CriteriaWrapperBuilder andOperator(boolean andOperator) {
|
||||
this.andOperator = andOperator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CriteriaWrapper build() {
|
||||
ImmutableListMultimap<String, QueryField> multimap = queryFields.stream()
|
||||
.collect(ImmutableListMultimap.toImmutableListMultimap(QueryField::getField, Function.identity()));
|
||||
return new CriteriaWrapper(multimap, this.andOperator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从bean对象来构造一个CriteriaWrapper
|
||||
* 需要这个bean对象中查询的属性通过{@link CriteriaField}来声明
|
||||
* @param bean 查询条件的bean对象
|
||||
* @return 查询对象
|
||||
*/
|
||||
public static CriteriaWrapper fromBean(Object bean) {
|
||||
return fromBean(bean, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从bean对象来构造一个CriteriaWrapper
|
||||
* 需要这个bean对象中查询的属性通过{@link CriteriaField}来声明
|
||||
* @param bean 查询条件的bean对象
|
||||
* @param andOperator true查询条件是and操作, false查询条件or操作.
|
||||
* @return 查询对象
|
||||
*/
|
||||
public static CriteriaWrapper fromBean(Object bean, boolean andOperator) {
|
||||
return fromBean(bean, andOperator, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从bean对象来构造一个CriteriaWrapper
|
||||
* 需要这个bean对象中查询的属性通过{@link CriteriaField}来声明
|
||||
* @param bean 查询条件的bean对象
|
||||
* @param andOperator true查询条件是and操作, false查询条件or操作.
|
||||
* @return 查询对象
|
||||
*/
|
||||
public static CriteriaWrapper fromBean(Object bean, boolean andOperator,
|
||||
Function<QueryField, List<QueryField>> converter) {
|
||||
return fromBean(bean, andOperator, converter, ImmutableMap.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从bean对象来构造一个CriteriaWrapper
|
||||
* 需要这个bean对象中查询的属性通过{@link CriteriaField}来声明
|
||||
*
|
||||
* @param bean 查询条件的bean对象
|
||||
* @param andOperator true查询条件是and操作, false查询条件or操作.
|
||||
* @return 查询对象
|
||||
*/
|
||||
public static CriteriaWrapper fromBean(Object bean, boolean andOperator,
|
||||
Function<QueryField, List<QueryField>> converter, Map<String, Operator> defaultOperators) {
|
||||
Map<String, CriteriaField> annotations = getFieldAnnotations(bean.getClass());
|
||||
Map<String, Object> beanMap = (bean instanceof Map) ? (Map<String, Object>) bean : BeanMap.create(bean);
|
||||
List<QueryField> queryFields = beanMap.entrySet().stream()
|
||||
.map(entry -> QueryField.build(entry.getKey(), entry.getValue(), annotations,
|
||||
(defaultOperators != null ? defaultOperators.get(entry.getKey()): null)))
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(queryField -> converter == null ? Stream.of(queryField) : converter.apply(queryField).stream())
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return CriteriaWrapper.builder().queryFields(queryFields).andOperator(andOperator).build();
|
||||
}
|
||||
|
||||
|
||||
private static Map<String, CriteriaField> getFieldAnnotations(Class beanClazz) {
|
||||
return criteriaFieldAnnotations.computeIfAbsent(beanClazz, clazz ->
|
||||
FieldUtils.getFieldsListWithAnnotation(clazz, CriteriaField.class).stream()
|
||||
.collect(Collectors.toMap(Field::getName, field -> field.getAnnotation(CriteriaField.class))));
|
||||
}
|
||||
|
||||
@Builder(toBuilder = true)
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class QueryField {
|
||||
/**
|
||||
* json字段查询条件构造模板,example:content->'$.filed1'__LIKE
|
||||
*/
|
||||
private final static String JSON_QUERY_FILED_TEMPLATE = "%s->'$.%s'%s";
|
||||
|
||||
/**
|
||||
* json查询条件作为filed时的特殊操作符
|
||||
*/
|
||||
private final static String JSON_QUERY_FILED_FLAG = "->";
|
||||
/**
|
||||
* 逻辑控制字段名称前缀
|
||||
*/
|
||||
private final static String LOGICAL_CONTROL_FIELD_PREFIX = "$$";
|
||||
/**
|
||||
* java bean的property name
|
||||
*/
|
||||
private String field;
|
||||
/**
|
||||
* 查询的operator
|
||||
*/
|
||||
private Operator operator;
|
||||
/**
|
||||
* 查询的值
|
||||
*/
|
||||
private Object value;
|
||||
/**
|
||||
* 字段前缀名称
|
||||
*/
|
||||
private String prefix;
|
||||
|
||||
public boolean isLogicalControlField() {
|
||||
return operator == Operator.OR || operator == Operator.NOT || operator == Operator.AND;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否逻辑控制字段
|
||||
* @param fieldName
|
||||
* @return 比如 or,and
|
||||
*/
|
||||
public static boolean isLogicalControlField(String fieldName) {
|
||||
return fieldName.startsWith(LOGICAL_CONTROL_FIELD_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是json查询字段,fieldName包含"->"符号表示是json查询
|
||||
* @param fieldName
|
||||
* @return
|
||||
*/
|
||||
public static boolean isJsonQueryField(String fieldName) {
|
||||
return fieldName.contains(JSON_QUERY_FILED_FLAG);
|
||||
}
|
||||
|
||||
public String getFieldWithPrefix() {
|
||||
if (Strings.isNullOrEmpty(prefix)) {
|
||||
return field;
|
||||
}
|
||||
return prefix + '.' + field;
|
||||
}
|
||||
|
||||
public String getColumnWithPrefix(String column) {
|
||||
if (Strings.isNullOrEmpty(prefix)) {
|
||||
return column;
|
||||
}
|
||||
return prefix + '.' + column;
|
||||
}
|
||||
|
||||
private static QueryField build(String field, Object value, Map<String, CriteriaField> annotations, Operator defaultOperator) {
|
||||
CriteriaField fieldAnnotation = annotations.get(field);
|
||||
if (fieldAnnotation == null) {
|
||||
// 如果没有Annotation,默认会过滤Null
|
||||
return QueryField.buildWithNullFilter(field, (defaultOperator != null ? defaultOperator : getDefaultOperator(field, value)), value);
|
||||
}
|
||||
|
||||
if (fieldAnnotation.ignore()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fieldAnnotation.filterEmpty()
|
||||
&& (value instanceof Collection)
|
||||
&& CollectionUtils.isEmpty((Collection)value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fieldAnnotation.filterEmpty()
|
||||
&& (value instanceof Map)
|
||||
&& CollectionUtils.isEmpty((Map) value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fieldAnnotation.filterNull() && Objects.isNull(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fieldAnnotation.filterBlank()
|
||||
&& (value instanceof String)
|
||||
&& StringUtils.isBlank((String) value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String fieldName = Strings.isNullOrEmpty(fieldAnnotation.field()) ? field : fieldAnnotation.field();
|
||||
// XXX 注解中获取的operator如果为EQ,无法判断是用户手动设置为EQ还是使用的默认operator.EQ,这里暂时保持原状
|
||||
return build(fieldName, fieldAnnotation.operator(), value, fieldAnnotation.prefix());
|
||||
}
|
||||
|
||||
private static QueryField build(String key, Object value) {
|
||||
return QueryField.build(key, getDefaultOperator(key, value), value, StringUtils.EMPTY);
|
||||
}
|
||||
|
||||
private static QueryField build(String key, Operator defaultOperator, Object value, String prefix) {
|
||||
String field = StringUtils.substringBefore(key, DELIMITER);
|
||||
String expression = StringUtils.substringAfter(key, DELIMITER);
|
||||
Operator operator = Strings.isNullOrEmpty(expression) ? defaultOperator : Operator.valueOf(expression);
|
||||
if (operator == Operator.OR || operator == Operator.NOT || operator == Operator.AND) {
|
||||
List<QueryField> nestedFields = ((Map<String, Object>)value).entrySet()
|
||||
.stream().map(e -> build(e.getKey(), Operator.EQ, e.getValue(), prefix)).collect(Collectors.toList());
|
||||
// FIXME: 因为为了减少修改,这里修改了 field 的名称,通过名称将状态传递下去。
|
||||
return new QueryField(LOGICAL_CONTROL_FIELD_PREFIX + field, operator, nestedFields, prefix);
|
||||
}
|
||||
if (operator == Operator.JSON || operator == Operator.JSON_OR) {
|
||||
List<QueryField> nestedFields = ((Map<String, Object>)value).entrySet()
|
||||
.stream().map(e -> build(buildJsonQueryFieldKey(field, e.getKey()), Operator.EQ, e.getValue(), prefix)).collect(Collectors.toList());
|
||||
// FIXME: 因为为了减少修改,这里修改了 field 的名称,通过名称将状态传递下去。
|
||||
return new QueryField(LOGICAL_CONTROL_FIELD_PREFIX + field, operator, nestedFields, prefix);
|
||||
|
||||
}
|
||||
return new QueryField(field, operator, value, prefix);
|
||||
}
|
||||
|
||||
|
||||
private static QueryField buildWithNullFilter(String key, Operator defaultOperator, Object value) {
|
||||
QueryField queryField = build(key, defaultOperator, value, StringUtils.EMPTY);
|
||||
if (queryField.getValue() == null && !queryField.getOperator().allowNullValue()) {
|
||||
return null;
|
||||
}
|
||||
return queryField;
|
||||
}
|
||||
|
||||
private static Operator getDefaultOperator(String field, Object value) {
|
||||
if (!(value instanceof Collection)) {
|
||||
return Operator.EQ;
|
||||
}
|
||||
|
||||
// 当value instanceof Collection,检查值是否为空
|
||||
if (((Collection)value).isEmpty()) {
|
||||
log.error("value is collection and is empty, field={}", field);
|
||||
}
|
||||
return Operator.IN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造json查询条件key
|
||||
* 例如fieldName=content, jsonFieldName=text__LIKE,返回:content->'$.text'__LIKE
|
||||
*
|
||||
* @param fieldName 数据库字段名
|
||||
* @param jsonFieldName json内的字段名
|
||||
* @return
|
||||
*/
|
||||
private static String buildJsonQueryFieldKey(String fieldName, String jsonFieldName) {
|
||||
String jsonField = StringUtils.substringBefore(jsonFieldName, DELIMITER);
|
||||
String jsonExpression = StringUtils.substringAfter(jsonFieldName, DELIMITER);
|
||||
if (Strings.isNullOrEmpty(jsonExpression)) {
|
||||
return String.format(JSON_QUERY_FILED_TEMPLATE, fieldName, jsonField, "");
|
||||
}
|
||||
return String.format(JSON_QUERY_FILED_TEMPLATE, fieldName, jsonField, DELIMITER + jsonExpression);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package cn.axzo.foundation.dao.support.data.wrapper;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum Operator {
|
||||
IN,
|
||||
LIKE,
|
||||
EQ,
|
||||
NE,
|
||||
GT,
|
||||
GE,
|
||||
LT,
|
||||
LE,
|
||||
IS_NULL,
|
||||
IS_NOT_NULL,
|
||||
BETWEEN,
|
||||
/**
|
||||
* start with
|
||||
*/
|
||||
SW,
|
||||
/**
|
||||
* end with
|
||||
*/
|
||||
EW,
|
||||
ORDER,
|
||||
OR,
|
||||
AND,
|
||||
NOT,
|
||||
/**
|
||||
* 添加
|
||||
* FULL_SEARCH
|
||||
*/
|
||||
FS,
|
||||
/**
|
||||
* JSON解析,现只支持mysql json类型字段
|
||||
* 查询条件示例:{"name":"张三","time__GT":"1595235995326","carNo__LIKE":"川A"}
|
||||
*/
|
||||
JSON,
|
||||
JSON_OR,
|
||||
/**
|
||||
* <pre>
|
||||
* JSON
|
||||
* 服务端 controller 接收参数示例代码:
|
||||
*
|
||||
* @CriteriaField(field="bizData", operator = Operator.JSON_QUERY)
|
||||
* private List<JSONQuery> bizData;
|
||||
*
|
||||
*
|
||||
* 调用端 查询参数构建 代码示例:
|
||||
* List<JSONQuery> bizData = List.of(
|
||||
* JSONQuery.builder().jsonPath("$.city").data(List.of("chengdu","xian")).operator(Operator.IN).build(),
|
||||
* JSONQuery.builder().jsonPath("$.name").data("street1").operator(Operator.EQ).build()
|
||||
* )
|
||||
* Param.builder().bizData(bizData).build();
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
JSON_QUERY,
|
||||
|
||||
CONTAIN_ALL,
|
||||
NOT_IN,
|
||||
@Deprecated
|
||||
CUSTOM,
|
||||
/**
|
||||
* 支持通过查询指定column, value传入map. 指定 distinct, sum等删除
|
||||
* eg: "id__SELECT": {"distinct": true, "function": "sum", "as": "count"}
|
||||
* 其他 select 只支持一个属性. 如果查询条件有多个. 只会保留最后一个
|
||||
*/
|
||||
SELECT;
|
||||
|
||||
public boolean allowNullValue() {
|
||||
return this == IS_NULL || this == IS_NOT_NULL;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package cn.axzo.foundation.dao.support.data.wrapper;
|
||||
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
|
||||
/**
|
||||
* 封装底层的查询实现
|
||||
*
|
||||
*/
|
||||
public interface OperatorProcessor<T> {
|
||||
|
||||
/**
|
||||
* 组装所有查询条件
|
||||
*
|
||||
* @param queryColumnMap
|
||||
* @param andOperator
|
||||
* @return
|
||||
*/
|
||||
T assembleAllQueryWrapper(ImmutableListMultimap<String, CriteriaWrapper.QueryField> queryColumnMap, boolean andOperator);
|
||||
}
|
||||
@ -0,0 +1,159 @@
|
||||
package cn.axzo.foundation.dao.support.data.wrapper;
|
||||
|
||||
import cn.axzo.foundation.exception.BusinessException;
|
||||
import cn.axzo.foundation.result.ResultCode;
|
||||
import com.google.common.collect.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 将CriteriaWrapper转换为mysql的QueryWrapper
|
||||
*/
|
||||
@Slf4j
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public class SimpleWrapperConverter<T> {
|
||||
|
||||
private OperatorProcessor<T> operatorProcessor;
|
||||
/**
|
||||
* key = property value = column
|
||||
*/
|
||||
private Map<String, String> fieldColumnMap;
|
||||
/**
|
||||
* key = property value = property class
|
||||
*/
|
||||
private Map<String, Class<?>> fieldTypeMap;
|
||||
|
||||
/**
|
||||
* value转换. key1 = value原来的type, key2 = targetType, value = 转换func
|
||||
*/
|
||||
private static final Table<Class<?>, Class<?>, Function<Object, Object>> VALUE_CONVERTERS =
|
||||
ImmutableTable.<Class<?>, Class<?>, Function<Object, Object>>builder()
|
||||
.put(Long.class, LocalDateTime.class, o -> Instant.ofEpochMilli((Long) o).atZone(ZoneId.systemDefault()).toLocalDateTime())
|
||||
.put(Long.class, LocalDate.class, o -> Instant.ofEpochMilli((Long) o).atZone(ZoneId.systemDefault()).toLocalDate())
|
||||
.put(Long.class, Date.class, o -> new Date((Long) o))
|
||||
.build();
|
||||
|
||||
|
||||
public T toWrapper(CriteriaWrapper criteriaWrapper) {
|
||||
ListMultimap<String, CriteriaWrapper.QueryField> multimap = criteriaWrapper.getQueryFieldMap()
|
||||
.asMap().entrySet().stream()
|
||||
.collect(Multimaps.flatteningToMultimap(
|
||||
query -> getColumnNotNull(query.getKey()),
|
||||
query -> convertValue(query.getValue()).stream(),
|
||||
ArrayListMultimap::create));
|
||||
return operatorProcessor.assembleAllQueryWrapper(ImmutableListMultimap.copyOf(multimap), criteriaWrapper.getAndOperator());
|
||||
}
|
||||
|
||||
public T toWrapper(CriteriaWrapper criteriaWrapper, SimpleWrapperConverter... anotherConverters) {
|
||||
ListMultimap<String, CriteriaWrapper.QueryField> multimap = criteriaWrapper.getQueryFieldMap()
|
||||
.asMap().entrySet().stream()
|
||||
.collect(Multimaps.flatteningToMultimap(
|
||||
query -> getColumnByConverters(query.getKey(), anotherConverters),
|
||||
query -> convertValue(query.getValue()).stream(),
|
||||
ArrayListMultimap::create));
|
||||
return operatorProcessor.assembleAllQueryWrapper(ImmutableListMultimap.copyOf(multimap), criteriaWrapper.getAndOperator());
|
||||
}
|
||||
|
||||
public T toWrapper(List<CriteriaWrapper> criteriaWrappers) {
|
||||
ListMultimap<String, CriteriaWrapper.QueryField> multimap = criteriaWrappers.stream().
|
||||
flatMap(e -> e.getQueryFieldMap().asMap().entrySet().stream())
|
||||
.collect(Multimaps.flatteningToMultimap(
|
||||
query -> getColumnByConverters(query.getKey()),
|
||||
query -> convertValue(query.getValue()).stream(),
|
||||
ArrayListMultimap::create));
|
||||
return operatorProcessor.assembleAllQueryWrapper(ImmutableListMultimap.copyOf(multimap), true);
|
||||
}
|
||||
|
||||
public Optional<String> getColumn(String fieldName) {
|
||||
if (CriteriaWrapper.QueryField.isLogicalControlField(fieldName) ||
|
||||
CriteriaWrapper.QueryField.isJsonQueryField(fieldName)) {
|
||||
// 逻辑控制字段是虚拟字段,不需要查找;json查询字段是特殊语法,构造的字段不需要查找。
|
||||
return Optional.of(fieldName);
|
||||
}
|
||||
return Optional.ofNullable(fieldColumnMap.get(fieldName));
|
||||
}
|
||||
|
||||
public String getColumnNotNull(String fieldName) {
|
||||
return getColumn(fieldName).orElseThrow(() -> new BusinessException(ResultCode.INVALID_PARAMS));
|
||||
}
|
||||
|
||||
public String getColumnByConverters(String fieldName, SimpleWrapperConverter... anotherConverters) {
|
||||
// 优先查找当前class的字段
|
||||
Optional<String> column = getColumn(fieldName);
|
||||
if (column.isPresent()) {
|
||||
return column.get();
|
||||
}
|
||||
for (SimpleWrapperConverter anotherConverter : anotherConverters) {
|
||||
column = anotherConverter.getColumn(fieldName);
|
||||
if (column.isPresent()) {
|
||||
return column.get();
|
||||
}
|
||||
}
|
||||
throw new BusinessException(ResultCode.INVALID_PARAMS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换QueryField的value
|
||||
*
|
||||
* @param queryFields
|
||||
* @return
|
||||
*/
|
||||
private List<CriteriaWrapper.QueryField> convertValue(Collection<CriteriaWrapper.QueryField> queryFields) {
|
||||
return queryFields.stream()
|
||||
.map(queryField -> queryField.toBuilder().value(getConvertedValue(queryField)).build())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Object getConvertedValue(CriteriaWrapper.QueryField queryField) {
|
||||
if (queryField.isLogicalControlField()) {
|
||||
List<CriteriaWrapper.QueryField> subfields = (List<CriteriaWrapper.QueryField>) queryField.getValue();
|
||||
// resolve 嵌套字段的字段和值
|
||||
for (final CriteriaWrapper.QueryField subfield : subfields) {
|
||||
subfield.setField(getColumnNotNull(subfield.getField()));
|
||||
}
|
||||
}
|
||||
|
||||
if (queryField.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//找到属性对应的类型与当前value的类型. 根据类型找到convert. 有则处理. 没有则直接返回
|
||||
Class<?> fieldType = fieldTypeMap.get(queryField.getField());
|
||||
Class<?> valueType = queryField.getValue().getClass();
|
||||
|
||||
//如果sourceType
|
||||
if (Collection.class.isAssignableFrom(valueType)) {
|
||||
Collection collection = (Collection) queryField.getValue();
|
||||
return collection.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(e -> {
|
||||
Function<Object, Object> valueConverter = VALUE_CONVERTERS.get(e.getClass(), fieldType);
|
||||
return valueConverter == null ? e : valueConverter.apply(e);
|
||||
}).collect(getCollectionCollector(valueType));
|
||||
}
|
||||
|
||||
Function<Object, Object> valueConverter = VALUE_CONVERTERS.get(valueType, fieldType);
|
||||
return valueConverter == null ? queryField.getValue() : valueConverter.apply(queryField.getValue());
|
||||
}
|
||||
|
||||
private Collector getCollectionCollector(Class clazz) {
|
||||
if (List.class.isAssignableFrom(clazz)) {
|
||||
return Collectors.toList();
|
||||
}
|
||||
if (Set.class.isAssignableFrom(clazz)) {
|
||||
return Collectors.toSet();
|
||||
}
|
||||
throw new UnsupportedOperationException(String.format("unsupported collection type of %s", clazz.getSimpleName()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package cn.axzo.foundation.dao.support.data.wrapper;
|
||||
|
||||
/**
|
||||
* 仅包内可访问
|
||||
*
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TriConsumer<K, V, S> {
|
||||
/**
|
||||
* 三入参的consumer
|
||||
*
|
||||
* @param k
|
||||
* @param v
|
||||
* @param s
|
||||
*/
|
||||
void accept(K k, V v, S s);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user