Bootstrap

[笔记] 使用 AOP 切面 与 mybatis-plus 实现 自定义排序注解(修改一个排序值,其他排序值也改变)

在许多业务场景中,我们需要对数据进行排序,尤其是在列表展示和排序管理中。传统的排序方式通常是在数据库查询时通过 SQL 语句实现,但这对于复杂的排序逻辑(如修改一个排序值时,其他排序值也需要相应调整)显得不够灵活。本文介绍如何使用 AOP 切面与 MyBatis-Plus 结合,实现自定义排序注解,以简化排序逻辑的管理。

也可以不用 mybatis-plus 用其他技术去实现也可以例如 Jdbc Template,AnyLine MDM 等 .

注意

一定要使用 @Transactional 开启事务管理

一. 使用示例

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二. 实现代码

1. 枚举

/**
 * 重新排序操作类型
 *
 * @author 鲁子狄
 **/
@Getter
@AllArgsConstructor
public enum ReorderOrderOperaEnum {
    /**
     * 新增操作
     */
    INSERT("insert"),
    /**
     * 更新操作
     */
    UPDATE("update"),
    /**
     * 删除操作
     */
    DELETE("delete"),
    /**
     * 默认操作
     */
    DEFAULT("default");

    private final String value;
}

2. 自定义注解

/**
 * 重新排序注解
 *
 * @author 鲁子狄
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ReorderOrder {
    /**
     * 操作类型
     */
    ReorderOrderOperaEnum opera() ;

    /**
     * mapper class
     */
    Class<? extends BaseMapperPlus> baseMapper() ;

    /**
     * 重新排序字段
     */
    String orderField() default "order_num";

    /**
     * 父级字段
     */
    String parentField() default "parent_id";

    /**
     * 主键字段
     */
    String idField() default "id";

    /**
     * 逻辑删除字段
     */
    String deletedField() default "del_flag";

    /**
     * 逻辑删除字段值
     */
    int deletedValue() default 2;
}

3. 切面逻辑

/**
 * 修改排序字段切面
 *
 * @author 鲁子狄
 **/
@Slf4j
@Aspect
@Component
@SuppressWarnings("unchecked")
public class ReorderOrderAspect {

    // 非静态变量,用于在切面中存储和管理状态信息
    private ReorderOrderOperaEnum operationType;
    private BaseMapperPlus<?, ?> baseMapper;
    private Class<?> entityType;
    private String orderField;
    private String parentField;
    private String idField;
    private String deletedField;
    private Integer deletedValue;
    private String entityParentId;
    private String entityOrderField;

    /**
     * 向前移动策略
     */
    private final OrderChangeStrategy MOVE_FORWARD = (updateWrapper, newOrder, currentOrder) ->
        updateWrapper.ge(orderField, newOrder).lt(orderField, currentOrder);
    /**
     * 向后移动策略
     */
    private final OrderChangeStrategy MOVE_BACKWARD = (updateWrapper, newOrder, currentOrder) ->
        updateWrapper.gt(orderField, currentOrder).le(orderField, newOrder);

    /**
     * 策略接口
     */
    @FunctionalInterface
    interface OrderChangeStrategy {
        void apply(UpdateWrapper<?> updateWrapper, int newOrder, int currentOrder);
    }

    /**
     * 环绕拦截
     *
     * @param joinPoint    切点
     * @param reorderOrder 注解
     * @return Object
     * @throws Throwable 错误
     */
    @Around("@annotation(reorderOrder)")
    public Object doAround(ProceedingJoinPoint joinPoint, ReorderOrder reorderOrder) throws Throwable {

        // 设置变量值
        setVariableValue(reorderOrder);

        // 根据业务类型处理不同的操作
        return switch (operationType) {
            case INSERT ->
                // 插入操作
                handleInsert(joinPoint);
            case UPDATE ->
                // 更新操作
                handleUpdate(joinPoint);
            case DELETE ->
                // 删除操作
                handleDelete(joinPoint);
            default ->
                // 默认情况下执行原方法
                joinPoint.proceed();
        };
    }

