应通过current_setting('app.tenant_id', true)安全读取会话变量获取租户ID,禁止依赖CURRENT_USER或查表反推;应用层须显式SET,触发器仅轻量读取与校验。

触发器里怎么拿到当前租户 ID?
多数多租户系统会在连接层或应用层通过 SET LOCAL 或会话变量(如 current_tenant_id)注入租户标识。PostgreSQL 支持 current_setting('app.tenant_id', true) 安全读取;MySQL 8.0+ 可用 @tenant_id,但需确保应用每次写入前已显式设置。别依赖 USER() 或 CURRENT_USER —— 它们反映的是数据库账号,不是业务租户。
常见错误:在触发器里直接查 tenant_mapping 表反推租户,这会引发递归或锁等待。正确做法是租户 ID 必须由上层传入会话上下文,触发器只做轻量读取和路由判断。
如何用触发器动态 INSERT 到不同物理表?
原生 SQL 触发器不支持拼接表名执行 INSERT INTO table_name,必须用动态 SQL(即 EXECUTE)。PostgreSQL 中需配合 format() 和 quote_ident() 防注入;MySQL 的 PREPARE/EXECUTE 不支持在触发器内使用,所以这条路在 MySQL 上走不通 —— 这点容易被忽略。
示例(PostgreSQL):
CREATE OR REPLACE FUNCTION route_by_tenant()
RETURNS TRIGGER AS $$
DECLARE
tenant_id TEXT := current_setting('app.tenant_id', true);
target_table TEXT := format('orders_tenant_%s', tenant_id);
BEGIN
EXECUTE format('INSERT INTO %I VALUES ($1.*)', target_table) USING NEW;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
注意:RETURN NULL 是关键,否则原表也会插入一行;%I 确保表名被双引号包裹,避免关键字冲突。
为什么不能在触发器里做租户表自动创建?
触发器执行时处于事务中,而 CREATE TABLE 是 DDL,在 PostgreSQL 中会隐式提交当前事务,导致触发器报错 ERROR: cannot execute CREATE TABLE in a read-only transaction;MySQL 更严格,直接禁止 DDL。
可行方案只有两个:
- 租户表必须预先建好(推荐,配合 CI 或租户注册流程生成)
- 把建表逻辑提到应用层或独立的管理服务中,触发器只负责路由
试图在触发器里 IF NOT EXISTS 检查并建表,99% 会失败,且带来不可控的锁和性能抖动。
替代方案比触发器更靠谱?
触发器路由本质是“黑盒写入”,调试困难、无法批量、绕过 ORM、难以审计。生产环境更推荐:
• 应用层根据 tenant_id 拼出目标表名,直连写入(如 MyBatis 的 <insert> 动态表名)
• 使用分库分表中间件(如 ShardingSphere、pg_partman),由中间件解析 SQL 并路由
• PostgreSQL 可用声明式分区(PARTITION BY LIST (tenant_id)),比触发器高效且原生支持查询下推
触发器适合 PoC 或遗留系统缝合,但一旦租户数超百、写入 QPS 过千,它的维护成本和不确定性就会快速超过收益。

















