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

相关推荐
言慢行善11 小时前
sqlserver模糊查询问题
java·数据库·sqlserver
专吃海绵宝宝菠萝屋的派大星11 小时前
使用Dify对接自己开发的mcp
java·服务器·前端
大数据新鸟12 小时前
操作系统之虚拟内存
java·服务器·网络
Tong Z12 小时前
常见的限流算法和实现原理
java·开发语言
凭君语未可12 小时前
Java 中的实现类是什么
java·开发语言
He少年12 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
克里斯蒂亚诺更新12 小时前
myeclipse的pojie
java·ide·myeclipse
迷藏49412 小时前
**eBPF实战进阶:从零构建网络流量监控与过滤系统**在现代云原生架构中,**网络可观测性**和**安全隔离**已成为
java·网络·python·云原生·架构
迷藏49412 小时前
**发散创新:基于Solid协议的Web3.0去中心化身份认证系统实战解析**在Web3.
java·python·web3·去中心化·区块链