    /**
     * 设置变量值
     *
     * @param reorderOrder 注解
     */
    private void setVariableValue(ReorderOrder reorderOrder) {
        operationType = reorderOrder.opera();
        baseMapper = SpringUtil.getBean(reorderOrder.baseMapper());
        entityType = baseMapper.currentModelClass();
        orderField = reorderOrder.orderField();
        parentField = reorderOrder.parentField();
        idField = reorderOrder.idField();
        deletedField = reorderOrder.deletedField();
        deletedValue = reorderOrder.deletedValue();
        entityParentId = StringUtils.toCamelCase(reorderOrder.parentField());
        entityOrderField = StringUtils.toCamelCase(reorderOrder.orderField());
    }

    /**
     * handleInsert 处理插入操作
     *
     * @param joinPoint 切点
     */
    private Object handleInsert(ProceedingJoinPoint joinPoint) throws Throwable {
        // 执行原方法
        Object result = joinPoint.proceed();
        if (result == null) {
            return null;
        }
        // 获取方法参数
        Object arg = joinPoint.getArgs()[0];
        Object parentId = getField(arg, entityParentId);
        // 获取新的排序值
        Integer newOrder = getField(arg, entityOrderField);
        // 获取最大排序值
        Integer maxOrderNum = getMaxOrCurrentOrderNum(parentId, null);
        if (newOrder != null && newOrder <= maxOrderNum) {
            // 确定排序变化方向
            int orderChange = determineOrderChange(newOrder, null);
            // 构建更新包装器
            UpdateWrapper updateWrapper = buildUpdateWrapper(null, newOrder, orderChange, parentId, (Long) result);
            // 执行更新
            baseMapper.update(updateWrapper);
        }
        return result;
    }

    /**
     * handleUpdate 处理更新操作
     *
     * @param joinPoint 切点
     */
    private Object handleUpdate(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法参数
        Object arg = joinPoint.getArgs()[0];
        // 获取父ID
        Object parentId = getField(arg, entityParentId);
        // 获取新的排序值
        Integer newOrder = getField(arg, entityOrderField);
        // 获取当前数据 id
        Object id = getField(arg, idField);
        // 获取当前排序值
        Integer currentOrder = getMaxOrCurrentOrderNum(parentId, id);
        // 判断新旧排序值是否相等
        if (newOrder != null && !newOrder.equals(currentOrder)) {
            // 确定排序变化方向
            int orderChange = determineOrderChange(newOrder, currentOrder);
            // 构建更新包装器
            UpdateWrapper updateWrapper = buildUpdateWrapper(currentOrder, newOrder, orderChange, parentId, id);
            // 执行更新
            baseMapper.update(updateWrapper);
        }
        return joinPoint.proceed();
    }

    /**
     * getMaxOrCurrentOrderNum 获取排序最大值或当前排序值
     *
     * @param parentId 父ID
     * @param id       id
     * @return {@link java.lang.Integer}
     */
    private Integer getMaxOrCurrentOrderNum(Object parentId, Object id) {
        QueryWrapper queryWrapper = new QueryWrapper<>(entityType);
        if (parentId != null) {
            queryWrapper.eq(parentField, parentId);
        }
        if (id != null) {
            // 添加主键ID条件
            queryWrapper.eq(idField, id);
            // 选择排序字段
            queryWrapper.select(orderField);
        } else {
            // 选择最大排序值
            queryWrapper.select(String.format("MAX(%s)", orderField));
        }
        List<?> list = baseMapper.selectObjs(queryWrapper);
        return list.isEmpty() ? 0 : (Integer) list.get(0);
    }

