Bläddra i källkod

优化树形模型业务逻辑

woody 1 år sedan
förälder
incheckning
4b6e40d1d7

+ 23 - 4
framework-database/src/main/java/com/chelvc/framework/database/context/DatabaseContextHolder.java

@@ -7,6 +7,7 @@ import java.lang.reflect.Modifier;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.sql.Connection;
+import java.sql.SQLException;
 import java.sql.Statement;
 import java.time.Instant;
 import java.time.LocalDate;
@@ -19,6 +20,7 @@ import java.time.Year;
 import java.time.YearMonth;
 import java.time.ZonedDateTime;
 import java.time.chrono.JapaneseDate;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.LinkedList;
@@ -185,12 +187,29 @@ public final class DatabaseContextHolder {
      * 执行原生SQL
      *
      * @param sql SQL语句
-     * @return true/false
-     * @throws Exception 执行异常
+     * @throws SQLException SQL执行异常
      */
-    public static boolean execute(@NonNull String sql) throws Exception {
+    public static void execute(@NonNull String sql) throws SQLException {
         try (Statement statement = getConnection().createStatement()) {
-            return statement.execute(sql);
+            statement.execute(sql);
+        }
+    }
+
+    /**
+     * 批量执行原生SQL
+     *
+     * @param sqls SQL语句集合
+     * @throws SQLException SQL执行异常
+     */
+    public static void execute(@NonNull Collection<String> sqls) throws SQLException {
+        if (ObjectUtils.isEmpty(sqls)) {
+            return;
+        }
+        try (Statement statement = getConnection().createStatement()) {
+            for (String sql : sqls) {
+                statement.addBatch(sql);
+            }
+            statement.executeBatch();
         }
     }
 

+ 8 - 2
framework-database/src/main/java/com/chelvc/framework/database/entity/BasicTreeEntity.java

@@ -4,7 +4,7 @@ import java.io.Serializable;
 import java.util.List;
 
 import com.baomidou.mybatisplus.annotation.TableField;
-import com.chelvc.framework.common.model.Tree;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -27,12 +27,18 @@ import lombok.experimental.SuperBuilder;
 @ToString(callSuper = true)
 @EqualsAndHashCode(callSuper = true)
 public abstract class BasicTreeEntity<ID extends Serializable, T extends BasicTreeEntity<ID, T>>
-        extends BasicEntity<ID> implements Tree<ID, T> {
+        extends BasicEntity<ID> implements TreeEntity<ID, T> {
     /**
      * 上级主键
      */
     private ID parentId;
 
+    /**
+     * 节点序号
+     */
+    @JsonIgnore
+    private String sequence;
+
     /**
      * 子节点列表
      */

+ 8 - 2
framework-database/src/main/java/com/chelvc/framework/database/entity/ModifyTreeEntity.java

@@ -4,7 +4,7 @@ import java.io.Serializable;
 import java.util.List;
 
 import com.baomidou.mybatisplus.annotation.TableField;
-import com.chelvc.framework.common.model.Tree;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -27,12 +27,18 @@ import lombok.experimental.SuperBuilder;
 @ToString(callSuper = true)
 @EqualsAndHashCode(callSuper = true)
 public abstract class ModifyTreeEntity<ID extends Serializable, T extends ModifyTreeEntity<ID, T>>
-        extends ModifyEntity<ID> implements Tree<ID, T> {
+        extends ModifyEntity<ID> implements TreeEntity<ID, T> {
     /**
      * 上级主键
      */
     private ID parentId;
 
+    /**
+     * 节点序号
+     */
+    @JsonIgnore
+    private String sequence;
+
     /**
      * 子节点列表
      */

+ 14 - 0
framework-database/src/main/java/com/chelvc/framework/database/interceptor/JsonHandlerConfigureInterceptor.java

@@ -44,6 +44,7 @@ import javassist.CtClass;
 import javassist.CtConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.ibatis.parsing.XNode;
+import org.apache.ibatis.type.TypeAliasRegistry;
 import org.apache.ibatis.type.UnknownTypeHandler;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -66,6 +67,11 @@ public class JsonHandlerConfigureInterceptor extends MybatisConfigureInterceptor
      */
     private final Map<Type, Class<?>> handlers = Maps.newConcurrentMap();
 
+    /**
+     * 内置的别名/对象类型映射表
+     */
+    private final Map<String, Class<?>> aliases = new TypeAliasRegistry().getTypeAliases();
+
     /**
      * 判断是否是元类型
      *
@@ -222,6 +228,10 @@ public class JsonHandlerConfigureInterceptor extends MybatisConfigureInterceptor
     private Class<?> getSelectResultType(XNode node) {
         String type = node.getStringAttribute("resultType");
         if (StringUtils.notEmpty(type)) {
+            Class<?> clazz = this.aliases.get(type);
+            if (clazz != null) {
+                return clazz;
+            }
             try {
                 return Class.forName(type);
             } catch (ClassNotFoundException e) {
@@ -240,6 +250,10 @@ public class JsonHandlerConfigureInterceptor extends MybatisConfigureInterceptor
     private Class<?> getResultMappingType(XNode node) {
         String type = node.getStringAttribute("type");
         if (StringUtils.notEmpty(type)) {
+            Class<?> clazz = this.aliases.get(type);
+            if (clazz != null) {
+                return clazz;
+            }
             try {
                 return Class.forName(type);
             } catch (ClassNotFoundException e) {

+ 250 - 0
framework-database/src/main/java/com/chelvc/framework/database/support/DefaultTreeService.java

@@ -0,0 +1,250 @@
+package com.chelvc.framework.database.support;
+
+import java.io.Serializable;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.StringUtils;
+import com.chelvc.framework.common.util.TreeUtils;
+import com.chelvc.framework.database.context.DatabaseContextHolder;
+import com.chelvc.framework.database.entity.TreeEntity;
+import lombok.NonNull;
+
+/**
+ * 树形模型业务增强处理默认实现
+ *
+ * @author Woody
+ * @date 2024/4/28
+ */
+public class DefaultTreeService<M extends BaseMapper<T>, T extends TreeEntity<? extends Serializable, T>>
+        extends ServiceImpl<M, T> implements TreeService<T> {
+    /**
+     * 序号替换SQL
+     */
+    private static final String SEQUENCE_REPLACE_SQL =
+            "UPDATE `%s` SET `sequence` = REPLACE(`sequence`, '%s', '%s') WHERE `sequence` LIKE '%s'";
+
+    /**
+     * 初始化树形节点序号
+     *
+     * @param entity 树形对象实例
+     */
+    private void initializeSequence(T entity) {
+        T parent = ObjectUtils.ifNull(entity.getParentId(), this::getById);
+        if (parent == null || StringUtils.isEmpty(parent.getSequence())) {
+            entity.setSequence(entity.getId() + StringUtils.HORIZONTAL);
+        } else {
+            entity.setSequence(parent.getSequence() + entity.getId() + StringUtils.HORIZONTAL);
+        }
+    }
+
+    /**
+     * 初始化树形节点序号
+     *
+     * @param entities 树形对象实例集合
+     */
+    private void initializeSequence(Collection<T> entities) {
+        List<T> parents = ObjectUtils.ifEmpty(
+                entities.stream().map(TreeEntity::getParentId).filter(Objects::nonNull).collect(Collectors.toSet()),
+                this::listByIds
+        );
+        if (ObjectUtils.isEmpty(parents)) {
+            entities.forEach(entity -> entity.setSequence(entity.getId() + StringUtils.HORIZONTAL));
+        } else {
+            Map<Serializable, String> sequences =
+                    parents.stream().collect(Collectors.toMap(TreeEntity::getId, TreeEntity::getSequence));
+            entities.forEach(entity -> {
+                String sequence = sequences.get(entity.getParentId());
+                if (StringUtils.isEmpty(sequence)) {
+                    entity.setSequence(entity.getId() + StringUtils.HORIZONTAL);
+                } else {
+                    entity.setSequence(sequence + entity.getId() + StringUtils.HORIZONTAL);
+                }
+            });
+        }
+    }
+
+    /**
+     * 构建序号替换SQL
+     *
+     * @param original 原始序号
+     * @param current  当前序号
+     * @return SQL语句
+     */
+    private String buildSequenceReplaceSql(String original, String current) {
+        String table = TableInfoHelper.getTableInfo(this.getEntityClass()).getTableName();
+        return String.format(SEQUENCE_REPLACE_SQL, table, original, current, original + "%");
+    }
+
+    /**
+     * 构建树查询对象
+     *
+     * @return Lambda查询包装器实例
+     */
+    protected LambdaQueryChainWrapper<T> treeQuery() {
+        return this.lambdaQuery();
+    }
+
+    @Override
+    public T tree(@NonNull Serializable id) {
+        List<T> trees = TreeUtils.assemble(this.families(id));
+        return ObjectUtils.isEmpty(trees) ? null : trees.get(0);
+    }
+
+    @Override
+    public List<T> trees() {
+        List<T> nodes = this.treeQuery().list();
+        return TreeUtils.assemble(nodes);
+    }
+
+    @Override
+    public List<T> trees(@NonNull Collection<? extends Serializable> ids) {
+        if (ObjectUtils.isEmpty(ids)) {
+            return Collections.emptyList();
+        }
+        List<T> nodes = this.treeQuery().in(TreeEntity::getId, ids).list();
+        return TreeUtils.assemble(nodes);
+    }
+
+    @Override
+    public List<T> children(@NonNull Serializable id) {
+        return this.treeQuery().eq(TreeEntity::getParentId, id).list();
+    }
+
+    @Override
+    public List<T> children(@NonNull Collection<? extends Serializable> ids) {
+        if (ObjectUtils.isEmpty(ids)) {
+            return Collections.emptyList();
+        }
+        return this.treeQuery().in(TreeEntity::getParentId, ids).list();
+    }
+
+    @Override
+    public List<T> families(@NonNull Serializable id) {
+        T root = this.treeQuery().eq(TreeEntity::getId, id).one();
+        if (root == null) {
+            return Collections.emptyList();
+        } else if (StringUtils.isEmpty(root.getSequence())) {
+            return Collections.singletonList(root);
+        }
+        return this.treeQuery().likeRight(TreeEntity::getSequence, root.getSequence()).list();
+    }
+
+    @Override
+    public List<T> families(@NonNull Collection<? extends Serializable> ids) {
+        if (ObjectUtils.isEmpty(ids)) {
+            return Collections.emptyList();
+        }
+        List<T> nodes = this.treeQuery().in(TreeEntity::getId, ids).list();
+        if (ObjectUtils.isEmpty(nodes)) {
+            return Collections.emptyList();
+        }
+        Set<String> sequences = nodes.stream().map(TreeEntity::getSequence).filter(StringUtils::notEmpty)
+                .collect(Collectors.toSet());
+        if (ObjectUtils.notEmpty(sequences)) {
+            List<T> children = this.treeQuery().and(query -> {
+                // 重置查询实体对象,否则使用实体对象父类或接口函数查询会出现异常
+                query.setEntityClass(getEntityClass());
+
+                // 设置多个Like或查询条件
+                sequences.forEach(sequence -> query.or().likeRight(TreeEntity::getSequence, sequence));
+            }).list();
+            nodes.addAll(children);
+        }
+        return nodes;
+    }
+
+    @Override
+    public boolean save(@NonNull T entity) {
+        return DatabaseContextHolder.transactional((Supplier<Boolean>) () -> {
+            boolean success = super.save(entity);
+            if (success) {
+                // 更新树形节点序号
+                this.initializeSequence(entity);
+                super.updateById(entity);
+            }
+            return success;
+        });
+    }
+
+    @Override
+    public boolean saveBatch(@NonNull Collection<T> entities, int batchSize) {
+        if (ObjectUtils.isEmpty(entities)) {
+            return false;
+        }
+        return DatabaseContextHolder.transactional((Supplier<Boolean>) () -> {
+            boolean success = super.saveBatch(entities, batchSize);
+            if (success) {
+                // 更新树形节点序号
+                this.initializeSequence(entities);
+                super.updateBatchById(entities, batchSize);
+            }
+            return success;
+        });
+    }
+
+    @Override
+    public boolean updateById(@NonNull T entity) {
+        return DatabaseContextHolder.transactional((Supplier<Boolean>) () -> {
+            // 获取原始序号
+            T original = ObjectUtils.ifNull(entity.getId(), this::getById);
+            String sequence = ObjectUtils.ifNull(original, TreeEntity::getSequence);
+
+            // 更新树形节点序号
+            this.initializeSequence(entity);
+            boolean success = super.updateById(entity);
+            if (success && !Objects.equals(sequence, entity.getSequence())) {
+                String sql = this.buildSequenceReplaceSql(sequence, entity.getSequence());
+                try {
+                    DatabaseContextHolder.execute(sql);
+                } catch (SQLException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+            return success;
+        });
+    }
+
+    @Override
+    public boolean updateBatchById(@NonNull Collection<T> entities, int batchSize) {
+        if (ObjectUtils.isEmpty(entities)) {
+            return false;
+        }
+        return DatabaseContextHolder.transactional((Supplier<Boolean>) () -> {
+            List<T> originals = this.listByIds(entities.stream().map(TreeEntity::getId).collect(Collectors.toSet()));
+            Map<Serializable, String> sequences =
+                    originals.stream().collect(Collectors.toMap(TreeEntity::getId, TreeEntity::getSequence));
+            this.initializeSequence(entities);
+            boolean success = super.updateBatchById(entities, batchSize);
+            if (success) {
+                List<String> sqls = entities.stream().map(entity -> {
+                    String sequence = sequences.get(entity.getId());
+                    if (Objects.equals(sequence, entity.getSequence())) {
+                        return null;
+                    }
+                    return this.buildSequenceReplaceSql(sequence, entity.getSequence());
+                }).filter(Objects::nonNull).collect(Collectors.toList());
+                if (ObjectUtils.notEmpty(sqls)) {
+                    try {
+                        DatabaseContextHolder.execute(sqls);
+                    } catch (SQLException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+            }
+            return success;
+        });
+    }
+}

+ 10 - 9
framework-database/src/main/java/com/chelvc/framework/database/support/EnhanceService.java

@@ -25,13 +25,14 @@ import lombok.NonNull;
  */
 public interface EnhanceService<T> extends IService<T> {
     /**
-     * 更新实体(自动绑定更新人及更新时间)
+     * 根据ID修改实体
      *
-     * @param entity 实体对象实例
+     * @param entity    实体对象实例
+     * @param modifying 是否需要更新修改信息
      * @return true/false
      */
-    default boolean modify(@NonNull T entity) {
-        if (entity instanceof Updatable) {
+    default boolean updateById(@NonNull T entity, boolean modifying) {
+        if (modifying && entity instanceof Updatable) {
             Updatable<?> updatable = (Updatable<?>) entity;
             updatable.setUpdater(SessionContextHolder.getId());
             updatable.setUpdateTime(new Date());
@@ -40,16 +41,16 @@ public interface EnhanceService<T> extends IService<T> {
     }
 
     /**
-     * 批量更新实体(自动绑定更新人及更新时间)
+     * 批量根据ID修改实体
      *
-     * @param entities 实体对象实例集合
+     * @param entities  实体对象实例集合
+     * @param modifying 是否需要更新修改信息
      * @return true/false
      */
-    default boolean batchModify(@NonNull Collection<T> entities) {
+    default boolean updateBatchById(@NonNull Collection<T> entities, boolean modifying) {
         if (ObjectUtils.isEmpty(entities)) {
             return false;
-        }
-        if (Updatable.class.isAssignableFrom(this.getEntityClass())) {
+        } else if (modifying && Updatable.class.isAssignableFrom(this.getEntityClass())) {
             Date now = new Date();
             Long operator = SessionContextHolder.getId();
             entities.forEach(entity -> {

+ 88 - 0
framework-database/src/main/java/com/chelvc/framework/database/support/TreeService.java

@@ -0,0 +1,88 @@
+package com.chelvc.framework.database.support;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.common.util.TreeUtils;
+import com.chelvc.framework.database.entity.TreeEntity;
+import lombok.NonNull;
+
+/**
+ * 树形模型业务增强处理接口
+ *
+ * @author Woody
+ * @date 2024/4/29
+ */
+public interface TreeService<T extends TreeEntity<? extends Serializable, T>> extends EnhanceService<T> {
+    /**
+     * 根据ID获取整颗树
+     *
+     * @param id 数据ID
+     * @return 树实例
+     */
+    T tree(Serializable id);
+
+    /**
+     * 获取所有树
+     *
+     * @return 树列表
+     */
+    List<T> trees();
+
+    /**
+     * 根据ID获取对应树列表
+     *
+     * @param ids 数据ID集合
+     * @return 树列表
+     */
+    List<T> trees(Collection<? extends Serializable> ids);
+
+    /**
+     * 根据ID获取对应树列表
+     *
+     * @param ids      数据ID集合
+     * @param drilling 是否钻取子节点
+     * @return 树列表
+     */
+    default List<T> trees(@NonNull Collection<? extends Serializable> ids, boolean drilling) {
+        if (ObjectUtils.isEmpty(ids)) {
+            return Collections.emptyList();
+        }
+        return drilling ? TreeUtils.assemble(this.families(ids)) : this.trees(ids);
+    }
+
+    /**
+     * 根据ID获取子节点
+     *
+     * @param id 数据ID
+     * @return 子节点列表
+     */
+    List<T> children(Serializable id);
+
+    /**
+     * 批量根据ID获取子节点
+     *
+     * @param ids 数据ID集合
+     * @return 子节点列表
+     */
+    List<T> children(Collection<? extends Serializable> ids);
+
+    /**
+     * 根据ID获取整个家族节点
+     *
+     * @param id 数据ID
+     * @return 树节点列表
+     */
+    List<T> families(Serializable id);
+
+    /**
+     * 批量根据ID获取整个家族节点
+     *
+     * @param ids 数据ID集合
+     * @return 树节点列表
+     */
+    List<T> families(Collection<? extends Serializable> ids);
+}