Implementing Multi‑Tenancy with MyBatis‑Plus in a Spring Boot Application
This article explains the concepts of multi‑tenant architecture, compares three data isolation strategies, and provides a complete Spring Boot example that uses MyBatis‑Plus to automatically inject a tenant identifier into SQL queries, including configuration, entity definitions, and unit tests.
Before implementing a multi‑tenant architecture, the article introduces the definition of multi‑tenancy (also known as SaaS) as a software architecture technique that allows multiple customers to share the same application while keeping their data isolated.
Data Isolation Schemes
Three main approaches are described:
Separate databases : each tenant gets its own database, offering the highest isolation but at higher cost.
Shared database, separate schema : tenants share a database but have distinct schemas, providing logical isolation with moderate cost.
Shared database, shared schema, shared tables : all tenants share the same tables and a tenant identifier column (e.g., provider_id ) is added to each row; this has the lowest cost and highest tenant count but the weakest isolation.
Implementing with MyBatis‑Plus
The article chooses the third scheme (shared database, shared schema, shared tables) and shows how to configure MyBatis‑Plus to handle tenant filtering automatically.
SELECT * FROM user t WHERE t.name LIKE '%Tom%' AND t.provider_id = 1;Spring Boot Project Setup
The Maven pom.xml includes Spring Boot starter, MyBatis‑Plus, Lombok, Guava, and H2 database for testing.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
...
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
...
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
...
</dependencies>
</project>The application.yml configures the H2 datasource and logging.
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:test
username: root
password: test
logging:
level:
com.wuwenze.mybatisplusmultitenancy: debugSQL schema files create a user table with a provider_id column.
#schema.sql
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT(20) NOT NULL COMMENT '主键',
provider_id BIGINT(20) NOT NULL COMMENT '服务商ID',
name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
PRIMARY KEY (id)
);
#data.sql
INSERT INTO user (id, provider_id, name) VALUES (1, 1, 'Tony老师');
INSERT INTO user (id, provider_id, name) VALUES (2, 1, 'William老师');
INSERT INTO user (id, provider_id, name) VALUES (3, 2, '路人甲');
...MyBatis‑Plus Configuration
A Spring configuration class defines a TenantSqlParser with a custom TenantHandler that reads the current tenant ID from an ApiContext component.
@Configuration
@MapperScan("com.wuwenze.mybatisplusmultitenancy.mapper")
public class MybatisPlusConfig {
private static final String SYSTEM_TENANT_ID = "provider_id";
private static final List
IGNORE_TENANT_TABLES = Lists.newArrayList("provider");
@Autowired
private ApiContext apiContext;
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
TenantSqlParser tenantSqlParser = new TenantSqlParser()
.setTenantHandler(new TenantHandler() {
@Override
public Expression getTenantId() {
Long currentProviderId = apiContext.getCurrentProviderId();
if (currentProviderId == null) {
throw new RuntimeException("#1129 getCurrentProviderId error.");
}
return new LongValue(currentProviderId);
}
@Override
public String getTenantIdColumn() { return SYSTEM_TENANT_ID; }
@Override
public boolean doTableFilter(String tableName) {
return IGNORE_TENANT_TABLES.stream().anyMatch(e -> e.equalsIgnoreCase(tableName));
}
});
paginationInterceptor.setSqlParserList(Lists.newArrayList(tenantSqlParser));
return paginationInterceptor;
}
@Bean(name = "performanceInterceptor")
public PerformanceInterceptor performanceInterceptor() {
return new PerformanceInterceptor();
}
}The ApiContext stores the current tenant ID in a thread‑safe map.
@Component
public class ApiContext {
private static final String KEY_CURRENT_PROVIDER_ID = "KEY_CURRENT_PROVIDER_ID";
private static final Map
mContext = Maps.newConcurrentMap();
public void setCurrentProviderId(Long providerId) {
mContext.put(KEY_CURRENT_PROVIDER_ID, providerId);
}
public Long getCurrentProviderId() {
return (Long) mContext.get(KEY_CURRENT_PROVIDER_ID);
}
}Entity and Mapper
@Data
@ToString
@Accessors(chain = true)
public class User {
private Long id;
private Long providerId;
private String name;
}
public interface UserMapper extends BaseMapper
{}Unit Tests
The test class sets the tenant ID in ApiContext , inserts a new user, and verifies that the provider_id is automatically populated and that queries are correctly filtered.
@Slf4j
@RunWith(SpringRunner.class)
@FixMethodOrder(MethodSorters.JVM)
@SpringBootTest(classes = MybatisPlusMultiTenancyApplication.class)
public class MybatisPlusMultiTenancyApplicationTests {
@Autowired
private ApiContext apiContext;
@Autowired
private UserMapper userMapper;
@Before
public void before() {
apiContext.setCurrentProviderId(1L);
}
@Test
public void insert() {
User user = new User().setName("新来的Tom老师");
Assert.assertTrue(userMapper.insert(user) > 0);
user = userMapper.selectById(user.getId());
log.info("#insert user={}", user);
Assert.assertEquals(apiContext.getCurrentProviderId(), user.getProviderId());
}
@Test
public void selectList() {
userMapper.selectList(null).forEach(e -> {
log.info("#selectList, e={}", e);
Assert.assertEquals(apiContext.getCurrentProviderId(), e.getProviderId());
});
}
}Running the tests shows that the tenant filter works perfectly: INSERT statements automatically include provider_id = 1 , and SELECT statements are scoped to the current tenant.
Overall, the guide demonstrates a clean, low‑maintenance way to achieve multi‑tenant data isolation in a Spring Boot project using MyBatis‑Plus.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.