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">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.wuwenze</groupId>
    <artifactId>mybatis-plus-multi-tenancy</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.0.RELEASE</version>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>19.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

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<String> 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<String, Object> 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<User> {}

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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

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.