    /**
     * buildUpdateWrapper 构建更新包装器
     *
     * @param currentOrder 当前的排序值(如果是插入操作,则为 null )
     * @param newOrder     新的排序值
     * @param orderChange  排序变化的方向
     * @param parentId     父级id
     * @param id           id
     * @return {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<T>}
     */
    private <T> UpdateWrapper<T> buildUpdateWrapper(Integer currentOrder, int newOrder, int orderChange, Object parentId, Object id) {
        UpdateWrapper<T> updateWrapper = new UpdateWrapper<>();
        if (parentId != null) {
            // 添加父ID条件
            updateWrapper.eq(parentField, parentId);
        }
        if (currentOrder != null) {
            // 修改操作
            OrderChangeStrategy strategy = orderChange == 1 ? MOVE_FORWARD : MOVE_BACKWARD;
            strategy.apply(updateWrapper, newOrder, currentOrder);
        } else {
            // 新增操作
            updateWrapper.ge(orderField, newOrder);
        }
        updateWrapper.ne(idField, id);
        // 更新SQL片段
        updateWrapper.setSql(true, String.format("%s = %s + %s", orderField, orderField, orderChange));
        return updateWrapper;
    }

    /**
     * determineOrderChange 确定排序变化的方向
     *
     * @param newOrder     新的排序值
     * @param currentOrder 当前的排序值
     * @return {@link int}
     */
    private int determineOrderChange(int newOrder, Integer currentOrder) {
        return switch (operationType) {
            // 插入操作,仅存在+1或不变
            case INSERT -> 1;
            // 删除操作,仅存在-1或不变
            case DELETE -> -1;
            // 更新操作,取决于新的排序位置是变大还是变小
            case UPDATE -> {
                boolean isOrderDecreasing = newOrder < currentOrder;
                // 返回变化方向
                yield isOrderDecreasing ? 1 : -1;
            }
            default -> throw new IllegalArgumentException("请检查传入操作类型参数.");
        };
    }

    /**
     * handleDelete 处理删除操作
     *
     * @param joinPoint 切点
     */
    private Object handleDelete(ProceedingJoinPoint joinPoint) throws Throwable {
        // 执行原方法
        Object result = joinPoint.proceed();
        if (result == null) {
            return null;
        }
        // 获取方法参数
        Collection<Long> ids = (Collection<Long>) joinPoint.getArgs()[0];
        // 转换为列表
        List<Long> idList = ids.stream().toList();
        // 获取要删除的数据
        List<?> deleteList = getDeleteList(idList);
        if (CollUtil.isEmpty(deleteList)) {
            return null;
        }
        List<Integer> deletedOrders = deleteList.stream()
            // 获取排序值
            .map(o -> (Integer) getField(o, entityOrderField))
            .filter(Objects::nonNull)
            .toList();
        // 获取最小排序值
        int minDeletedOrder = Collections.min(deletedOrders);
        // 获取父ID
        Object parentId = getField(deleteList.get(0), entityParentId);
        // 更新排序值
        deleteUpdateOrderNum(minDeletedOrder, parentId);
        return result;
    }

    /**
     * getDeleteList 获取删除的数据
     *
     * @param ids id 集合
     * @return {@link java.util.List<?>}
     */
    private List<?> getDeleteList(List<Long> ids) {
        QueryWrapper queryWrapper = new QueryWrapper<>();
        queryWrapper.in(idField, ids);

        String sql = String.format(
            "OR (%s IN (%s) AND %s = %d)",
            idField,
            StringUtils.join(ids, ", "),
            deletedField,
            deletedValue
        );
        queryWrapper.last(sql);
        return baseMapper.selectList(queryWrapper);
    }

