Nestjs框架: 多租户与多数据库的架构设计与实现
概述
- 在 NestJS 项目中实现多数据库对接,尤其是结合 TypeORM、Prisma 和 Mongoose 三种不同类型的 OM(对象关系映射)库,来应对多租户架构下的数据隔离与灵活切换需求
- 在现代企业级应用开发中,项目的模型层往往需要根据具体的业务场景来选择合适的数据库与对应的 OM 库。
- 在大多数情况下,不会在同一个项目中同时使用三种数据库,而是根据应用场景来决定是否需要支持多数据库连接
- 只有在项目中确实需要对接多种数据库时,才需要使用我们所讲的最小可行性方案
- 这个方案的核心思想是通过分层来统一接口,再根据不同的数据库类型实现具体的操作逻辑
- 这样,我们就不需要每种数据库都 inject 一次服务,通过统一的服务内部租户身份来进行区分,相当于外观(门面)模式 + 抽象工厂模式 + 策略模式
多租户与多数据库架构下的挑战与优化
- 在多租户系统中,数据隔离是一个核心问题。传统的单库架构无法满足多租户场景下的数据隔离与性能要求。因此,我们需要考虑多库架构(即每个租户拥有独立数据库)
- 然而,多库架构也带来了如下挑战
- 系统复杂度提升: 多库意味着更多的数据库连接、配置管理与数据迁移逻辑
- 维护成本上升: 数据库数量增加,对运维和监控系统提出了更高要求
- 数据同步问题: 跨库数据同步、一致性保障、事务处理等都需要额外设计
- 为解决这些问题,我们在项目中设计了一套基于多 ORM 库的动态数据库连接机制:
- 数据库连接信息可以从本地配置文件读取
- 也可以通过远程服务动态获取(如根据不同租户 ID 获取其数据库连接信息)
- 支持不同类型 OM 库(如 TypeORM、Prisma、Mongoose)的动态切换
ORM 库的适用场景与选型建议
- 在选择 ORM 库时,我们需要根据数据库类型与业务需求进行合理选择:
- TypeORM:更适合关系型数据库(如 MySQL、PostgreSQL),对非关系型数据库(如 MongoDB)支持有限
- Prisma:支持关系型与非关系型数据库,但版本更新频繁,社区支持不如 TypeORM 稳定
- Mongoose:专为 MongoDB 设计,功能强大,是操作 MongoDB 的首选 OM 库
- 因此,在项目中若需要同时对接多种数据库类型,可以采用如下组合:
- 对关系型数据库使用 TypeORM 或 Prisma
- 对 MongoDB 使用 Mongoose
- 项目中使用了 Prisma、TypeORM 和 Mongoose 三种 OM 库
- 这种组合适用于极端复杂的业务场景(如本地与云端数据同步、多种数据库共存等)
需求与架构设计
- 在设计之前,先了解需求:
-
- 支持 prisma, typeorm, mongoose 三种类型的库
-
- 支持mysql, postgresql, mongodb 数据库
-
- 租户标识
- prisma 库
- prisma1 -> mysql 数据库
- prisma2 -> postgresql 数据库
- typeorm 数据库有
- typeorm1 -> mysql1 数据库
- typeorm2 -> mysql2 数据库
- typeorm3 -> postgresql 数据库
- mongoose
- mongoose1 -> mongodb1 数据库
- mongoose2 -> mongodb2 数据库
- prisma 库
- 租户标识
-
- 请按实际业务场景来设计,上面仅作为演示需求,上面表达的是多租户和多数据库的需求
- 目录设计 (只列举核心目录)
src ├── app.module.ts ├── app.controller.ts ├── main.ts ├── ormconfig.ts ├── package.json ├── database │ ├── database.constants.ts │ ├── database.interfaces.ts │ ├── database.module.ts │ ├── database.utils.ts │ ├── prisma │ │ ├── prisma-common.module.ts │ │ ├── prisma-config.service.ts │ │ ├── prisma-core.module.ts │ │ ├── prisma-options.interfaces.ts │ │ ├── prisma-constants.ts │ │ ├── prisma.module.ts │ │ └── prisma.utils.ts │ ├── typeorm │ │ ├── typeorm-common.module.ts │ │ ├── typeorm-config.service.ts │ │ ├── typeorm.constants.ts │ │ └── typeorm.provider.ts │ ├── mongoose │ │ ├── mongoose-common.module.ts │ │ ├── mongoose-config.service.ts │ │ ├── mongoose-core.module.ts │ │ ├── mongoose.constants.ts │ │ ├── mongoose.interfaces.ts │ │ ├── mongoose.module.ts │ │ ├── mongoose.providers.ts │ │ └── mongoose.utils.ts ├── user │ └── repositories │ │ ├── repository.prisma.ts │ │ ├── repository.typeorm.ts │ │ └── repository.mongoose.ts │ ├── user.controller.ts │ ├── user.entity.ts │ ├── user.interfaces.ts │ ├── user.module.ts │ ├── user.repository.ts │ └── user.schema.ts
- 服务启动的 compose.yaml 文件此处不再提供,因为部署时的服务和相关环境都是不一样的
- 只提供下面的配置上的地址,仅作演示
租户环境变量.env设计
# prisma 租户1/2
DATABASE_URL="mysql://root:123456_mysql@localhost:13306/testdb" # 这个需要留着, 生成客户端的时候需要此类格式
T_PRISMA1_DATABASE_URL="mysql://root:123456_mysql@localhost:13306/testdb"
T_PRISMA2_DATABASE_URL="postgresql://pguser:123456_postgresql@localhost:15432/testdb"# typeorm 租户1 配置
T_TYPEORM1_DB_TYPE=mysql
T_TYPEORM1_DB_HOST=localhost
T_TYPEORM1_DB_PORT=13306
T_TYPEORM1_DB_USERNAME=root
T_TYPEORM1_DB_PASSWORD=123456_mysql
T_TYPEORM1_DB_DATABASE=testdb
T_TYPEORM1_DB_AUTOLOAD=true
T_TYPEORM1_DB_SYNC=true# typeorm 租户2 配置
T_TYPEORM2_DB_TYPE=mysql
T_TYPEORM2_DB_HOST=localhost
T_TYPEORM2_DB_PORT=13307
T_TYPEORM2_DB_USERNAME=root
T_TYPEORM2_DB_PASSWORD=123456_mysql
T_TYPEORM2_DB_DATABASE=testdb
T_TYPEORM2_DB_AUTOLOAD=true
T_TYPEORM2_DB_SYNC=true# typeorm 租户3 配置
T_TYPEORM3_DB_TYPE=postgres
T_TYPEORM3_DB_HOST=localhost
T_TYPEORM3_DB_PORT=15432
T_TYPEORM3_DB_USERNAME=pguser
T_TYPEORM3_DB_PASSWORD=123456_postgresql
T_TYPEORM3_DB_DATABASE=testdb
T_TYPEORM3_DB_AUTOLOAD=true
T_TYPEORM3_DB_SYNC=true# mongoose 租户1/2
T_MONGOOSE1_MONGODB_URI="mongodb://root:123456_mongodb@localhost:27018/nestmongodb"
T_MONGOOSE2_MONGODB_URI="mongodb://root:123456_mongodb@localhost:27019/nestmongodb"
根目录下 app.module.ts 模块设计
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { UserModule } from './user/user.module';@Module({imports: [ConfigModule.forRoot({ // 配置环境变量模块envFilePath: '.env', // 指定环境变量文件路径isGlobal: true, // 全局可用}),DatabaseModule,UserModule,],controllers: [AppController]
})export class AppModule {}
这里,可知,我们把 3种类型的库进行了统一管理,使用 DatabaseModule
来集中管理
UserModule
为我们最终成品的测试模块,从这里可以看到封装的意义
database 模块设计
1 )database/database.module.ts 设计
import { Module } from '@nestjs/common';
import { TypeormCommonModule } from './typeorm/typeorm-common.module';
import { PrismaCommonModule } from './prisma/prisma-common.module';
import { MongooseCommonModule } from './mongoose/mongoose-common.module';@Module({imports: [TypeormCommonModule,PrismaCommonModule,MongooseCommonModule],providers: [],exports: [],
})export class DatabaseModule {}
- TypeormCommonModule
- 使用 TypeORM 作为 ORM 的公共模块
- 通常用于关系型数据库(如 PostgreSQL、MySQL)
- 封装了 TypeORM 的连接配置、实体注册、仓储服务等
- PrismaCommonModule
- 使用 Prisma ORM 的公共模块
- Prisma 是新一代的 Node.js ORM,支持 TypeScript,具有良好的类型推导
- 通常用于构建类型安全的数据访问层
- MongooseCommonModule
- 使用 Mongoose 的公共模块
- Mongoose 是 MongoDB 的 Node.js 驱动 ORM
- 适用于非关系型数据库(NoSQL)开发
2 )database/database.constants.ts 设计
export const TYPEORM_DB_CLIENT = 'TYPEORM_DB_CLIENT';
export const PRISMA_DB_CLIENT = 'PRISMA_DB_CLIENT';
export const IS_PRISMA = 'IS_PRISMA';
export const IS_TYPEORM = 'IS_TYPEORM';
export const IS_MONGOOSE = 'IS_MONGOOSE';
3 )database/database.interfaces.ts 设计
export interface ItenantRs {isPrisma: Boolean,isTypeorm: Boolean,isMongoose: Boolean,tPrefix: string,tenantId: string,
}
4 )database/database.utils.ts 设计
import { IncomingHttpHeaders } from "http";
import { tenantMap as prismaTenantMap, defaultTenant as prismaDefaultTenant } from '@/database/prisma/prisma.constants';
import { tenantMap as typeormTenantMap, defaultTenant as typeormDefaultTenant } from '@/database/typeorm/typeorm.constants';
import { tenantMap as mongooseTenantMap, defaultTenant as mongooseDefaultTenant } from '@/database/mongoose/mongoose.constants';
import { IS_PRISMA, IS_TYPEORM, IS_MONGOOSE } from './database.constants';
import { ItenantRs } from './database.interfaces';export function getTenantPrefix(headers: IncomingHttpHeaders | Headers): ItenantRs {let tenantId = headers['x-tenant-id'] as string;tenantId = tenantId.toUpperCase();const isPrisma = prismaTenantMap.has(tenantId);const isTypeorm = typeormTenantMap.has(tenantId);const isMongoose = mongooseTenantMap.has(tenantId);const isValid = tenantId && (isPrisma || isTypeorm || isMongoose);if (!isValid) throw new Error('invalid tenantId');let prefix = '';if (isPrisma) prefix = !tenantId ? prismaDefaultTenant : prismaTenantMap.get(tenantId);if (isTypeorm) prefix = !tenantId ? typeormDefaultTenant : typeormTenantMap.get(tenantId);if (isMongoose) prefix = !tenantId ? mongooseDefaultTenant : mongooseTenantMap.get(tenantId);const tPrefix = prefix;return {isPrisma,isTypeorm,isMongoose,tPrefix,tenantId: tenantId.toLowerCase()}
}export function getdefaultTenantPrefix(flag: string) {switch(flag) {case IS_PRISMA:return prismaDefaultTenant;case IS_TYPEORM:return typeormDefaultTenant;case IS_MONGOOSE:return mongooseDefaultTenant;}
}
- 该模块的核心功能是:
- 根据请求头中的 x-tenant-id 判断租户(tenant)的标识
- 检查当前租户是否存在于 Prisma、TypeORM、Mongoose 的租户映射中
- 返回该租户在不同 ORM 下的数据库前缀
- 如果租户无效,则抛出错误
- 提供一个辅助函数,用于获取默认租户的数据库前缀
5 )database/prisma/prisma-common.module.ts 设计
import { Module } from '@nestjs/common';
import { PrismaConfigService } from './prisma-config.service';
import { PRISMA_DB_CLIENT } from '../database.constants';
import { PrismaModule } from './prisma.module';@Module({imports: [PrismaModule.forRootAsync({name: PRISMA_DB_CLIENT,useClass: PrismaConfigService})],dwdproviders: [],exports: [],
})
export class PrismaCommonModule {}
- 这里提供了一个 common 层,用于分层管理模块
- database/prisma 下的其他文件不提供,之前文章提供过类似的,但部分或许需要改造
6 )database/typeorm/typeorm-common.module.ts 设计
import { Module } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmConfigService } from './typeorm-config.service';
import { TypeormProvider } from './typeorm.provider';
import { TYPEORM_CONNECTIONS } from './typeorm.constants';
import { TYPEORM_DB_CLIENT } from '../database.constants';const connections = new Map<string, DataSource>();@Module({imports: [TypeOrmModule.forRootAsync({name: TYPEORM_DB_CLIENT,useClass: TypeOrmConfigService,dataSourceFactory: async (options) => {const tenantId = options?.['tenantId'] ?? '';if (tenantId && connections.has(tenantId)) {return connections.get(tenantId)!;}const dataSource = await new DataSource(options!).initialize();connections.set(tenantId, dataSource);return dataSource;},inject:[],extraProviders: [],}),],providers: [TypeormProvider,{provide: TYPEORM_CONNECTIONS,useValue: connections,}, ],
})
export class TypeormCommonModule {}
- 这里提供了一个 common 层,用于分层管理模块
- database/typeorm 下的其他文件不提供,之前文章提供过类似的,但部分或许需要改造
7 )database/mongoose/mongoose-common.module.ts 设计
import { Module } from '@nestjs/common';
import { MongooseModule } from './mongoose.module';
import { MongooseConfigService } from './mongoose-config.service';@Module({imports: [MongooseModule.forRootAsync({useClass: MongooseConfigService,}),],providers: [],exports: [],
})
export class MongooseCommonModule {}
- 这里提供了一个 common 层,用于分层管理模块
- database/mongoose 下的其他文件不提供,之前文章提供过类似的,但部分或许需要改造
user 模块设计
1 ) user.module.ts 设计
import { Module } from '@nestjs/common';
import { User } from '@/user/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TYPEORM_DB_CLIENT } from '@/database/database.constants';
import { UserTypeOrmRepository } from '@/user/repositories/repository.typeorm';
import { UserPrismaRepository } from '@/user/repositories/repository.prisma';
import { UserMongooseRepository } from '@/user/repositories/repository.mongoose';
import { UserSchema } from '@/user/user.schema';
import { MongooseModule } from '@/database/mongoose/mongoose.module';
import { UserController } from './user.controller';
import { UserRepository } from './user.repository';@Module({imports: [TypeOrmModule.forFeature([User], TYPEORM_DB_CLIENT),MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),],controllers: [UserController],providers: [UserTypeOrmRepository,UserPrismaRepository,UserMongooseRepository,UserRepository]
})
export class UserModule {}
- 这段代码的关键特征是支持多种数据库(TypeORM + Prisma + Mongoose)
- 体现了 NestJS 的灵活性和模块化设计能力,适用于需要多数据库共存的复杂项目
2 ) user.controller.ts 设计
import { Controller, Get } from '@nestjs/common';
import { UserRepository } from './user.repository';@Controller('user')
export class UserController {constructor(private userRepository: UserRepository,) {}@Get('/multi')async getMultiTypeorm(): Promise<any> {const rs = await this.userRepository.find();return rs;}
}
- 此处提供控制器,用于测试封装的通用 userRepository 服务
3 ) user.interfaces.ts 设计
export interface UserAdapter {find(): Promise<any[]>;create(userObj: any): Promise<any>;update(userObj: any): Promise<any>;delete(id: string | number): Promise<any>;
}
4 ) user.repository.ts 设计
import { REQUEST } from "@nestjs/core";
import { Inject } from '@nestjs/common';
import { Request } from "express";
import { UserMongooseRepository } from './repositories/repository.mongoose';
import { UserPrismaRepository } from './repositories/repository.prisma';
import { UserTypeOrmRepository } from './repositories/repository.typeorm';
import { UserAdapter } from "./user.interfaces";
import { getTenantPrefix } from "@/database/database.utils";export class UserRepository implements UserAdapter {constructor(@Inject(REQUEST) private request: Request,private userPrismaRepository: UserPrismaRepository,private userMongooseRepository: UserMongooseRepository,private userTypeOrmRepository: UserTypeOrmRepository,) {}find(): Promise<any[]> {const client = this.getRepository();return client.find();}create(userObj: any): Promise<any> {const client = this.getRepository();return client.create(userObj);}update(userObj: any): Promise<any> {const client = this.getRepository();return client.update(userObj);}delete(id: string | number): Promise<any> {const client = this.getRepository();return client.delete(id);}getRepository(): UserAdapter {const headers = this.request.headers;const { isPrisma, isTypeorm, isMongoose } = getTenantPrefix(headers);if (isPrisma) return this.userPrismaRepository;if (isTypeorm) return this.userTypeOrmRepository;if (isMongoose) return this.userMongooseRepository;throw new Error('Something went wrong in user/user.repository please check!');}
}
- 这里使用了 NestJS 的核心装饰器 @Inject(REQUEST) 来注入当前请求对象 (Request),用于获取多租户上下文信息(如请求头)
- 引入了三种不同的数据库适配器:
- UserPrismaRepository
- UserMongooseRepository
- UserTypeOrmRepository
- 通过构造函数注入这些仓库类,实现了松耦合,便于替换与测试
- 多租户支持
- 通过解析请求头(如 x-tenant)动态选择数据源
- 非常适合 SaaS 架构中不同租户使用不同数据库的情况
- 减少了多租户逻辑的侵入性,数据源选择逻辑集中在一个地方
- 清晰的接口抽象
- 所有操作方法(find, create, update, delete)都通过统一接口调用,隐藏底层实现细节
- 提升了可维护性与扩展性
- 模块化结构
- 数据源实现类独立存在,便于单元测试与替换
- 适配器接口可以复用到其他实体(如 ProductRepository, OrderRepository)
- 设计意图与架构模式
- 策略模式(Strategy Pattern)
- 每个数据源的实现类(如 UserPrismaRepository)可以看作一个策略
- getRepository() 根据上下文选择执行策略
- 这种方式使得系统可以在运行时动态切换数据源,适应多租户场景
- 适配器模式(Adapter Pattern)
- UserAdapter 接口定义统一的访问接口,屏蔽底层实现差异
- 使得上层业务无需关心具体数据库类型。
- 依赖注入(DI)
- 通过 NestJS 的 DI 机制注入不同数据源的实现
- 符合 SOLID 原则中的依赖倒置原则(DIP)
- 策略模式(Strategy Pattern)
5 ) user.entity.ts 设计
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';@Entity()
export class User {@PrimaryGeneratedColumn()id: number;@Column()username: string;@Column()password: string;
}
6 ) user.schema.ts 设计
import mongoose from 'mongoose';export const UserSchema = new mongoose.Schema({username: String,password: String,},{ collection: 'users' },
);
user 模块下的 repositories 设计
1 ) repositories/repository.prisma
import { Inject } from '@nestjs/common';
import { PRISMA_DB_CLIENT } from '@/database/database.constants';
import { PrismaClient } from '@prisma/client';
import { UserAdapter } from '../user.interfaces';export class UserPrismaRepository implements UserAdapter {constructor(@Inject(PRISMA_DB_CLIENT) private prismaClient: PrismaClient) {}find(): Promise<any[]> {return this.prismaClient.user.findMany({});}create(userObj: any): Promise<any> {return this.prismaClient.user.create({data: userObj});}update(userObj: any): Promise<any> {return this.prismaClient.user.update({where: { id: userObj.id },data: userObj,});}delete(id: number): Promise<any> {return this.prismaClient.user.delete({ where: { id }})}
}
2 ) repositories/repository.typeorm
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../user.entity';
import { UserAdapter } from '../user.interfaces';
import { TYPEORM_DB_CLIENT } from '@/database/database.constants';export class UserTypeOrmRepository implements UserAdapter {constructor(@InjectRepository(User, TYPEORM_DB_CLIENT) private userRepository: Repository<User>,) {}find(): Promise<any[]> {return this.userRepository.find({});}create(userObj: any): Promise<any> {return this.userRepository.save(userObj);}update(userObj: any): Promise<any> {return this.userRepository.update({ id: userObj.id }, userObj);}delete(id: string): Promise<any> {return this.userRepository.delete(id);}
}
3 ) repositories/repository.mongoose
import { InjectModel } from '@nestjs/mongoose';
import { Model, Document as Doc } from 'mongoose';
import { UserAdapter } from '../user.interfaces';export class UserMongooseRepository implements UserAdapter {constructor(@InjectModel('User') private readonly userModel: Model<Doc>,) {}find(): Promise<any[]> {return this.userModel.find();}create(userObj: any): Promise<any> {return this.userModel.create(userObj);}update(userObj: any): Promise<any> {return this.userModel.updateOne(userObj);}delete(id: string): Promise<any> {return this.userModel.deleteOne({_id: id});}
}
prisma 相关测试
1 )测试1
-
请求
curl --request GET \--url http://localhost:3000/user/multi \--header 'x-tenant-id: prisma1'
-
响应
[{"id": 1,"username": "mysql","password": "123456"} ]
2 )测试2
-
请求
curl --request GET \--url http://localhost:3000/user/multi \--header 'x-tenant-id: prisma2'
-
响应
[{"id": 1,"username": "postgresql","password": "123456"} ]
typeorm 相关测试
1 )测试
-
请求
curl --request GET \--url http://localhost:3000/user/multi \--header 'x-tenant-id: typeorm1'
-
响应
[{"id": 1,"username": "mysql","password": "123456"} ]
2 )测试2
-
请求
curl --request GET \--url http://localhost:3000/user/multi \--header 'x-tenant-id: typeorm2'
-
响应
[{"id": 1,"username": "mysql2","password": "123456"} ]
3 )测试3
-
请求
curl --request GET \--url http://localhost:3000/user/multi \--header 'x-tenant-id: typeorm3'
-
响应
[{"id": 1,"username": "postgresql","password": "123456"} ]
mongoose 相关测试
1 )测试1
-
请求
curl --request GET \--url http://localhost:3000/user/multi \--header 'x-tenant-id: mongoose1'
-
响应
[{"_id": "6874d4b0d10e36e350dd588d","username": "mongo1","password": "123456"},{"_id": "6874d4d9d10e36e350dd588f","username": "lee","password": "123456"} ]
2 )测试2
-
请求
curl --request GET \--url http://localhost:3000/user/multi \--header 'x-tenant-id: mongoose2'
-
响应
[{"_id": "6874d4b0d10e36e350dd588d","username": "mongo2","password": "123456"},{"_id": "6874d4d9d10e36e350dd588f","username": "lee","password": "123456"} ]
分层架构与 AOP 思想的重要性
- 在整个实现过程中,我们强调了 分层架构与 AOP(面向切面编程)思想的应用。这在 Java 等语言中非常常见,但在 Node.js/NestJS 中同样具有重要意义:
- 通过抽象类统一接口
- 通过 Repository 模式解耦业务逻辑与数据访问层
- 通过工厂类实现动态切换
- 通过配置中心实现数据库连接信息的灵活加载
- 这些设计不仅提升了系统的可扩展性,也为后续的维护与升级提供了良好的基础
开发建议与实践指导
- 在实际开发中,建议遵循以下原则
- 1 ) 小项目优先使用单库架构
- 本地开发时使用单库,线上部署初期也建议使用单一数据库,降低复杂度
- 2 ) 根据业务需求逐步扩展
- 当业务增长到需要多租户或多种数据库支持时,再逐步引入多库架构与多 OM 库方案
- 保持代码结构清晰: 每个 Repository 对应一个 OM 实现,避免混杂逻辑
- 注重抽象设计: 提前设计好抽象类与接口,便于后续扩展