租户模型设计:从实体定义到层级关系
好,咱们今天来聊聊租户模型设计。这是多租户系统的地基,地基没打好,后面盖什么楼都晃悠。我在好几个项目里都吃过租户模型设计不清晰的亏,所以这块我特别想跟你好好说说。
租户实体定义:到底什么是“租户”?
先别笑,这个问题真得想清楚。很多人一上来就写代码,结果租户和用户、租户和组织混成一锅粥。
我个人习惯把租户定义为:一个独立的业务运营单元。它拥有自己的数据、配置、用户和资源。说白了,租户就是一个“小王国”,里面的人、权限、数据都是隔离的。
来看一个最简的租户实体定义:
// 租户核心实体
public class Tenant {
private String tenantId; // 租户唯一标识
private String tenantName; // 租户名称
private String tenantCode; // 租户编码(业务用)
private String tenantType; // 租户类型
private String status; // 状态
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
嗯,这里要注意:tenantId 和 tenantCode 是两回事。前者是系统内部用的 UUID,后者是业务上可读的编码,比如“ACME_CORP”。我见过有人把这两个混用,结果业务方要求改编码时,整个系统都得跟着改,那叫一个酸爽。
租户属性设计:别只盯着那几个字段
租户的属性远不止名字和编码。你想想看,一个租户需要配置多少东西?
我在项目中遇到过最头疼的情况:租户属性写死在代码里,每次新增属性都要改表结构、改代码、重新部署。后来我学乖了,用扩展属性来解决。
推荐两种方案:
| 方案 | 适用场景 | 优缺点 |
|---|---|---|
| 固定字段 + JSON扩展 | 核心属性固定,少量扩展属性 | 查询方便,但JSON字段不好做索引 |
| EAV模型(实体-属性-值) | 属性频繁变化,每个租户属性差异大 | 灵活,但查询复杂,性能差一些 |
我个人更倾向第一种。核心属性用固定字段,比如:
public class Tenant {
// 核心字段
private String tenantId;
private String tenantName;
private String tenantCode;
private String status;
// 扩展属性(JSON格式)
private String extraProperties;
// 示例:{"logoUrl":"xxx","contactPhone":"138xxxx","industry":"IT"}
}
租户状态管理:别让租户“死”得不明不白
租户的状态管理,说白了就是租户的生命周期。我见过最粗暴的做法:只有“启用”和“禁用”两个状态。结果租户欠费了、到期了、被投诉了,全都混在一起处理。
我建议至少设计这几个状态:
- 待激活:刚注册,还没完成配置
- 试用中:可以正常使用,但有时间或功能限制
- 已激活:正式付费用户,所有功能开放
- 已冻结:欠费或违规,暂时限制访问
- 已注销:租户主动退出,数据保留一段时间
- 已删除:数据彻底清理(通常是软删除)
状态流转图大概是这样的:
待激活 → 试用中 → 已激活 → 已冻结 → 已注销 → 已删除
↑ ↓
└────────┘ (解冻后回到已激活)
租户层级关系:单层还是多层?
这个问题其实取决于你的业务场景。我见过三种常见的层级模型:
- 扁平模型:所有租户平级,没有父子关系。适合SaaS工具类产品。
- 树形模型:租户有父子关系,比如集团-子公司-部门。适合企业级应用。
- 混合模型:既有平级租户,也有层级租户。适合平台型产品。
我个人习惯用树形模型 + 路径枚举的方式。为什么?因为查询某个租户的所有子租户时,性能特别好。
来看具体实现:
public class Tenant {
private String tenantId;
private String parentId; // 父租户ID,根租户为null
private String tenantPath; // 路径枚举,如 "/root/child/grandchild"
private Integer level; // 层级深度,根为0
private String status;
}
// 查询某个租户的所有子租户
SELECT * FROM tenant
WHERE tenant_path LIKE CONCAT(#{tenantPath}, '/%')
AND status = 'ACTIVATED';
路径枚举的好处是:查询时只需要一次 LIKE 操作,不需要递归。坏处是:移动租户时需要更新所有子租户的路径。不过在实际业务中,租户移动的场景很少,所以这个代价可以接受。
核心要点:
- 租户实体要区分内部ID和业务编码
- 扩展属性用JSON字段,别动不动就改表结构
- 状态管理要细化,每个状态有独立逻辑
- 层级关系推荐路径枚举,查询性能好
好了,租户模型设计这块就聊到这儿。下一章咱们会讲租户的创建流程和初始化策略,到时候你会看到这些模型是怎么在实际代码里跑起来的。有什么问题欢迎随时交流。