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

相关推荐
莹雨潇潇几秒前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
杨哥带你写代码19 分钟前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
郭二哈1 小时前
C++——模板进阶、继承
java·服务器·c++
A尘埃1 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23071 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
沉登c1 小时前
幂等性接口实现
java·rpc
Marst Code1 小时前
(Django)初步使用
后端·python·django
代码之光_19801 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
编程老船长1 小时前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
IT果果日记2 小时前
DataX+Crontab实现多任务顺序定时同步
后端