实现多租户系统的动态表名方案
背景
在实现多租户系统时,一种常见的方案是采用同库不同表的策略。这种方法根据当前登录用户所属的租户,动态地选择不同的表进行查询操作。本文将介绍一种优雅的实现方式,通过自定义注解和插件来实现动态表名的功能。
实现方案
我们引入了一个自定义注解@DynamicTableName,用于标注动态表名的规则。该注解包含两个内置参数:
tableName: 表示当前表名tenant: 代表租户ID经过排序后的字符串
例如,如果我们标注@DynamicTableName("biz_#{#tenant}_#{#tableName}")在一个实体类上,当前表名为person,租户序列为aa,那么最终生成的动态表名将是biz_aa_person。
这种方案的优势在于,我们可以使用同一个Entity类实现对不同租户表的查询,大大提高了代码的复用性和灵活性。
核心代码实现
1. 自定义注解
首先,我们定义@DynamicTableName注解:
@Documented
@Retention(RUNTIME)
@Target(TYPE)
public @interface DynamicTableName {
String dynamicExpression() default "";
}
2. 动态表名处理器
接下来,我们实现TableNameHandler接口来处理动态表名:
public class DynamicTableNameHandler implements TableNameHandler {
@Override
public String dynamicTableName(String sql, String tableName) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
if (tableInfo == null) {
return tableName;
}
Class<?> entityType = tableInfo.getEntityType();
DynamicTableName annotation = entityType.getAnnotation(DynamicTableName.class);
if (annotation != null) {
return DynamicTableNameUtils.parseTableName(tableInfo.getTableName(), annotation.dynamicExpression());
} else {
return tableName;
}
}
}
3. 表名解析工具
为了解析动态表达式,我们创建了DynamicTableNameUtils工具类:
public abstract class DynamicTableNameUtils {
public static String parseTableName(String tableName, String paramExpression) {
StandardEvaluationContext context = new StandardEvaluationContext();
ExpressionParser parser = new SpelExpressionParser();
context.setVariable("tableName", tableName);
context.setVariable("tenant", resolveTenantId(UserContext.getTenantId()));
Expression expr = parser.parseExpression(paramExpression, new TemplateParserContext());
return expr.getValue(context, String.class);
}
// ... 其他辅助方法 ...
}
使用示例
定义动态用户表
@Data
@EqualsAndHashCode(callSuper = true)
@DynamicTableName(dynamicExpression = "biz_#{#tenant}_#{#tableName}")
public class DynamicUser extends Model<DynamicUser> {
private Long id;
private String position;
private String dept;
}
测试用例
我们编写了一个完整的测试用例来验证动态表名的功能:
@Slf4j
@RunWith(SpringRunner.class)
@TestPropertySource(locations = "classpath:application-dynamic.properties")
@ContextConfiguration(classes = {
DataSourceAutoConfiguration.class,
MybatisPlusAutoConfiguration.class,
OrmMpConfiguration.class,
TestConfig.class,
})
@SpringBootTest(classes = {DynamicTableNameTest.class})
public class DynamicTableNameTest {
// ... 测试方法和辅助方法 ...
@Test
public void testDynamicTableName() {
// 模拟租户1
MockSessionUser mockSessionUser = new MockSessionUser();
mockSessionUser.setId(1L);
mockSessionUser.setTenantId(1L);
UserContext.setUser(mockSessionUser);
List<DynamicUser> list = dynamicUserService.list();
Assert.assertEquals(2, list.size());
// 模拟租户2
MockSessionUser mockSessionUser2 = new MockSessionUser();
mockSessionUser2.setId(2L);
mockSessionUser2.setTenantId(37L);
UserContext.setUser(mockSessionUser2);
List<DynamicUser> list2 = dynamicUserService.list();
Assert.assertEquals(1, list2.size());
}
}
总结
通过实现动态表名,我们巧妙地解决了多租户系统中的数据隔离问题。这种方案不仅保证了数据的安全性,还提高了系统的扩展性和维护性。开发人员可以使用同一套代码来处理不同租户的数据,大大简化了开发流程。
本文介绍的实现方式基于自定义注解和SpEL表达式,具有很高的灵活性。您可以根据实际需求进行定制和扩展。
完整的代码示例可以在 GitHub仓库 中找到。希望这个方案能为您的多租户系统开发提供一些思路和帮助。
如果您有任何问题或建议,欢迎在评论区留言讨论。