Implementing Multi‑Tenancy with MyBatis‑Plus in a Spring Boot Application
This article explains the concept of multi‑tenancy, compares three data‑isolation strategies, and demonstrates how to configure MyBatis‑Plus, Spring Boot, and H2 to achieve transparent tenant filtering and automatic tenant‑ID handling using a custom SQL parser and context component.
What is Multi‑Tenancy
Multi‑tenancy (also called SaaS) is a software architecture technique that allows a single application instance to serve multiple customers (tenants) while keeping each tenant's data isolated.
Data Isolation Schemes
There are three common ways to isolate tenant data:
Separate Database
Pros: highest isolation and security; each tenant has its own database.
Cons: higher cost and maintenance overhead.
Shared Database, Separate Schema
Pros: logical isolation with a single database; supports more tenants than separate databases.
Cons: recovery is more complex because schemas are shared.
Shared Database, Shared Schema, Shared Table (Tenant ID Column)
Pros: lowest cost and highest tenant capacity; only one table per entity with a provider_id column.
Cons: lowest isolation; requires careful handling of the tenant column for security and backup.
Implementation with MyBatis‑Plus
The article chooses the third scheme (shared database, shared schema, shared table) and uses MyBatis‑Plus to inject the tenant filter automatically.
SQL Example
SELECT * FROM user t WHERE t.name LIKE '%Tom%' AND t.provider_id = 1;Manually adding the tenant condition to every query is error‑prone, so MyBatis‑Plus provides a TenantSqlParser to do it automatically.
Setting Up the Spring Boot Environment
POM file (relevant dependencies)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
com.wuwenze
mybatis-plus-multi-tenancy
0.0.1-SNAPSHOT
jar
org.springframework.boot
spring-boot-starter-parent
2.1.0.RELEASE
UTF-8
1.8
org.springframework.boot
spring-boot-starter
org.projectlombok
lombok
provided
com.google.guava
guava
19.0
com.baomidou
mybatis-plus-boot-starter
3.0.5
com.h2database
h2
org.springframework.boot
spring-boot-maven-pluginapplication.yml (H2 datasource)
spring:
datasource:
driver-class-name: org.h2.Driver
schema: classpath:db/schema.sql
data: classpath:db/data.sql
url: jdbc:h2:mem:test
username: root
password: test
logging:
level:
com.wuwenze.mybatisplusmultitenancy: debugschema.sql
#schema.sql
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT(20) NOT NULL COMMENT 'Primary Key',
provider_id BIGINT(20) NOT NULL COMMENT 'Tenant ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT 'Name',
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, '路人甲');
INSERT INTO user (id, provider_id, name) VALUES (4, 2, '路人乙');
INSERT INTO user (id, provider_id, name) VALUES (5, 2, '路人丙');
INSERT INTO user (id, provider_id, name) VALUES (6, 2, '路人丁');MyBatis‑Plus Configuration
The core is a TenantSqlParser that reads the current tenant ID from a custom ApiContext and injects it into every SQL statement.
@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();
}
}ApiContext
@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 tests set the tenant ID in ApiContext , insert a new user, and verify that the tenant column is automatically populated and that queries are automatically 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());
});
}
}The console output shows that INSERT statements automatically include provider_id = 1 , SELECT statements add the tenant filter, and the test assertions pass, confirming that the multi‑tenancy solution works transparently.
Conclusion
By configuring a TenantSqlParser and a simple ApiContext , developers can achieve perfect data isolation for multi‑tenant SaaS applications with minimal code changes, low cost, and high security.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.