公共工具类设计之《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个类后,项目就已经具备动态表名的能力了。

相关推荐
Theodore_10223 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
冰帝海岸4 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象5 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了5 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
小二·5 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic6 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
懒洋洋大魔王6 小时前
RocketMQ的使⽤
java·rocketmq·java-rocketmq
武子康6 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
qq_17448285756 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
转世成为计算机大神6 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式