Backend Development 14 min read

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.

Architecture Digest
Architecture Digest
Architecture Digest
Implementing Multi‑Tenancy with MyBatis‑Plus in a Spring Boot Application

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-plugin

application.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: debug

schema.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.

JavaSQLSpring BootMyBatis-PlusMulti-tenancy
Architecture Digest
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.