Mybatis Plus 实现自定义规则的动态表名

890 阅读2分钟

实现多租户系统的动态表名方案

背景

在实现多租户系统时,一种常见的方案是采用同库不同表的策略。这种方法根据当前登录用户所属的租户,动态地选择不同的表进行查询操作。本文将介绍一种优雅的实现方式,通过自定义注解和插件来实现动态表名的功能。

实现方案

我们引入了一个自定义注解@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仓库 中找到。希望这个方案能为您的多租户系统开发提供一些思路和帮助。

如果您有任何问题或建议,欢迎在评论区留言讨论。