Elegant Multi‑Tenant Data Isolation with MyBatis‑Plus in SaaS Systems
This article explains the concept of SaaS multi‑tenant architecture, compares three data‑isolation designs, and demonstrates how to implement elegant tenant data isolation in Java using MyBatis‑Plus’s tenant plugin, including configuration, code examples, and practical considerations.
Developers of SaaS platforms are familiar with the concept of multi‑tenant architecture, where each tenant represents a separate company sharing the same application instance. When the SaaS system becomes unavailable, all tenants are affected, similar to a building where a broken elevator disrupts all office floors.
Multi‑tenant design aims to run a single SaaS system on one or a group of servers while providing data isolation between tenants. Because tenant data is centrally stored, achieving security relies on isolating each tenant’s data to prevent accidental or malicious access.
What Is a SaaS System?
A SaaS platform supplies all network infrastructure, software, and hardware services required by enterprises, allowing tenants to use the system without purchasing hardware or hiring IT staff. Tenants pay rent to access the platform’s functional services, typical of cloud platforms and service providers.
Multi‑Tenant Data Isolation Architecture Designs
There are three common architectural approaches for data isolation in SaaS systems:
Separate Database per Tenant : Each tenant uses an independent database, offering high isolation and security but increasing hardware and maintenance costs.
Separate Table Space : Tenants share a single database instance but have distinct table spaces, providing moderate isolation and cost.
Tenant‑ID Field Isolation : A tenant identifier column (e.g., tenant_id or org_id ) is added to every table, and queries filter by this column. This method has the lowest cost and highest tenant capacity but the weakest isolation.
The table below summarizes the trade‑offs:
Isolation Scheme
Cost
Supported Tenants
Advantages
Disadvantages
Separate Database
High
Few
High isolation, security, per‑tenant customization
High physical and maintenance cost
Separate Table Space
Medium
More
Logical isolation, multiple tenants per DB
Complex DB management, many tables
Tenant‑ID Field
Low
Many
Lowest cost, highest tenant count
Lowest isolation and security
Most companies adopt the third approach—tenant‑ID field isolation—due to its low cost and scalability.
Elegant Multi‑Tenant Data Permission Isolation with MyBatis‑Plus
MyBatis‑Plus provides a TenantLineInnerInterceptor that automatically appends the tenant ID condition to every SQL statement, ensuring data isolation without manual query modifications.
The core interceptor class looks like this:
public class TenantLineInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {
// Multi‑tenant handler
private TenantLineHandler tenantLineHandler;
// Modify SQL to add tenant ID condition
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (!InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(this.parserSingle(mpBs.sql(), null));
}
}
// ... other methods omitted for brevity
}The TenantLineHandler interface must be implemented to provide tenant‑specific configuration:
public class TenantDatabaseHandler implements TenantLineHandler {
private final Set
ignoreTables = new HashSet<>();
public TenantDatabaseHandler(TenantProperties properties) {
properties.getIgnoreTables().forEach(table -> {
ignoreTables.add(table.toLowerCase());
ignoreTables.add(table.toUpperCase());
});
}
@Override
public String getTenantIdColumn() { return "org_id"; }
@Override
public Expression getTenantId() { return new LongValue(RequestUserHolder.getCurrentUser().getOrgId()); }
@Override
public boolean ignoreTable(String tableName) { return CollUtil.contains(ignoreTables, tableName); }
}Configuration properties are defined as:
@ConfigurationProperties(prefix = "ptc.tenant")
@Data
public class TenantProperties {
private Boolean enable = Boolean.TRUE;
private Set
ignoreTables = Collections.emptySet();
}Register the interceptor in a Spring configuration:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties properties) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
if (properties.getEnable()) {
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantDatabaseHandler(properties)));
}
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}Example MyBatis XML query ( getUserList() ) demonstrates how the tenant condition is automatically added to both user and user_role tables:
<select id="getUserList" resultType="com.plasticene.textile.entity.User">
select u.* from user u
left join user_role r on u.id = r.user_id
<where>
<if test="query.status != null">and u.status = #{query.status}</if>
<if test="query.roleId != null">and r.role_id = #{query.roleId}</if>
<if test="query.keyword != null">and ((u.name like concat('%',#{query.keyword},'%')) or (u.mobile like concat(#{query.keyword},'%')))</if>
<if test="query.startEntryTime != null">and u.entry_time >= #{query.startEntryTime}</if>
<if test="query.endEntryTime != null">and u.entry_time <= #{query.endEntryTime}</if>
</where>
group by u.id
order by u.id desc
</select>Running the application shows the generated SQL with the tenant filter, e.g., WHERE u.org_id = 3 . To exclude a table from tenant filtering, add it to ptc.tenant.ignore-tables in the configuration. Note that MySQL treats user as a keyword, so using backticks (`` `user` ``) may bypass the ignore logic because the handler matches only plain table names.
If a specific query should ignore tenant filtering altogether, annotate the mapper method with @InterceptorIgnore(tenantLine = "true") .
The underlying parser used by the plugin is JSqlParser . Certain alias names (e.g., ur ) can cause parsing failures, as reported in MyBatis‑Plus issue #5086. Updating to a newer version of JSqlParser or avoiding problematic aliases resolves the issue.
Conclusion
The article covered multi‑tenant system data isolation strategies, architectural designs, and a practical, elegant implementation using MyBatis‑Plus’s tenant plugin. While the example focuses on tenant‑level isolation, the same pattern can be extended to role‑based or department‑level data permissions by adding additional identifier columns and custom handlers.
For deeper data‑permission implementations, developers can mimic the tenant‑plugin approach to enforce hierarchical access controls such as company → department → team → user.
Promotional Note: The author encourages readers to like, follow, share, and bookmark the article, and mentions a paid knowledge community offering advanced Spring, MyBatis, and distributed system tutorials.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.