问题分析
上一节我们在 Controller 中通过 @Query('db') 参数来选择不同的 Repository。但这种方案有一个明显问题:每个路由方法都需要手动判断参数并选择 Repository,代码重复度极高。
解决思路:创建一个公共 Repository 层,在该层中封装数据库选择逻辑,Controller 只需调用一个统一的方法。
实现方案
创建公共 UserRepository
// user/user.repository.ts
import { Injectable, Inject } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Request } from 'express';
@Injectable()
export class UserRepository {
constructor(
// 注入默认数据库的 Repository
@InjectRepository(User)
private userRepository: Repository<User>,
// 注入 mysql1 数据库的 Repository
@InjectRepository(User, 'mysql1')
private userRepository1: Repository<User>,
// 注入全局 Request 对象
@Inject('REQUEST')
private request: Request,
) {}
/**
* 根据请求中的租户标识返回对应的 Repository
*/
getRepository(): Repository<User> {
const tenantId = this.request.headers['x-tenant-id'] as string;
if (tenantId === 'mysql1') {
return this.userRepository1;
}
return this.userRepository; // 默认返回
}
}
typescript
注入全局 Request 对象
在非 Controller 的 Service/Repository 中,无法直接使用 @Req() 装饰器。需要通过 @Inject('REQUEST') 注入 NestJS 全局的 Request 作用域对象:
import { Inject } from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class UserRepository {
constructor(
@Inject('REQUEST')
private request: Request,
) {}
}
typescript
注意:注入 REQUEST 会使该 Provider 变为 Request 作用域,每个请求都会创建新的实例。这对于多租户场景是必要的行为。
注册到 Module 的 providers
// user/user.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([User], 'mysql1'),
],
providers: [UserRepository],
exports: [UserRepository],
})
export class UserModule {}
typescript
Controller 中使用
// app.controller.ts
@Controller('api/v1')
export class AppController {
constructor(
private userRepository: UserRepository, // 注入公共 Repository
) {}
@Get('hello')
async getData() {
// 自动根据请求头选择数据库
const repo = this.userRepository.getRepository();
const data = await repo.find();
return data;
}
}
typescript
支持多种判断策略
getRepository() 方法可以根据业务需要支持多种参数来源:
基于 Query 参数
getRepository(): Repository<User> {
const query = this.request.query;
if (query.db === 'mysql1') {
return this.userRepository1;
}
return this.userRepository;
}
typescript
请求:GET /api/v1/hello?db=mysql1
基于 Header(推荐)
getRepository(): Repository<User> {
const tenantId = this.request.headers['x-tenant-id'] as string;
if (tenantId === 'mysql1') {
return this.userRepository1;
}
return this.userRepository;
}
typescript
请求:GET /api/v1/hello + Header x-tenant-id: mysql1
基于 JWT Token 中的租户信息
getRepository(): Repository<User> {
// 从已解析的 JWT payload 中获取租户信息
const user = (this.request as any).user;
if (user?.tenantId === 'mysql1') {
return this.userRepository1;
}
return this.userRepository;
}
typescript
这种方式最安全,因为租户信息来自服务端签发的 Token,客户端无法伪造。
架构流程
HTTP 请求 → Controller
↓
UserRepository.getRepository()
↓ 读取 request.headers['x-tenant-id']
↓
┌──────────┼──────────┐
↓ ↓ ↓
TypeORM TypeORM Mongoose
Repo(默认) Repo(mysql1) Repo(mongo)
↓ ↓ ↓
MySQL A MySQL B MongoDB
text
验证测试
# 请求默认数据库
curl http://localhost:3000/api/v1/hello
# 请求 mysql1 数据库(通过 Header)
curl -H "x-tenant-id: mysql1" http://localhost:3000/api/v1/hello
# 请求 mysql1 数据库(通过 Query)
curl http://localhost:3000/api/v1/hello?db=mysql1
bash
小结
- 公共 Repository 层封装了数据库选择逻辑,Controller 无需关心底层细节
- 通过
@Inject('REQUEST')在非 Controller 中获取请求对象 - 支持多种参数来源:Query、Header、JWT Token
- Header 方式是推荐的多租户标识传递方式,便于统一管理
↑