Backend Development 14 min read

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.

Top Architect
Top Architect
Top Architect
Implementing Multi‑Tenancy with MyBatis‑Plus in a Spring Boot Application

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

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

JavaSpring BootMyBatis-PlusMulti-tenancyDatabase Isolation
Top Architect
Written by

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.

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.