Browse Source

优化数据库操作逻辑

woody 1 year ago
parent
commit
e146fef177
17 changed files with 444 additions and 289 deletions
  1. 3 3
      framework-database/src/main/java/com/chelvc/framework/database/annotation/CreateUpdate.java
  2. 1 1
      framework-database/src/main/java/com/chelvc/framework/database/config/DatabaseConfigurer.java
  3. 17 31
      framework-database/src/main/java/com/chelvc/framework/database/context/DatabaseContextHolder.java
  4. 10 6
      framework-database/src/main/java/com/chelvc/framework/database/context/GetterContext.java
  5. 4 4
      framework-database/src/main/java/com/chelvc/framework/database/context/UniqueContext.java
  6. 58 138
      framework-database/src/main/java/com/chelvc/framework/database/interceptor/DynamicDatasourceInterceptor.java
  7. 109 67
      framework-database/src/main/java/com/chelvc/framework/database/interceptor/PropertyUpdateInterceptor.java
  8. 0 27
      framework-database/src/main/java/com/chelvc/framework/database/mapper/BasicMapper.java
  9. 15 3
      framework-database/src/main/java/com/chelvc/framework/database/support/AbstractInsertMethod.java
  10. 45 0
      framework-database/src/main/java/com/chelvc/framework/database/support/BasicMapper.java
  11. 46 0
      framework-database/src/main/java/com/chelvc/framework/database/support/BasicService.java
  12. 37 0
      framework-database/src/main/java/com/chelvc/framework/database/support/BasicServiceImpl.java
  13. 31 0
      framework-database/src/main/java/com/chelvc/framework/database/support/BatchInsertIgnoreMethod.java
  14. 56 0
      framework-database/src/main/java/com/chelvc/framework/database/support/BatchInsertUpdateMethod.java
  15. 3 1
      framework-database/src/main/java/com/chelvc/framework/database/support/CustomerSqlInjector.java
  16. 1 1
      framework-database/src/main/java/com/chelvc/framework/database/support/InsertIgnoreMethod.java
  17. 8 7
      framework-database/src/main/java/com/chelvc/framework/database/support/InsertUpdateMethod.java

+ 3 - 3
framework-database/src/main/java/com/chelvc/framework/database/annotation/CreateUpdate.java

@@ -30,7 +30,7 @@ public @interface CreateUpdate {
      *
      * @return 更新方式
      */
-    Mode mode() default Mode.REPLACE;
+    Mode mode() default Mode.DEFAULT;
 
     /**
      * 更新方式枚举
@@ -57,8 +57,8 @@ public @interface CreateUpdate {
         DIVIDE,
 
         /**
-         * 原值替换新值
+         * 默认等于新值
          */
-        REPLACE;
+        DEFAULT;
     }
 }

+ 1 - 1
framework-database/src/main/java/com/chelvc/framework/database/config/DatabaseConfigurer.java

@@ -16,7 +16,7 @@ import com.chelvc.framework.common.function.Provider;
 import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.database.context.Transactor;
 import com.chelvc.framework.database.interceptor.DeletedExcludeHandler;
-import com.chelvc.framework.database.mapper.CustomerSqlInjector;
+import com.chelvc.framework.database.support.CustomerSqlInjector;
 import com.chelvc.framework.redis.context.RedisContextHolder;
 import lombok.NonNull;
 import lombok.RequiredArgsConstructor;

+ 17 - 31
framework-database/src/main/java/com/chelvc/framework/database/context/DatabaseContextHolder.java

@@ -49,8 +49,6 @@ import com.chelvc.framework.database.entity.Entity;
 import com.chelvc.framework.database.entity.Updatable;
 import com.google.common.collect.Maps;
 import lombok.NonNull;
-import org.apache.ibatis.reflection.MetaObject;
-import org.apache.ibatis.reflection.SystemMetaObject;
 import org.mybatis.spring.SqlSessionTemplate;
 import org.springframework.context.ApplicationContext;
 import org.springframework.util.CollectionUtils;
