在实际的各种业务中,存储分表的需求是比较容易涉及到的,在MongoDB的场景中,如何设计一个动态表组件,满足下面的要求呢?
- 灵活支持各种的的分表策略,例如后缀,前缀模式,以及支持分组、哈希等各种分表规则。
- 兼容已有的业务,实现无感知的切换,无需修改任何业务侧的代码。
话不多说,直入主题。
整体方案
- 基于Properties 配置动态表参数,使用spel表达式实现自定义要求。
- 继承 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个类后,项目就已经具备动态表名的能力了。