    /**
     * deleteUpdateOrderNum 删除操作时更新排序字段
     *
     * @param minDeletedOrder 最小删除排序值
     * @param parentId        父ID
     */
    private void deleteUpdateOrderNum(int minDeletedOrder, Object parentId) {
        // 1. 查询需要更新的记录
        QueryWrapper queryWrapper = new QueryWrapper<>();
        int rankNum = minDeletedOrder - 1;
        queryWrapper.select(idField, orderField,
            rankNum + " + RANK() OVER (ORDER BY " + orderField + ") AS rank_before_delete");
        queryWrapper.gt(orderField, minDeletedOrder);
        if (parentId != null) {
            queryWrapper.eq(parentField, parentId);
        }

        // 获取结果集
        List<Map<String, Object>> resultMaps = baseMapper.selectMaps(queryWrapper);
        if (CollectionUtils.isEmpty(resultMaps)) {
            return;
        }

        // 2. 提取结果集中每条记录的id和rank_before_delete
        Map<Long, Integer> newOrderNumMap = resultMaps.stream()
            .collect(Collectors.toMap(
                map -> ((Number) map.get(idField)).longValue(),
                map -> ((Number) map.get("rank_before_delete")).intValue()
            ));

        // 3. 构建动态CASE语句
        StringBuilder sqlCase = buildCaseStatement(newOrderNumMap);

        // 4. 创建UpdateWrapper对象来更新order_num字段
        UpdateWrapper updateWrapper = new UpdateWrapper<>();
        updateWrapper.ge(orderField, minDeletedOrder);
        updateWrapper.setSql(orderField + " = " + sqlCase);

        if (parentId != null) {
            updateWrapper.eq(parentField, parentId);
        }

        // 5. 执行更新操作
        baseMapper.update(updateWrapper);
    }

    /**
     * 构建动态CASE语句
     *
     * @param newOrderNumMap 新的排序值映射
     * @return CASE语句字符串
     */
    private StringBuilder buildCaseStatement(Map<Long, Integer> newOrderNumMap) {
        StringBuilder sqlCase = new StringBuilder("CASE ");
        boolean isFirst = true;
        for (Map.Entry<Long, Integer> entry : newOrderNumMap.entrySet()) {
            if (!isFirst) {
                sqlCase.append(" ");
            } else {
                isFirst = false;
            }
            sqlCase.append("WHEN ").append(idField).append(" = ").append(entry.getKey()).append(" THEN ").append(entry.getValue());
        }
        sqlCase.append(" ELSE ").append(orderField).append(" END");
        return sqlCase;
    }

    /**
     * getField 获取指定对象的指定字段的值
     *
     * @param obj       对象
     * @param fieldName 字段名
     * @return {@link T}
     */
    private <T> T getField(Object obj, String fieldName) {
        // 特殊处理的字段集合
        Set<String> specialFields = Set.of(entityParentId);
        // 获取指定对象的指定字段的值
        return FieldUtils.getField(obj, fieldName, entityType, specialFields);
    }
}

4. 帮助类

/**
 * 字段工具类
 *
 * @author 鲁子狄
 **/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("unchecked")
public class FieldUtils {

    /**
     * 获取指定对象的指定字段的值。
     *
     * @param obj          对象
     * @param fieldName    字段名
     * @param entityType   实体类型
     * @param specialFields 特殊处理的字段集合
     * @param <T>          返回类型的泛型
     * @return 字段值
     */
    public static <T> T getField(Object obj, String fieldName, Class<?> entityType, Set<String> specialFields) {

        Class<?> clazz = obj.getClass();

        // 如果对象类型不匹配,进行类型转换
        if (!clazz.equals(entityType)) {
            obj = MapstructUtils.convert(obj, entityType);
            clazz = entityType;
        }

        while (clazz != null) {
            try {
                // 获取字段
                Field field = clazz.getDeclaredField(fieldName);
                // 设置访问权限
                field.setAccessible(true);
                // 获取字段值
                return (T) field.get(obj);
            } catch (NoSuchFieldException e) {
                // 如果字段在特殊处理字段集合中,则返回 null
                if (specialFields != null && specialFields.contains(fieldName)) {
                    return null;
                }
                // 继续向上级类中查找
                clazz = clazz.getSuperclass();
            } catch (IllegalAccessException e) {
                throw new RuntimeException("无法访问字段 " + fieldName + ":" + e);
            }
        }

        // 如果到达这里,说明没有找到字段
        throw new NoSuchElementException("无法找到字段 " + fieldName);
    }
}
;