公共工具类设计之《MongoDB 动态分表组件》

在实际的各种业务中,存储分表的需求是比较容易涉及到的,在MongoDB的场景中,如何设计一个动态表组件,满足下面的要求呢?

  1. 灵活支持各种的的分表策略,例如后缀,前缀模式,以及支持分组、哈希等各种分表规则。
  2. 兼容已有的业务,实现无感知的切换,无需修改任何业务侧的代码。

话不多说,直入主题。

整体方案

  1. 基于Properties 配置动态表参数,使用spel表达式实现自定义要求。
  2. 继承 MongoTemplate,实现各SQL方法,添加分表逻辑,业务层无需做任何改动。

分表配置

使用spring.properties进行配置,支持spel表达式。 按照示例的分表配置,订单表site_order,使用后缀策略,分表的规则使用id字段取模

  • id = 1的数据将存到 site_order_1表
  • id = 5的数据将存到site_order_2表

商品表使用后缀策略,按照租户字段,单独进行分组操作

  • tenantId = 2 的数据将存到 product_2表
  • tenantId = 8的数据将存到product_8表中
yaml 复制代码
spring:  
  data:  
    mongodb:  
      uri: mongodb://root:123456@localhost:27017/admin
      dynamic-collections:  
        - collection: site_order  
          strategy: SUFFIX  
          field: "#root.id % 3"  #基于id哈希分组进行分表
        - collection: product  
          strategy: SUFFIX  
          field: "tenantId"  #每个租户ID单独使用一个表

DynamicProperties 读取分表配置

java 复制代码
@Configuration  
@EnableConfigurationProperties  
@ConfigurationProperties(prefix = "spring.data.mongodb")  
public class DynamicMongoProperties {  

	private List<DynamicCollection> dynamicCollections = new ArrayList<>();  
	  
	private volatile Map<String, DynamicCollection> dynamicCollectionMap = new HashMap<>();
  
    public void setDynamicCollections(List<DynamicCollection> dynamicCollections) {  
        this.dynamicCollections = dynamicCollections;  
    }  


	public Map<String, DynamicCollection> getDynamicCollections() {  
	    if (dynamicCollections == null || dynamicCollections.isEmpty()) {  
	        return dynamicCollectionMap;  
	    }  
	    if (dynamicCollectionMap.isEmpty()) {  
	        synchronized (dynamicCollectionMap) {  
	            if (dynamicCollectionMap.isEmpty()) {  
	                dynamicCollectionMap = dynamicCollections.stream()  
	                        .collect(Collectors.toMap(DynamicCollection::getCollection, c -> c));  
	            }  
	        }  
	    }  
	    return dynamicCollectionMap;  
	}
  
    @Data  
    public static class DynamicCollection {  
        private String collection;  
        private Strategy strategy = Strategy.SUFFIX;  
        private String field;  
    }  
  
}

public enum Strategy {  
    SUFFIX  
}

DynamicMongoTemplate 实现 MongoTemplate

需要重写MongoTemplate里sql相关的操作,但是这里有一个限制,只能重写参数包含obj对象或者Query对象的,因为要获取动态表名必须要知道分表键的值,像findById这样的只传入了一个id,对于获取动态表名来说信息不够。 下面只给了insert 、findById 、 findOne的实现,其他的方法都可以参考这3个进行实现。

java 复制代码
  
public class DynamicMongoTemplate extends MongoTemplate {  
  
    private DynamicMongoProperties dynamicMongoProperties;  
  
    public DynamicMongoTemplate(MongoDatabaseFactory mongoDatabaseFactory,    
                                DynamicMongoProperties dynamicMongoProperties) {  
        super(mongoDatabaseFactory);  
        this.dynamicMongoProperties = dynamicMongoProperties;  
    }  
  
    @Override  
    public String getCollectionName(Class<?> entityClass) {  
        DynamicCollection dynamicCollection = this.getDynamicCollection(entityClass);  
        return super.getCollectionName(entityClass) + "_" + dynamicCollection.getField();  
    }  
  
    @Override  
    public <T> T findById(Object id, Class<T> entityClass) {  
        throw new RuntimeException("DynamicMongoTemplate unsupported findById");  
    }  
  
    @Override  
    public <T> T findOne(Query query, Class<T> entityClass) {  
        String dynamicCollectionName = this.getDynamicCollectionName(query, entityClass);  
        return super.findOne(query, entityClass, dynamicCollectionName);  
    }  
  
    @Override  
    public <T> T insert(T objectToSave) {  
        String dynamicCollectionName = this.getDynamicCollectionName(objectToSave);  
        return super.insert(objectToSave, dynamicCollectionName);  
    }  
  
