ZenWeb Tenant module
多租户多数据库支持 —— 多个租户共享数据库连接池,按请求动态切换数据库。
工作原理
传统多租户方案通常为每个租户创建独立的数据库连接池,当租户数量增多时会耗尽连接资源。本模块采用共享连接池策略:
- 同一台数据库服务器上的所有租户共用一个连接池
- 获取连接后通过
USE <数据库名>切换到对应租户的数据库 - 按服务器维度配置连接池,而非按租户维度
请求进来 → tenantGetter 识别租户 → 从对应服务器的连接池取连接 → USE db_name → 执行查询
安装
npm install @zenweb/tenant
快速开始
编辑项目入口代码 src/index.ts
src/index.ts
import { create } from 'zenweb';
import modTenant, { Tenant } from '@zenweb/tenant';
export const app = create();
// 租户映射表(实际项目中通常从数据库或配置中心读取)
const tenants: Record<string, Tenant> = {
'a.example.com': { server: 'S1', database: 'db_a' },
'b.example.com': { server: 'S1', database: 'db_b' },
'c.example.com': { server: 'S2', database: 'db_c' },
};
app.setup(modTenant({
// 从请求上下文中识别当前租户
tenantGetter: ctx => {
const tenant = tenants[ctx.host];
if (!tenant) throw new Error('租户不存在: ' + ctx.host);
return tenant;
},
// 按数据库服务器配置连接池
pools: {
S1: {
MASTER: {
host: '127.0.0.1',
port: 3306,
user: 'root',
password: '123456',
charset: 'utf8mb4',
timezone: '+08:00',
connectionLimit: 100,
},
},
S2: {
MASTER: {
host: '192.168.1.100',
port: 3306,
user: 'root',
password: 'secret',
charset: 'utf8mb4',
timezone: '+08:00',
connectionLimit: 50,
},
},
},
}));
app.start();
配置说明
tenantGetter
tenantGetter: (ctx: Context) => Tenant | Promise<Tenant>
从请求上下文中确定当前租户,返回包含 server 和 database 的对象。识别方式由你决定,常见的有:
- 域名区分:
ctx.host - 请求头区分:
ctx.headers['x-tenant-id'] - 路径前缀区分:
ctx.path.split('/')[1] - Token 区分: 解析 JWT 中的租户字段
pools
pools: {
[服务器名称: string]: {
MASTER: PoolOptions; // 必填,读写库
SLAVES?: PoolOptions[]; // 可选,只读从库,可配多个
}
}
- 服务器名称是自定义的标识符,与
Tenant.server对应 - MASTER 是必须配置的读写库
- SLAVES 是可选的只读从库列表,配置后读操作会自动路由到从库
可选配置
| 参数 | 说明 |
|---|---|
bindQuery | ZenORM 的 bindQuery 方法,传入后可直接使用 Model.find() 等方法 |
cluster | mysql2 的 PoolClusterOptions,连接池集群配置 |
数据库查询
配置完成后,在控制器中通过 ctx.mysql 进行查询:
src/controller/demo.ts
import { Get, Context } from 'zenweb';
export class DemoController {
@Get()
async list(ctx: Context) {
// 默认使用主库(MASTER)
const rows = await ctx.mysql.query('SELECT * FROM users');
// 显式指定主库
const rows2 = await ctx.mysql.of('MASTER').query('SELECT * FROM users');
// 使用从库(未配置从库时自动回退到主库)
const rows3 = await ctx.mysql.of('SLAVE*').query('SELECT * FROM users');
return rows;
}
}
[可选] 集成 ZenORM
安装
# 生产依赖
npm install zenorm
# 开发依赖
npm install --save-dev @zenorm/generate @zenorm/generate-mysql
配置生成脚本
在 package.json 的 scripts 中增加:
package.json
{
"scripts": {
"dbgen": "zenorm-generate .dbgen.js"
}
}
创建文件 .dbgen.js 用于生成数据库结构代码时连接到指定数据库:
.dbgen.js
/** @type {import("@zenorm/generate").GenerateConfig} */
module.exports = {
host: "localhost",
port: 3306,
user: "root",
password: "",
database: "my_database",
outputDir: "./model",
bindQuery: true,
};
运行命令生成数据库结构代码:
npm run dbgen
传入 bindQuery
编辑项目入口代码,将生成的 bindQuery 传入模块配置:
src/index.ts
import { bindQuery } from './model';
app.setup(modTenant({
bindQuery, // 传入后 ORM 模型可直接查询
tenantGetter,
pools,
}));
使用模型查询
src/controller/user.ts
import { Get } from 'zenweb';
import { User } from '../model';
export class UserController {
@Get()
async list() {
return await User.find().all();
}
@Get(':id')
async detail({ id }) {
return await User.find({ id }).get();
}
}