queryWrapper, String column, Object value) {
+ // 如果value不能转成Direction,会抛异常
+ if (Sort.Direction.fromString(String.valueOf(value)).isDescending()) {
+ queryWrapper.orderByDesc(column);
+ return;
+ }
+
+ queryWrapper.orderByAsc(column);
+ }
+}
diff --git a/src/main/java/cn/axzo/pokonyan/dao/mysql/MysqlJsonHelper.java b/src/main/java/cn/axzo/pokonyan/dao/mysql/MysqlJsonHelper.java
new file mode 100644
index 0000000..12dee2a
--- /dev/null
+++ b/src/main/java/cn/axzo/pokonyan/dao/mysql/MysqlJsonHelper.java
@@ -0,0 +1,117 @@
+package cn.axzo.pokonyan.dao.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
+ *
+ * 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 values) {
+ Preconditions.checkArgument(!values.isEmpty());
+
+ return values.entrySet().stream().flatMap(e -> {
+ List nodes = Splitter.on(".").splitToList(e.getKey());
+ List 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(), resolveValue(e.getValue()));
+ sqls.add(sqlValue);
+ return sqls.stream();
+ }).distinct().collect(Collectors.joining(",\n"));
+ }
+
+ private Object resolveValue(Object value) {
+ if (value instanceof String) {
+ return "'" + value + "'";
+ }
+
+ if (value instanceof List) {
+ return "JSON_ARRAY(JSON_OBJECT" + Joiner.on(",JSON_OBJECT").join((List) value)
+ .replace("\":\"", "\",\"")
+ .replace("{", "(")
+ .replace("}", ")") + ")";
+ }
+ return value;
+ }
+
+ 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 QueryWrapper buildJsonIn(@NonNull QueryWrapper 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 QueryWrapper buildJsonContains(@NonNull QueryWrapper 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;
+ }
+}
diff --git a/src/main/java/cn/axzo/pokonyan/dao/mysql/QueryWrapperHelper.java b/src/main/java/cn/axzo/pokonyan/dao/mysql/QueryWrapperHelper.java
new file mode 100644
index 0000000..acd8902
--- /dev/null
+++ b/src/main/java/cn/axzo/pokonyan/dao/mysql/QueryWrapperHelper.java
@@ -0,0 +1,50 @@
+package cn.axzo.pokonyan.dao.mysql;
+
+import cn.axzo.pokonyan.dao.wrapper.CriteriaWrapper;
+import cn.axzo.pokonyan.dao.wrapper.SimpleWrapperConverter;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+public class QueryWrapperHelper {
+
+ public static QueryWrapper fromBean(Object bean, Class clazz) {
+ SimpleWrapperConverter converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
+ return converter.toWrapper(CriteriaWrapper.fromBean(bean));
+ }
+
+ public static QueryWrapper fromMap(Map map, Class clazz) {
+ SimpleWrapperConverter converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
+ return converter.toWrapper(CriteriaWrapper.builder().queryField(map).build());
+ }
+
+ public static QueryWrapper fromBean(Object bean, Class clazz,
+ Function> fieldConverter) {
+ SimpleWrapperConverter converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
+ return converter.toWrapper(CriteriaWrapper.fromBean(bean, true, fieldConverter));
+ }
+
+ public static QueryWrapper fromBean(Object bean, Class clazz, Class... others) {
+ SimpleWrapperConverter converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
+ return converter.toWrapper(CriteriaWrapper.fromBean(bean), Arrays.stream(others)
+ .map(MybatisPlusConverterUtils::getWrapperConverter).toArray(SimpleWrapperConverter[]::new));
+ }
+
+ public static QueryWrapper fromMap(Map map, Class clazz, Class... others) {
+ SimpleWrapperConverter converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
+ return converter.toWrapper(CriteriaWrapper.builder().queryField(map).build(), Arrays.stream(others)
+ .map(MybatisPlusConverterUtils::getWrapperConverter).toArray(SimpleWrapperConverter[]::new));
+ }
+
+ public static QueryWrapper fromBean(Object bean, Class clazz,
+ Function> fieldConverter,
+ Class... others) {
+ SimpleWrapperConverter converter = MybatisPlusConverterUtils.getWrapperConverter(clazz);
+ return converter.toWrapper(CriteriaWrapper.fromBean(bean, true, fieldConverter), Arrays.stream(others)
+ .map(MybatisPlusConverterUtils::getWrapperConverter).toArray(SimpleWrapperConverter[]::new));
+ }
+}
diff --git a/src/main/java/cn/axzo/pokonyan/dao/mysql/type/BaseListTypeHandler.java b/src/main/java/cn/axzo/pokonyan/dao/mysql/type/BaseListTypeHandler.java
new file mode 100644
index 0000000..a4a35ab
--- /dev/null
+++ b/src/main/java/cn/axzo/pokonyan/dao/mysql/type/BaseListTypeHandler.java
@@ -0,0 +1,51 @@
+package cn.axzo.pokonyan.dao.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 extends BaseTypeHandler> {
+
+ private Class type = getGenericType();
+
+ @Override
+ public void setNonNullParameter(PreparedStatement preparedStatement, int i,
+ List list, JdbcType jdbcType) throws SQLException {
+ preparedStatement.setString(i, JSONArray.toJSONString(list, SerializerFeature.WriteMapNullValue,
+ SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty));
+ }
+
+ @Override
+ public List getNullableResult(ResultSet resultSet, String s) throws SQLException {
+ return JSONArray.parseArray(resultSet.getString(s), type);
+ }
+
+ @Override
+ public List getNullableResult(ResultSet resultSet, int i) throws SQLException {
+ return JSONArray.parseArray(resultSet.getString(i), type);
+ }
+
+ @Override
+ public List getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
+ return JSONArray.parseArray(callableStatement.getString(i), type);
+ }
+
+ private Class getGenericType() {
+ Type t = getClass().getGenericSuperclass();
+ Type[] params = ((ParameterizedType) t).getActualTypeArguments();
+ return (Class) params[0];
+ }
+}
diff --git a/src/main/java/cn/axzo/pokonyan/dao/mysql/type/LinkedHashSetTypeHandler.java b/src/main/java/cn/axzo/pokonyan/dao/mysql/type/LinkedHashSetTypeHandler.java
new file mode 100644
index 0000000..51a2aae
--- /dev/null
+++ b/src/main/java/cn/axzo/pokonyan/dao/mysql/type/LinkedHashSetTypeHandler.java
@@ -0,0 +1,54 @@
+package cn.axzo.pokonyan.dao.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.Sets;
+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.LinkedHashSet;
+import java.util.Optional;
+
+/**
+ * 1. 用于将数据库中的, 逗号分割的String, 转换为LinkedHashSet.
+ * 2. 存储时将LinkedHashSet直接存String, 多个使用逗号分割
+ */
+@MappedJdbcTypes({JdbcType.VARCHAR})
+public class LinkedHashSetTypeHandler extends BaseTypeHandler> {
+
+ private static final String DELIMITER = ",";
+
+ @Override
+ public void setNonNullParameter(PreparedStatement ps, int i, LinkedHashSet parameter, JdbcType jdbcType) throws SQLException {
+ String value = Joiner.on(DELIMITER).join(Optional.ofNullable(parameter).orElseGet(Sets::newLinkedHashSet));
+ ps.setString(i, value);
+ }
+
+ @Override
+ public LinkedHashSet getNullableResult(ResultSet rs, String columnName) throws SQLException {
+ return getSet(rs.getString(columnName));
+ }
+
+ @Override
+ public LinkedHashSet getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+ return getSet(rs.getString(columnIndex));
+ }
+
+ @Override
+ public LinkedHashSet getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
+ return getSet(cs.getString(columnIndex));
+ }
+
+ private LinkedHashSet getSet(String dbValue) {
+ return Sets.newLinkedHashSet(Splitter.on(DELIMITER)
+ .omitEmptyStrings()
+ .trimResults()
+ .splitToList(Strings.nullToEmpty(dbValue)));
+ }
+}
diff --git a/src/main/java/cn/axzo/pokonyan/dao/mysql/type/SetTypeHandler.java b/src/main/java/cn/axzo/pokonyan/dao/mysql/type/SetTypeHandler.java
new file mode 100644
index 0000000..5bece50
--- /dev/null
+++ b/src/main/java/cn/axzo/pokonyan/dao/mysql/type/SetTypeHandler.java
@@ -0,0 +1,60 @@
+package cn.axzo.pokonyan.dao.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> {
+
+ private static final String DELIMITER = ",";
+
+ @Override
+ public void setNonNullParameter(PreparedStatement ps, int i, Set 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 getNullableResult(ResultSet rs, String columnName) throws SQLException {
+ return getSet(rs.getString(columnName));
+ }
+
+ @Override
+ public Set getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+ return getSet(rs.getString(columnIndex));
+ }
+
+ @Override
+ public Set getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
+ return getSet(cs.getString(columnIndex));
+ }
+
+ private Set getSet(String dbValue) {
+ return Splitter.on(",")
+ .omitEmptyStrings()
+ .trimResults()
+ .splitToList(Optional.of(dbValue).orElse(StringUtils.EMPTY))
+ .stream()
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/cn/axzo/pokonyan/dao/page/IPageParam.java b/src/main/java/cn/axzo/pokonyan/dao/page/IPageParam.java
new file mode 100644
index 0000000..eab36eb
--- /dev/null
+++ b/src/main/java/cn/axzo/pokonyan/dao/page/IPageParam.java
@@ -0,0 +1,37 @@
+package cn.axzo.pokonyan.dao.page;
+
+import com.google.common.collect.ImmutableList;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+public interface IPageParam {
+ 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 getPageNumber() {
+ return DEFAULT_PAGE_NUMBER;
+ }
+
+ default Integer getPageSize() {
+ return DEFAULT_PAGE_SIZE;
+ }
+
+ default List getSort() {
+ return ImmutableList.of();
+ }
+
+ @RequiredArgsConstructor
+ @Getter
+ public static enum OrderEnum {
+ DESC,
+ ASC;
+ }
+
+}
diff --git a/src/main/java/cn/axzo/pokonyan/dao/utils/RepairDataHelper.java b/src/main/java/cn/axzo/pokonyan/dao/utils/RepairDataHelper.java
new file mode 100644
index 0000000..af14cd6
--- /dev/null
+++ b/src/main/java/cn/axzo/pokonyan/dao/utils/RepairDataHelper.java
@@ -0,0 +1,255 @@
+package cn.axzo.pokonyan.dao.utils;
+
+import cn.axzo.pokonyan.dao.mysql.MybatisPlusConverterUtils;
+import cn.axzo.pokonyan.dao.wrapper.SimpleWrapperConverter;
+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 com.google.common.base.Preconditions;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cglib.beans.BeanMap;
+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.stream.Collectors;
+
+/**
+ * 提供修复数据的工具类, 支持mysql. 修复方式为通过指定起始id批量查询并修复
+ *
+ * @param mapper {@link BaseMapper}对象
+ * @param clz 实体类
+ * @param updater 用于更新数据库中查询出来的记录
+ * @param queryWrapper 可选. 查询数据库时指定的查询条件. 注意因为需要clone该wrapper, 要求其中的查询参数是serializable的,否则可能出错
+ * @param selectFields 可选. 查询数据库时指定返回的字段
+ */
+@Slf4j
+public class RepairDataHelper {
+
+ private RepositoryWrapper repositoryWrapper;
+ private Function, List> updater;
+
+ @Builder
+ public RepairDataHelper(BaseMapper mapper, Class clz, Function, List> updater,
+ QueryWrapper queryWrapper, Set selectFields) {
+ 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;
+
+ 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()) {
+ log.info("[{}]---Repair Data Helper--- start process, startId: {}", batchIndex, startId);
+
+ boolean includeStartId = batchIndex == 0;
+ int batchSize = Math.min(req.getBatchSize(), req.getLimit() - totalProcessed);
+ List selectedRecords = repositoryWrapper.selectByStartId(startId, batchSize, includeStartId);
+ log.info("[{}]---Repair Data Helper--- selected records count: {}", batchIndex, selectedRecords.size());
+
+ if (CollectionUtils.isEmpty(selectedRecords)) {
+ break;
+ }
+ totalProcessed += selectedRecords.size();
+
+ List updateRecords = updater.apply(selectedRecords);
+ log.info("[{}]---Repair Data Helper--- update records count: {}", batchIndex, updateRecords.size());
+
+ List