    private <T> String getDynamicCollectionName(Query query, Class<T> entityClass) {  
        DynamicCollection dynamicCollection = getDynamicCollection(entityClass);  
        if (dynamicCollection == null) {  
            throw new RuntimeException("unknown entityClass " + entityClass.getName());  
        }  
        String collection = dynamicCollection.getCollection();  
        String field = dynamicCollection.getField();  
        String fieldValue = "";  
        try {  
            fieldValue = getFieldValue(query, field, entityClass);  
        } catch (InstantiationException | IllegalAccessException e) {  
            throw new RuntimeException("entityClass (" + entityClass.getName()  
                    + ") cant be instantiated");  
        }  
        return generateCollectionName(collection, fieldValue, dynamicCollection.getStrategy());  
    }  
  
    private <T> String getDynamicCollectionName(T obj) {  
        DynamicCollection dynamicCollection = getDynamicCollection(obj.getClass());  
        if (dynamicCollection == null) {  
            return null;  
        }  
        String collection = dynamicCollection.getCollection();  
        String field = dynamicCollection.getField();  
        String fieldValue = getFieldValue(obj, field);  
        return generateCollectionName(collection, fieldValue, dynamicCollection.getStrategy());  
    }  
  
    private <T> String getFieldValue(Query query, String field, Class<T> entityClass) throws InstantiationException, IllegalAccessException {  
        T t = entityClass.newInstance();  
  
        BeanWrapperImpl beanWrapper = new BeanWrapperImpl(t);  
        query.getQueryObject().entrySet().forEach(entry ->  
                beanWrapper.setPropertyValue(entry.getKey(), entry.getValue()));  
  
        return getFieldValue(t, field);  
    }  
  
    private String getFieldValue(Object obj, String field) {  
        ExpressionParser parser = new SpelExpressionParser();  
        Expression exp = parser.parseExpression(field);  
        EvaluationContext context = new StandardEvaluationContext(obj);  
        return exp.getValue(context, String.class);  
    }  
  
    private String generateCollectionName(String originCollectionName,  
                                          String fieldValue, Strategy strategy) {  
        if (!StringUtils.hasText(fieldValue)) {  
            throw new RuntimeException(  
                    "dynamicCollection field missing. collection = " + originCollectionName);  
        }  
        switch (strategy) {  
            case SUFFIX -> {  
                return originCollectionName + "_" + fieldValue;  
            }  
            default -> throw new RuntimeException("unknown dynamicCollection strategy " + strategy);  
        }  
    }  
  
    private DynamicCollection getDynamicCollection(Class<?> entityClass) {  
        return dynamicMongoProperties.getDynamicCollections()  
                .get(super.getCollectionName(entityClass));  
    }  
  
}

DynamicMongoConfig

最后一步要做的就是提供给业务侧使用动态表名逻辑了,本质就是使用DynamicMongoTemplate覆盖MongoTemplate,业务侧无感知的进行切换。这里可以使用Configuration类配置:

java 复制代码
@Configuration  
public class DynamicMongoConfig {  
  
    @Bean  
    public DynamicMongoTemplate mongoTemplate(MongoDatabaseFactory mongoDatabaseFactory,  
                                              DynamicMongoProperties dynamicMongoProperties) {  
        return new DynamicMongoTemplate(mongoDatabaseFactory, dynamicMongoProperties);  
    }  
  
}

添加这3个类后,项目就已经具备动态表名的能力了。

相关推荐
Aric_Jones1 小时前
lua入门语法,包含安装,注释,变量,循环等
java·开发语言·git·elasticsearch·junit·lua
Akiiiira1 小时前
【日撸 Java 三百行】Day 12(顺序表(二))
java·开发语言
Chase_Mos5 小时前
Spring 必会之微服务篇(1)
java·spring·微服务
懵逼的小黑子6 小时前
Django 项目的 models 目录中,__init__.py 文件的作用
后端·python·django
小林学习编程7 小时前
SpringBoot校园失物招领信息平台
java·spring boot·后端
撸码到无法自拔7 小时前
docker常见命令
java·spring cloud·docker·容器·eureka
heart000_17 小时前
IDEA 插件推荐:提升编程效率
java·ide·intellij-idea
ŧ榕树先生8 小时前
查看jdk是否安装并且配置成功?(Android studio安装前的准备)
java·jdk
未来的JAVA高级开发工程师8 小时前
适配器模式
java
LUCIAZZZ8 小时前
JVM之内存管理(一)
java·jvm·spring·操作系统·springboot