@@ -70,7 +68,7 @@ public final class DatabaseContextHolder {
     /**
      * 对象/唯一约束条件列表映射
      */
-    private static final Map<Class<?>, List<UniqueContext>> CLASS_UNIQUE_MAPPING = new ConcurrentHashMap<>();
+    private static final Map<Class<?>, List<UniqueContext<?>>> CLASS_UNIQUE_MAPPING = new ConcurrentHashMap<>();
 
     /**
      * 对象加密数据字段映射表
@@ -287,24 +285,6 @@ public final class DatabaseContextHolder {
         return null;
     }
 
-    /**
-     * 获取原始对象实例
-     *
-     * @param entity 对象实例
-     * @return 对象实例
-     */
-    public static Object getOriginalEntity(Object entity) {
-        if (!(entity instanceof Map)) {
-            return entity;
-        }
-        MetaObject meta = SystemMetaObject.forObject(entity);
-        if (meta.hasGetter("et")) {
-            // 如果是批量更新操作,则entity为Map结构,则通过"et"键获取对象实例
-            entity = meta.metaObjectForProperty("et").getOriginalObject();
-        }
-        return entity;
-    }
-
     /**
      * 将属性Getter方法引用转换成数据列信息
      *
@@ -324,9 +304,11 @@ public final class DatabaseContextHolder {
      *
      * @param clazz Getter方法引用对象
      * @param field 字段对象
+     * @param <T>   Getter方法实体类型
+     * @param <R>   Getter方法返回类型
      * @return 方法引用
      */
-    public static <T, R> GetterContext<T, R> field2getter(@NonNull Class<?> clazz, @NonNull Field field) {
+    public static <T, R> GetterContext<T, R> field2getter(@NonNull Class<T> clazz, @NonNull Field field) {
         return new GetterContext<>(field.getName(), clazz, field.getDeclaringClass());
     }
 
@@ -334,10 +316,12 @@ public final class DatabaseContextHolder {
      * 获取指定对象对应的唯一约束上下文
      *
      * @param clazz 数据实体对象
+     * @param <T>   实体类型
      * @return 唯一约束上下文列表
      */
-    public static List<UniqueContext> getUniqueContexts(@NonNull Class<?> clazz) {
-        return CLASS_UNIQUE_MAPPING.computeIfAbsent(clazz, c -> {
+    @SuppressWarnings("unchecked")
+    public static <T> List<UniqueContext<T>> getUniqueContexts(@NonNull Class<T> clazz) {
+        List<?> contexts = CLASS_UNIQUE_MAPPING.computeIfAbsent(clazz, c -> {
             // 获取唯一约束属性名称/唯一约束注解实例映射
             Map<String, Field> fields = Maps.newHashMap();
             Map<Field, Unique> annotations = Maps.newHashMap();
@@ -353,20 +337,21 @@ public final class DatabaseContextHolder {
             }
 
             // 构建唯一约束上下文
-            List<UniqueContext> uniques = annotations.entrySet().stream().map(entry -> {
+            List<UniqueContext<T>> uniques = annotations.entrySet().stream().map(entry -> {
                 Field field = entry.getKey();
                 Unique unique = entry.getValue();
                 Stream<String> properties = Stream.concat(Stream.of(field.getName()), Stream.of(unique.composites()));
-                List<GetterContext<Object, ?>> getters = properties.map(String::trim).distinct()
+                List<GetterContext<T, ?>> getters = properties.map(String::trim).distinct()
                         .filter(StringUtils::notEmpty).map(property -> {
                             Field f = fields.get(property);
                             AssertUtils.nonnull(f, () -> "Field not found: " + clazz.getName() + "." + property);
                             return field2getter(clazz, f);
                         }).collect(Collectors.toList());
-                return new UniqueContext(field, unique, getters);
+                return new UniqueContext<T>(field, unique, getters);
             }).collect(Collectors.toList());
             return Collections.unmodifiableList(uniques);
         });
+        return (List<UniqueContext<T>>) contexts;
     }
 
     /**
@@ -575,7 +560,7 @@ public final class DatabaseContextHolder {
      *
      * @param entity 数据实体
      */
-    public static void initializeEntityDefaultValues(@NonNull Entity<?> entity) {
+    public static void initializeEntityDefaultValue(@NonNull Entity<?> entity) {
         // 设置新增属性默认值
         Long operator = SessionContextHolder.getId();
         if (operator != null && entity instanceof Creatable) {
@@ -610,11 +595,12 @@ public final class DatabaseContextHolder {
      * @param clazz   对象类型
      * @param entity  对象实例
      * @param uniques 唯一约束上下文列表
+     * @param <T>     实体类型
      * @return Lambda查询对象实例
      */
-    public static LambdaQueryWrapper<Object> initializeUniqueQuery(@NonNull Class<Object> clazz, @NonNull Object entity,
-                                                                   @NonNull List<UniqueContext> uniques) {
-        GetterContext.Query query = new GetterContext.Query(clazz);
+    public static <T> LambdaQueryWrapper<T> initializeUniqueQuery(@NonNull Class<T> clazz, @NonNull T entity,
+                                                                  @NonNull List<UniqueContext<T>> uniques) {
+        GetterContext.Query<T> query = new GetterContext.Query<>(clazz);
         if (uniques.size() > 1) {
             uniques.forEach(unique -> query.or(subquery -> unique.getGetters().forEach(getter -> {
                 Object value = getter.apply(entity);

+ 10 - 6
framework-database/src/main/java/com/chelvc/framework/database/context/GetterContext.java

@@ -16,6 +16,8 @@ import lombok.NonNull;
 /**
  * 对象实体Getter方法上下文
  *
+ * @param <T> Getter方法实体类型
+ * @param <T> Getter方法返回类型
  * @author Woody
  * @date 2024/1/30
  */
@@ -57,19 +59,21 @@ public class GetterContext<T, R> implements SFunction<T, R> {
 
     /**
      * 自定义Getter方法引用查询包装器
+     *
+     * @param <T> 实体类型
      */
-    public static class Query extends LambdaQueryWrapper<Object> {
+    public static class Query<T> extends LambdaQueryWrapper<T> {
         private static final Field PARAM_NAME_SEQ_FIELD = ObjectUtils.getField(AbstractWrapper.class, "paramNameSeq");
         private static final Field PARAM_NAME_VALUE_PAIRS_FIELD =
                 ObjectUtils.getField(AbstractWrapper.class, "paramNameValuePairs");
 
-        public Query(Class<Object> clazz) {
+        public Query(Class<T> clazz) {
             super(clazz);
         }
 
         @Override
-        protected LambdaQueryWrapper<Object> instance() {
-            LambdaQueryWrapper<Object> instance = new Query(this.getEntityClass());
+        protected LambdaQueryWrapper<T> instance() {
+            LambdaQueryWrapper<T> instance = new Query<>(this.getEntityClass());
             // 复制paramNameSeq、paramNameValuePairs,不然会出现子查询参数为空的情况
             ObjectUtils.setValue(instance, PARAM_NAME_SEQ_FIELD, this.paramNameSeq);
             ObjectUtils.setValue(instance, PARAM_NAME_VALUE_PAIRS_FIELD, this.paramNameValuePairs);
@@ -78,8 +82,8 @@ public class GetterContext<T, R> implements SFunction<T, R> {
 
         @Override
         @SuppressWarnings("unchecked")
-        protected String columnToString(SFunction<Object, ?> getter, boolean onlyColumn) {
-            ColumnCache column = DatabaseContextHolder.getter2column((GetterContext<Object, ?>) getter);
+        protected String columnToString(SFunction<T, ?> getter, boolean onlyColumn) {
+            ColumnCache column = DatabaseContextHolder.getter2column((GetterContext<T, ?>) getter);
             return onlyColumn ? column.getColumn() : column.getColumnSelect();
         }
     }

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

@@ -10,11 +10,12 @@ import lombok.NonNull;
 /**
  * 唯一约束字段上下文
  *
+ * @param <T> 实体类型
  * @author Woody
  * @date 2024/1/30
  */
 @Getter
-public class UniqueContext {
+public class UniqueContext<T> {
     /**
      * 目标字段
      */
@@ -28,10 +29,9 @@ public class UniqueContext {
     /**
      * 唯一约束字段Getter方法列表
      */
-    private final List<GetterContext<Object, ?>> getters;
+    private final List<GetterContext<T, ?>> getters;
 
-    public UniqueContext(@NonNull Field field, @NonNull Unique unique,
-                         @NonNull List<GetterContext<Object, ?>> getters) {
+    public UniqueContext(@NonNull Field field, @NonNull Unique unique, @NonNull List<GetterContext<T, ?>> getters) {
         this.field = field;
         this.unique = unique;
         this.getters = getters;

+ 58 - 138
framework-database/src/main/java/com/chelvc/framework/database/interceptor/DynamicDatasourceInterceptor.java

@@ -14,11 +14,6 @@ import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.springframework.aop.framework.AopProxyUtils;
-import org.springframework.beans.BeansException;
-import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
-import org.springframework.beans.factory.support.BeanDefinitionRegistry;
-import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
-import org.springframework.beans.factory.support.RootBeanDefinition;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.core.Ordered;
 import org.springframework.core.annotation.Order;
@@ -31,158 +26,83 @@ import org.springframework.util.CollectionUtils;
  * @date 2024/1/30
  */
 @Slf4j
+@Aspect
+@Order(Ordered.HIGHEST_PRECEDENCE)
 @ConditionalOnClass(DynamicDataSourceContextHolder.class)
-public class DynamicDatasourceInterceptor implements BeanDefinitionRegistryPostProcessor {
-    @Override
-    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
-    }
+public class DynamicDatasourceInterceptor {
+    private final Map<String, String> datasourceContext;
 
-    @Override
-    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
-        boolean standard = true;
-        try {
-            // 尝试加载@DubboService注解类
-            this.getClass().getClassLoader().loadClass("org.apache.dubbo.config.annotation.DubboService");
-        } catch (ClassNotFoundException e) {
-            standard = false;
-        }
-        if (standard) {
-            registry.registerBeanDefinition("standardDynamicDatasourceInterceptor",
-                    new RootBeanDefinition(StandardDynamicDatasourceInterceptor.class));
+    public DynamicDatasourceInterceptor(@NonNull DynamicDataSourceProperties properties) {
+        // 初始化数据源标识集合
+        Set<String> keys = properties.getDatasource().keySet();
+        if (CollectionUtils.isEmpty(keys)) {
+            this.datasourceContext = Collections.emptyMap();
         } else {
-            registry.registerBeanDefinition("basicDynamicDatasourceInterceptor",
-                    new RootBeanDefinition(BasicDynamicDatasourceInterceptor.class));
+            this.datasourceContext = Maps.newHashMapWithExpectedSize(keys.size());
+            keys.forEach(value -> {
+                for (String key : StringUtils.splitActives(value)) {
+                    this.datasourceContext.put(key, value);
+                }
+            });
+        }
+        if (log.isDebugEnabled()) {
+            log.debug("Initialized datasource context: {}", this.datasourceContext);
         }
     }
 
     /**
-     * 动态数据源拦截器抽象实现
+     * 查找数据源标识
+     *
+     * @param point 方法拦截点
+     * @return 数据源标识
      */
-    public static abstract class AbstractDynamicDatasourceInterceptor {
-        private final Map<String, String> datasourceContext;
+    private String lookupDatasourceKey(ProceedingJoinPoint point) {
+        Object target = point.getTarget();
 
-        public AbstractDynamicDatasourceInterceptor(@NonNull DynamicDataSourceProperties properties) {
-            // 初始化数据源标识集合
-            Set<String> keys = properties.getDatasource().keySet();
-            if (CollectionUtils.isEmpty(keys)) {
-                this.datasourceContext = Collections.emptyMap();
-            } else {
-                this.datasourceContext = Maps.newHashMapWithExpectedSize(keys.size());
-                keys.forEach(value -> {
-                    for (String key : StringUtils.splitActives(value)) {
-                        this.datasourceContext.put(key, value);
-                    }
-                });
-            }
-            if (log.isDebugEnabled()) {
-                log.debug("Initialized datasource context: {}", this.datasourceContext);
-            }
+        // 根据目标类对象查找数据源标识
+        String className = AopProxyUtils.ultimateTargetClass(target).getName();
+        Set<Map.Entry<String, String>> routes = this.datasourceContext.entrySet();
+        String key = routes.stream().filter(entry -> className.startsWith(entry.getKey())).findAny()
+                .map(Map.Entry::getValue).orElse(null);
+        if (StringUtils.notEmpty(key)) {
+            return key;
         }
 
-        /**
-         * 查找数据源标识
-         *
-         * @param point 方法拦截点
-         * @return 数据源标识
-         */
-        private String lookupDatasourceKey(ProceedingJoinPoint point) {
-            Object target = point.getTarget();
-
-            // 根据目标类对象查找数据源标识
-            String className = AopProxyUtils.ultimateTargetClass(target).getName();
-            Set<Map.Entry<String, String>> routes = this.datasourceContext.entrySet();
-            String key = routes.stream().filter(entry -> className.startsWith(entry.getKey())).findAny()
-                    .map(Map.Entry::getValue).orElse(null);
-            if (StringUtils.notEmpty(key)) {
-                return key;
-            }
-
-            // 根据目标接口对象查找数据源标识
-            Class<?>[] interfaces = AopProxyUtils.proxiedUserInterfaces(target);
-            if (interfaces.length > 0) {
-                for (Class<?> clazz : interfaces) {
-                    String interfaceName = clazz.getName();
-                    String route = routes.stream().filter(entry -> interfaceName.startsWith(entry.getKey())).findAny()
-                            .map(Map.Entry::getValue).orElse(null);
-                    if (StringUtils.notEmpty(route)) {
-                        return route;
-                    }
+        // 根据目标接口对象查找数据源标识
+        Class<?>[] interfaces = AopProxyUtils.proxiedUserInterfaces(target);
+        if (interfaces.length > 0) {
+            for (Class<?> clazz : interfaces) {
+                String interfaceName = clazz.getName();
+                String route = routes.stream().filter(entry -> interfaceName.startsWith(entry.getKey())).findAny()
+                        .map(Map.Entry::getValue).orElse(null);
+                if (StringUtils.notEmpty(route)) {
+                    return route;
                 }
             }
-            return null;
-        }
-
-        /**
-         * 数据源路由
-         *
-         * @param point 方法拦截点
-         * @return 方法调用结果
-         * @throws Throwable 方法调用异常
-         */
-        protected Object routing(ProceedingJoinPoint point) throws Throwable {
-            String key = this.lookupDatasourceKey(point);
-            if (log.isDebugEnabled()) {
-                log.debug("Initializing datasource context for {}: {}", point.getSignature().toShortString(), key);
-            }
-            DynamicDataSourceContextHolder.push(key);
-            try {
-                return point.proceed(point.getArgs());
-            } finally {
-                DynamicDataSourceContextHolder.poll();
-            }
         }
+        return null;
     }
 
     /**
-     * 动态数据源拦截器基础实现
+     * 数据源路由
+     *
+     * @param point 方法拦截点
+     * @return 方法调用结果
+     * @throws Throwable 方法调用异常
      */
-    @Aspect
-    @Order(Ordered.HIGHEST_PRECEDENCE)
-    public static class BasicDynamicDatasourceInterceptor extends AbstractDynamicDatasourceInterceptor {
-        public BasicDynamicDatasourceInterceptor(DynamicDataSourceProperties properties) {
-            super(properties);
-        }
-
-        /**
-         * 数据源路由
-         *
-         * @param point 方法拦截点
-         * @return 方法调用结果
-         * @throws Throwable 方法调用异常
-         */
-        @Around("@within(org.springframework.stereotype.Service) " +
-                "|| @within(org.apache.ibatis.annotations.Mapper) " +
-                "|| this(com.baomidou.mybatisplus.core.mapper.BaseMapper) " +
-                "|| this(com.chelvc.framework.database.mapper.BasicMapper)")
-        public Object routing(ProceedingJoinPoint point) throws Throwable {
-            return super.routing(point);
+    @Around("this(com.baomidou.mybatisplus.core.mapper.BaseMapper) " +
+            "|| this(com.chelvc.framework.database.support.BasicMapper)"
+    )
+    public Object routing(ProceedingJoinPoint point) throws Throwable {
+        String key = this.lookupDatasourceKey(point);
+        if (log.isDebugEnabled()) {
+            log.debug("Initializing datasource context for {}: {}", point.getSignature().toShortString(), key);
         }
-    }
-
-    /**
-     * 动态数据源拦截器标准实现
-     */
-    @Aspect
-    @Order(Ordered.HIGHEST_PRECEDENCE)
-    public static class StandardDynamicDatasourceInterceptor extends AbstractDynamicDatasourceInterceptor {
-        public StandardDynamicDatasourceInterceptor(DynamicDataSourceProperties properties) {
-            super(properties);
-        }
-
-        /**
-         * 数据源路由
-         *
-         * @param point 方法拦截点
-         * @return 方法调用结果
-         * @throws Throwable 方法调用异常
-         */
-        @Around("@within(org.springframework.stereotype.Service) " +
-                "|| @within(org.apache.ibatis.annotations.Mapper) " +
-                "|| @within(org.apache.dubbo.config.annotation.DubboService) " +
-                "|| this(com.baomidou.mybatisplus.core.mapper.BaseMapper) " +
-                "|| this(com.chelvc.framework.database.mapper.BasicMapper)")
-        public Object routing(ProceedingJoinPoint point) throws Throwable {
-            return super.routing(point);
+        DynamicDataSourceContextHolder.push(key);
+        try {
+            return point.proceed(point.getArgs());
+        } finally {
+            DynamicDataSourceContextHolder.poll();
         }
     }
 }

+ 109 - 67
framework-database/src/main/java/com/chelvc/framework/database/interceptor/PropertyUpdateInterceptor.java

@@ -1,5 +1,6 @@
 package com.chelvc.framework.database.interceptor;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -13,6 +14,7 @@ import com.chelvc.framework.database.context.DatabaseContextHolder;
 import com.chelvc.framework.database.context.UniqueContext;
 import com.chelvc.framework.database.entity.Entity;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.binding.MapperMethod;
 import org.apache.ibatis.cache.CacheKey;
 import org.apache.ibatis.executor.Executor;
 import org.apache.ibatis.mapping.BoundSql;
@@ -44,86 +46,126 @@ import org.springframework.stereotype.Component;
 })
 public class PropertyUpdateInterceptor implements Interceptor {
     /**
-     * 属性更新拦截处理
+     * 数据查询
      *
-     * @param invocation 调用信息
-     * @return 调用结果
-     * @throws Throwable 调用异常
+     * @param invocation 方法调用信息
+     * @return 查询结果
+     * @throws Throwable 操作异常
      */
-    @SuppressWarnings("unchecked")
-    private Object processing(Invocation invocation) throws Throwable {
-        SqlCommandType operation = ((MappedStatement) invocation.getArgs()[0]).getSqlCommandType();
-        if (operation == SqlCommandType.SELECT) {
-            // 加密数据解码
-            Object result = invocation.proceed();
-            if (ObjectUtils.notBlank(result) && DatabaseContextHolder.isCodecEnabled()) {
-                try {
-                    DatabaseContextHolder.decoder(cipher -> ObjectUtils.stream(result).forEach(
-                            entity -> DatabaseContextHolder.decode(cipher, entity)
-                    ));
-                } catch (Exception e) {
-                    log.warn("Confidential value decode failed: {}", e.getMessage());
-                }
-            }
-            return result;
-        } else if (operation == SqlCommandType.INSERT || operation == SqlCommandType.UPDATE) {
-            Object entity = DatabaseContextHolder.getOriginalEntity(invocation.getArgs()[1]);
-            if (entity instanceof Entity) {
-                // 初始化默认属性值
-                DatabaseContextHolder.initializeEntityDefaultValues((Entity<?>) entity);
+    private Object select(Invocation invocation) throws Throwable {
+        // 加密数据解码
+        Object result = invocation.proceed();
+        if (ObjectUtils.notBlank(result) && DatabaseContextHolder.isCodecEnabled()) {
+            try {
+                DatabaseContextHolder.decoder(cipher -> ObjectUtils.stream(result).forEach(
+                        entity -> DatabaseContextHolder.decode(cipher, entity)
+                ));
+            } catch (Exception e) {
+                log.warn("Confidential value decode failed: {}", e.getMessage());
             }
+        }
+        return result;
+    }
 
-            // 设置加密属性值
-            Class<Object> clazz = (Class<Object>) entity.getClass();
-            boolean encoding = DatabaseContextHolder.isCodecEnabled() && DatabaseContextHolder.isConfidential(clazz);
-            Object original = encoding ? ObjectUtils.copying(entity, clazz) : entity;
-            if (encoding) {
-                DatabaseContextHolder.encode(entity);
+    /**
+     * 数据更新
+     *
+     * @param invocation 方法调用信息
+     * @return 更新结果
+     * @throws Throwable 操作异常
+     */
+    private Object modify(Invocation invocation) throws Throwable {
+        // 获取更新目标对象实例
+        Object object = invocation.getArgs()[1];
+        if (object instanceof MapperMethod.ParamMap) {
+            // 如果是批量更新则为ParamMap
+            // 如果Mapper方法使用@Param参数注解则map包含"param1"和自定义参数名称,否则使用"collection"获取列表
+            // ParamMap如果get不存在的key会排除异常,所以先判断目标key是否存在
+            MapperMethod.ParamMap<?> param = (MapperMethod.ParamMap<?>) object;
+            if (param.containsKey("collection")) {
+                object = param.get("collection");
+            } else if (param.containsKey("param1")) {
+                object = param.get("param1");
             }
+        }
 
-            // 唯一属性值校验
-            // 根据唯一属性值查询现有记录
-            List<UniqueContext> uniques = DatabaseContextHolder.getUniqueContexts(clazz);
-            if (ObjectUtils.isEmpty(uniques)) {
-                return invocation.proceed();
-            }
-            LambdaQueryWrapper<Object> query = DatabaseContextHolder.initializeUniqueQuery(clazz, entity, uniques);
-            List<Object> objects = query.isEmptyOfWhere() ? Collections.emptyList() :
-                    DatabaseContextHolder.lookupEntityService(clazz).list(query);
-            if (ObjectUtils.isEmpty(objects)) {
-                return invocation.proceed();
-            }
+        // 数据更新
+        if (object instanceof Collection) {
+            ((Collection<?>) object).forEach(entity -> {
+                if (entity instanceof Entity) {
+                    this.modify((Entity<?>) entity);
+                }
+            });
+        } else if (object instanceof Entity) {
+            this.modify((Entity<?>) object);
+        }
+        return invocation.proceed();
+    }
 
-            // 如果是修改,则排除主键相同的数据
-            if (operation == SqlCommandType.UPDATE) {
-                Object id = DatabaseContextHolder.getEntityIdentity(entity);
-                objects.removeIf(object -> Objects.equals(id, DatabaseContextHolder.getEntityIdentity(object)));
-            }
+    /**
+     * 实体信息更新
+     *
+     * @param entity 实体对象实例
+     * @param <T>    实体类型
+     */
+    @SuppressWarnings("unchecked")
+    private <T extends Entity<?>> void modify(T entity) {
+        // 初始化默认属性值
+        DatabaseContextHolder.initializeEntityDefaultValue(entity);
 
-            // 比较唯一属性值,找出已存在唯一属性名称
-            Map<String, String> errors = uniques.stream().filter(context -> objects.stream().anyMatch(
-                    object -> context.getGetters().stream().allMatch(getter -> {
-                        Object value = getter.apply(object);
-                        if (encoding) {
-                            return Objects.equals(value, getter.apply(original))
-                                    || Objects.equals(value, getter.apply(entity));
-                        }
-                        return Objects.equals(value, getter.apply(original));
-                    })
-            )).collect(Collectors.toMap(
-                    context -> context.getField().getName(), context -> context.getUnique().message()
-            ));
-            if (ObjectUtils.notEmpty(errors)) {
-                throw new ParameterInvalidException(errors);
-            }
+        // 设置加密属性值
+        Class<T> clazz = (Class<T>) entity.getClass();
+        boolean encoding = DatabaseContextHolder.isCodecEnabled() && DatabaseContextHolder.isConfidential(clazz);
+        T original = encoding ? ObjectUtils.copying(entity, clazz) : entity;
+        if (encoding) {
+            DatabaseContextHolder.encode(entity);
+        }
+
+        // 唯一属性值校验
+        // 根据唯一属性值查询现有记录
+        List<UniqueContext<T>> uniques = DatabaseContextHolder.getUniqueContexts(clazz);
+        if (ObjectUtils.isEmpty(uniques)) {
+            return;
+        }
+        LambdaQueryWrapper<T> query = DatabaseContextHolder.initializeUniqueQuery(clazz, entity, uniques);
+        List<T> objects = query.isEmptyOfWhere() ? Collections.emptyList() :
+                DatabaseContextHolder.lookupEntityService(clazz).list(query);
+        if (ObjectUtils.isEmpty(objects)) {
+            return;
+        }
+
+        // 排除ID相同的数据
+        Object id = DatabaseContextHolder.getEntityIdentity(entity);
+        objects.removeIf(object -> Objects.equals(id, DatabaseContextHolder.getEntityIdentity(object)));
+
+        // 比较唯一属性值,找出已存在唯一属性名称
+        Map<String, String> errors = uniques.stream().filter(context -> objects.stream().anyMatch(
+                object -> context.getGetters().stream().allMatch(getter -> {
+                    Object value = getter.apply(object);
+                    if (encoding) {
+                        return Objects.equals(value, getter.apply(original))
+                                || Objects.equals(value, getter.apply(entity));
+                    }
+                    return Objects.equals(value, getter.apply(original));
+                })
+        )).collect(Collectors.toMap(
+                context -> context.getField().getName(), context -> context.getUnique().message()
+        ));
+        if (ObjectUtils.notEmpty(errors)) {
+            throw new ParameterInvalidException(errors);
         }
-        return invocation.proceed();
     }
 
     @Override
     public Object intercept(Invocation invocation) throws Throwable {
-        if (invocation.getTarget() instanceof Executor) {
-            return this.processing(invocation);
+        if (!(invocation.getTarget() instanceof Executor)) {
+            return invocation.proceed();
+        }
+        SqlCommandType operation = ((MappedStatement) invocation.getArgs()[0]).getSqlCommandType();
+        if (operation == SqlCommandType.SELECT) {
+            return this.select(invocation);
+        } else if (operation == SqlCommandType.INSERT || operation == SqlCommandType.UPDATE) {
+            return this.modify(invocation);
         }
         return invocation.proceed();
     }

+ 0 - 27
framework-database/src/main/java/com/chelvc/framework/database/mapper/BasicMapper.java

@@ -1,27 +0,0 @@
-package com.chelvc.framework.database.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-
-/**
- * 数据模型操作接口
- *
- * @author Woody
- * @date 2024/2/27
- */
-public interface BasicMapper<T> extends BaseMapper<T> {
-    /**
-     * 创建或忽略实体
-     *
-     * @param entity 实体对象实例
-     * @return true/false
-     */
-    boolean insertIgnore(T entity);
-
-    /**
-     * 创建或修改实体
-     *
-     * @param entity 实体对象实例
-     * @return true/false
-     */
-    boolean insertUpdate(T entity);
-}

+ 15 - 3
framework-database/src/main/java/com/chelvc/framework/database/mapper/AbstractInsertMethod.java → framework-database/src/main/java/com/chelvc/framework/database/support/AbstractInsertMethod.java

@@ -1,4 +1,4 @@
-package com.chelvc.framework.database.mapper;
+package com.chelvc.framework.database.support;
 
 import java.util.List;
 
@@ -53,8 +53,20 @@ public abstract class AbstractInsertMethod extends AbstractMethod {
      * @return 属性信息
      */
     protected String field2property(TableInfo table, List<TableFieldInfo> fields) {
-        String properties = table.getKeyInsertSqlProperty(null, false) +
-                this.filterTableFieldInfo(fields, null, i -> i.getInsertSqlProperty(null), EMPTY);
+        return this.field2property(table, null, fields);
+    }
+
+    /**
+     * 将字段转换成属性
+     *
+     * @param table  表信息
+     * @param prefix 属性前缀
+     * @param fields 字段列表
+     * @return 属性信息
+     */
+    protected String field2property(TableInfo table, String prefix, List<TableFieldInfo> fields) {
+        String properties = table.getKeyInsertSqlProperty(prefix, false) +
+                this.filterTableFieldInfo(fields, null, i -> i.getInsertSqlProperty(prefix), EMPTY);
         return properties.substring(0, properties.length() - 1);
     }
 

+ 45 - 0
framework-database/src/main/java/com/chelvc/framework/database/support/BasicMapper.java

@@ -0,0 +1,45 @@
+package com.chelvc.framework.database.support;
+
+import java.util.Collection;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ * 数据模型操作接口
+ *
+ * @author Woody
+ * @date 2024/2/27
+ */
+public interface BasicMapper<T> extends BaseMapper<T> {
+    /**
+     * 创建或忽略实体
+     *
+     * @param entity 实体对象实例
+     * @return 新增行数
+     */
+    int insertIgnore(T entity);
+
+    /**
+     * 创建或修改实体
+     *
+     * @param entity 实体对象实例
+     * @return 更新行数
+     */
+    int insertUpdate(T entity);
+
+    /**
+     * 批量创建或忽略实体
+     *
+     * @param entities 实体对象实例集合
+     * @return 新增行数
+     */
+    int batchInsertIgnore(Collection<T> entities);
+
+    /**
+     * 批量创建或修改实体
+     *
+     * @param entities 实体对象实例集合
+     * @return 更新行数
+     */
+    int batchInsertUpdate(Collection<T> entities);
+}

+ 46 - 0
framework-database/src/main/java/com/chelvc/framework/database/support/BasicService.java

@@ -0,0 +1,46 @@
+package com.chelvc.framework.database.support;
+
+import java.util.Collection;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * 基础业务操作接口
+ *
+ * @param <T> 数据模型类型
+ * @author Woody
+ * @date 2024/3/10
+ */
+public interface BasicService<T> extends IService<T> {
+    /**
+     * 创建或忽略实体
+     *
+     * @param entity 实体对象实例
+     * @return true/false
+     */
+    boolean createIgnore(T entity);
+
+    /**
+     * 创建或修改实体
+     *
+     * @param entity 实体对象实例
+     * @return true/false
+     */
+    boolean createUpdate(T entity);
+
+    /**
+     * 批量创建或忽略实体
+     *
+     * @param entities 实体对象实例集合
+     * @return true/false
+     */
+    boolean batchCreateIgnore(Collection<T> entities);
+
+    /**
+     * 批量创建或修改实体
+     *
+     * @param entities 实体对象实例集合
+     * @return true/false
+     */
+    boolean batchCreateUpdate(Collection<T> entities);
+}

+ 37 - 0
framework-database/src/main/java/com/chelvc/framework/database/support/BasicServiceImpl.java

@@ -0,0 +1,37 @@
+package com.chelvc.framework.database.support;
+
+import java.util.Collection;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.chelvc.framework.common.util.ObjectUtils;
+import lombok.NonNull;
+
+/**
+ * 基础业务操作实现
+ *
+ * @param <M> 数据操作接口类型
+ * @param <T> 数据模型类型
+ * @author Woody
+ * @date 2024/3/10
+ */
+public class BasicServiceImpl<M extends BasicMapper<T>, T> extends ServiceImpl<M, T> implements BasicService<T> {
+    @Override
+    public boolean createIgnore(T entity) {
+        return entity != null && this.baseMapper.insertIgnore(entity) > 0;
+    }
+
+    @Override
+    public boolean createUpdate(T entity) {
+        return entity != null && this.baseMapper.insertUpdate(entity) > 0;
+    }
+
+    @Override
+    public boolean batchCreateIgnore(Collection<T> entities) {
+        return ObjectUtils.notEmpty(entities) && this.baseMapper.batchInsertIgnore(entities) > 0;
+    }
+
+    @Override
+    public boolean batchCreateUpdate(@NonNull Collection<T> entities) {
+        return ObjectUtils.notEmpty(entities) && this.baseMapper.batchInsertUpdate(entities) > 0;
+    }
+}

+ 31 - 0
framework-database/src/main/java/com/chelvc/framework/database/support/BatchInsertIgnoreMethod.java

@@ -0,0 +1,31 @@
+package com.chelvc.framework.database.support;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
+import com.baomidou.mybatisplus.core.metadata.TableInfo;
+import lombok.NonNull;
+
+/**
+ * 批量插入或忽略方法实现
+ *
+ * @author Woody
+ * @date 2024/2/27
+ */
+public class BatchInsertIgnoreMethod extends AbstractInsertMethod {
+    public BatchInsertIgnoreMethod() {
+        super("batchInsertIgnore");
+    }
+
+    @Override
+    protected String initializeMethodScript(@NonNull TableInfo table) {
+        List<TableFieldInfo> fields = table.getFieldList();
+        String columns = this.field2column(table, fields);
+        String properties = this.field2property(table, "et.", fields);
+        return String.format(
+                "<script>\nINSERT IGNORE INTO %s (%s) VALUES\n" +
+                        "<foreach collection=\"collection\" item=\"et\" separator=\",\">(%s)</foreach>\n</script>",
+                table.getTableName(), columns, properties
+        );
+    }
+}

+ 56 - 0
framework-database/src/main/java/com/chelvc/framework/database/support/BatchInsertUpdateMethod.java

@@ -0,0 +1,56 @@
+package com.chelvc.framework.database.support;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
+import com.baomidou.mybatisplus.core.metadata.TableInfo;
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.chelvc.framework.common.util.ObjectUtils;
+import com.chelvc.framework.database.annotation.CreateUpdate;
+import lombok.NonNull;
+
+/**
+ * 批量插入或修改方法实现
+ *
+ * @author Woody
+ * @date 2024/2/27
+ */
+public class BatchInsertUpdateMethod extends AbstractInsertMethod {
+    public BatchInsertUpdateMethod() {
+        super("batchInsertUpdate");
+    }
+
+    @Override
+    protected String initializeMethodScript(@NonNull TableInfo table) {
+        List<TableFieldInfo> fields = table.getFieldList();
+        String columns = this.field2column(table, fields);
+        String properties = this.field2property(table, "et.", fields);
+        String updates = fields.stream().map(field -> {
+            CreateUpdate annotation = field.getField().getAnnotation(CreateUpdate.class);
+            if (annotation != null && !annotation.enabled()) {
+                return null;
+            }
+            String column = field.getColumn();
+            StringBuilder buffer = new StringBuilder().append(column).append(" = ");
+            CreateUpdate.Mode mode = ObjectUtils.ifNull(annotation, CreateUpdate::mode);
+            if (mode == CreateUpdate.Mode.ADD) {
+                buffer.append(column).append(" + ");
+            } else if (mode == CreateUpdate.Mode.SUBTRACT) {
+                buffer.append(column).append(" - ");
+            } else if (mode == CreateUpdate.Mode.MULTIPLY) {
+                buffer.append(column).append(" * ");
+            } else if (mode == CreateUpdate.Mode.DIVIDE) {
+                buffer.append(column).append(" / ");
+            }
+            return buffer.append("VALUES(").append(column).append(")");
+        }).filter(Objects::nonNull).collect(Collectors.joining(StringPool.COMMA));
+        return String.format(
+                "<script>\nINSERT IGNORE INTO %s (%s) VALUES\n" +
+                        "<foreach collection=\"collection\" item=\"et\" separator=\",\">(%s)</foreach>\n" +
+                        "ON DUPLICATE KEY UPDATE %s\n</script>",
+                table.getTableName(), columns, properties, updates
+        );
+    }
+}

+ 3 - 1
framework-database/src/main/java/com/chelvc/framework/database/mapper/CustomerSqlInjector.java → framework-database/src/main/java/com/chelvc/framework/database/support/CustomerSqlInjector.java

@@ -1,4 +1,4 @@
-package com.chelvc.framework.database.mapper;
+package com.chelvc.framework.database.support;
 
 import java.util.List;
 
@@ -17,6 +17,8 @@ public class CustomerSqlInjector extends DefaultSqlInjector {
         List<AbstractMethod> methods = super.getMethodList(mapperClass);
         methods.add(new InsertIgnoreMethod());
         methods.add(new InsertUpdateMethod());
+        methods.add(new BatchInsertIgnoreMethod());
+        methods.add(new BatchInsertUpdateMethod());
         return methods;
     }
 }

+ 1 - 1
framework-database/src/main/java/com/chelvc/framework/database/mapper/InsertIgnoreMethod.java → framework-database/src/main/java/com/chelvc/framework/database/support/InsertIgnoreMethod.java

@@ -1,4 +1,4 @@
-package com.chelvc.framework.database.mapper;
+package com.chelvc.framework.database.support;
 
 import java.util.List;
 

+ 8 - 7
framework-database/src/main/java/com/chelvc/framework/database/mapper/InsertUpdateMethod.java → framework-database/src/main/java/com/chelvc/framework/database/support/InsertUpdateMethod.java

@@ -1,4 +1,4 @@
-package com.chelvc.framework.database.mapper;
+package com.chelvc.framework.database.support;
 
 import java.util.List;
 import java.util.Objects;
@@ -7,6 +7,7 @@ import java.util.stream.Collectors;
 import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
 import com.baomidou.mybatisplus.core.metadata.TableInfo;
 import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.database.annotation.CreateUpdate;
 import lombok.NonNull;
 
@@ -32,16 +33,16 @@ public class InsertUpdateMethod extends AbstractInsertMethod {
                 return null;
             }
             String column = field.getColumn();
-            StringBuilder buffer = new StringBuilder().append(column).append(" = ").append(column);
-            CreateUpdate.Mode mode = annotation == null ? CreateUpdate.Mode.REPLACE : annotation.mode();
+            StringBuilder buffer = new StringBuilder().append(column).append(" = ");
+            CreateUpdate.Mode mode = ObjectUtils.ifNull(annotation, CreateUpdate::mode);
             if (mode == CreateUpdate.Mode.ADD) {
-                buffer.append(" + ");
+                buffer.append(column).append(" + ");
             } else if (mode == CreateUpdate.Mode.SUBTRACT) {
-                buffer.append(" - ");
+                buffer.append(column).append(" - ");
             } else if (mode == CreateUpdate.Mode.MULTIPLY) {
-                buffer.append(" * ");
+                buffer.append(column).append(" * ");
             } else if (mode == CreateUpdate.Mode.DIVIDE) {
-                buffer.append(" / ");
+                buffer.append(column).append(" / ");
             }
             return buffer.append("VALUES(").append(column).append(")");
         }).filter(Objects::nonNull).collect(Collectors.joining(StringPool.COMMA));