Nestjs框架: 基于Prisma的多租户功能集成和优化

Nestjs框架: 多租户与多数据库的架构设计与实现 本文介绍了在NestJS项目中实现多数据库对接的方案,重点解决多租户架构下的数据隔离与灵活切换需求。通过抽象层统一接口,结合TypeORM、Prisma和Mongoose三种ORM库,支持MySQL、PostgreSQL和MongoDB等多种数据库类型。方案采用外观模式+抽象工厂模式+策略模式,通过DatabaseModule集中管理数据库连接,根据租户ID动态切换不同数据库配置。文章详细阐述了环境变量设计、目录结构和模块实现,为复杂业务场景下的多数据库管理提供了可行方案。 阅读详情

概述

  • 多租户系统指一个系统可以为多个客户(Tenant)提供服务,每个客户的数据相互隔离。常见做法是每个租户使用独立的数据库或共享数据库但通过 schema 或前缀隔离
  • 这里,我们演示一下基于Prisma集成mysql和postgresql的多种数据库类型多数据库的多租户模式
  • Prisma 内置数据库的驱动,它会根据 schema 文件中的 provider 在 generate 的时候
  • 产生何种类型的 Prisma client, 需要根据不同的 provider 生成不同的 client
  • 以便同一个 Prisma module 来访问不同类型数据库,这点和 typeorm 非常类似,只不过后者是外置驱动
  • prisma 这里麻烦一点儿,参考 generate命令 就是根据 generator 和 data model 来产生对应的 Prisma Client
  • 默认产生的位置在 node_module/@prisma/client中,我们现在要把不同数据库的client产生到指定的目录

产生不同的数据库client

1 ) 配置 mysql

src/prisma/schema.prisma 下

generator client {
  provider = "prisma-client-js"
  output   = "./clients/mysql"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

注意,上面 .env 中的 DATABASE_URL 中配置的是有效的mysql链接, 如:

DATABASE_URL="mysql://root:123456_mysql@localhost:13306/testdb"

执行 $ pnpx prisma generate 此时就在 ./clients/mysql 目录中产生了

2 ) 配置 postgresql

src/prisma/schema.prisma 下

generator client {
  provider = "prisma-client-js"
  output   = "./clients/postgresql"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

对应的 DATABASE_URL 修改为

DATABASE_URL="postgresql://pguser:123456_postgresql@localhost:15432/testdb"
  • 执行 $ pnpx prisma generate 此时就在 ./clients/postgresql 目录中产生了

  • 这里,如果重新 $ npm run start:dev 会有报错,这是临时产生的,先不要管它

    PrismaClientInitializationError: error: Error validating datasource `db`: the URL must start with the protocol `mysql://`.
    
  • 注意:目前2个租户都是使用 一个 DATABASE_URL 这是为了生成客户端时候用的,之后,程序写到多租户的时候,就可以替换掉了

  • 在 Prisma 多租户方案中,生成时使用的 URL 仅作为占位符,无需真实有效,运行时才会动态连接真实数据库 URL

prisma generate 生成原理

  • 生成阶段(prisma generate)

    • Prisma 客户端生成过程仅解析 Schema 结构(表、字段、关系等),不验证数据库连接有效性
      占位 URL 只需满足格式规范(如 postgresql://dummy:dummy@localhost:5432/dummy),不要求实际可访问。
  • 运行时阶段(动态连接)

    • 通过 PrismaClient 构造函数动态注入真实 URL:
      new PrismaClient({ datasources: { db: { url: realTenantUrl } } })
      
    • 每个租户独立实例化客户端,实现隔离连接

注意事项

  • 占位 URL 的格式要求

    • 必须包含协议前缀(如 postgresql:// 或 mysql://),否则生成可能报错。
    • 示例:
      # 有效占位(符合 URL 格式)
      DATABASE_URL="mysql://dummy:dummy@dummy:3306/dummy"
      
  • 生成与运行时的环境变量分离

    • 生成时:依赖 .env 中的占位 URL(或命令行临时指定)。
    • 运行时:从独立配置源(如数据库、配置中心)读取租户真实 URL,避免硬编码

方案优势与风险

优势风险
一套 Schema 支持异构数据库(MySQL/PostgreSQL)占位 URL 格式错误会导致生成失败
避免生成时依赖真实数据库环境运行时需确保租户 URL 可访问且权限正确
动态扩展租户无需重新生成客户端多客户端实例可能增加内存开销(需监控)
  • 生产实践提示:

    • 通过 prisma validate 命令可提前验证 Schema 正确性,无需真实数据库
    • 运行时建议增加租户 URL 的格式校验逻辑(如正则匹配),防止配置错误导致连接失败。
  • 总结

    • 占位 URL 的核心价值:
    • 解耦生成时与运行时的关注点
      • 生成时:仅关注 Schema 结构兼容性(如避免 MySQL/PostgreSQL 特有语法)。
      • 运行时:通过租户标识动态路由连接,实现多租户隔离。
  • 有些业务场景,可能支持百级别租户混合使用 MySQL/PostgreSQL,如果需要进一步优化可结合连接池参数调优,或引入熔断机制(如 Hystrix)处理数据库故障

使用生成的客户端

  • $ pnpm add prisma-mysql@link:./prisma/clients/mysql
  • $ pnpm add prisma-postgresql@link:./prisma/clients/postgresql

关键输出信息

prisma-mysql <- prisma-client-e6c9963f1991a4598b0cb949accc2f3a50f445a316da9910f0b1811a3ad47379 6.12.0 <- prisma/clients/mysql

和

prisma-postgresql <- postgresql 0.0.0 <- prisma/clients/postgresql

可以看到 package.json 中多出来了 2 个依赖

"prisma-mysql": "link:prisma/clients/mysql",
"prisma-postgresql": "link:prisma/clients/postgresql",

Prisma 多租户集成初步规划

在 src/database/prisma/ 下

src/database/prisma/
|-- prisma-core.module.ts
|-- prisma-options.interface.ts
|-- prisma.constants.ts
|-- prisma.module.ts
|-- prisma.service.ts
|-- prisma.utils.ts

这里将 prisma module 拆分细化

1 ) prisma.constants.ts

export const PRISMA_CONNECTION_NAME = Symbol('PRISMA_CONNECTION_NAME');
export const PRISMA_MODULE_OPTIONS = Symbol('PRISMA_MODULE_OPTIONS');
export const PRISMA_CONNECTIONS = Symbol('PRISMA_CONNECTIONS');
// 这个配置模拟调接口/读数据库获取的
export const tenantMap = new Map([
	['1', 'T1'],
	['2', 'T2']
]);
export const defaultTenant = tenantMap.values().next().value; // 第一个
  • 定义抽离出来的可用变量

2 )prisma-options.interface.ts

import { Prisma } from '@prisma/client';
import { ModuleMetadata, Type } from '@nestjs/common';

export interface PrismaModuleOptions {
	url?: string;
	datasourceUrl?: string;
	name?: string;
	options?: Prisma.PrismaClientOptions;
	retryAttempts?: number;
	retryDelay?: number;
	connectionFactory?: (connection: any, clientClass: any) => any;
	connectionErrorFactory?: (
		error: Prisma.PrismaClientKnownRequestError,
	) => Prisma.PrismaClientKnownRequestError;
}

export interface PrismaOptionsFactory {
	createPrismaModuleOptions(): | Promise<PrismaModuleOptions> | PrismaModuleOptions;
}

export type PrismaModuleFactoryOptions = Omit<PrismaModuleOptions, 'name'>; // 排除name属性,为了避免可能得冲突

export interface PrismaModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
	name?: string;
	useExisting?: Type<PrismaOptionsFactory>;
	useClass?: Type<PrismaOptionsFactory>;
	useFactory?: (...args: any[]) => Promise<PrismaModuleFactoryOptions> | PrismaModuleFactoryOptions;
	inject?: any[];
}
  • 定义后续可能用到的类型

3 )prisma.utils.ts

import { retry, timer, throwError, catchError } from 'rxjs';

export const PROTOCALREGEX = /^(.*?):\/\//;

export function getDBType(url: string) {
	const matches = url.match(PROTOCALREGEX);
	const protocol = matches ? matches[1] : 'file';
	return protocol === 'file' ? 'sqlite' : protocol;
}

export function handleRetry(retryAttempts: number, retryDelay: number) {
	return (source) =>
		source.pipe(
			retry({
				count: retryAttempts < 0 ? Infinity : retryAttempts, // 默认 count 是 正数,为了让 -1 这类值也能运行, 并无限重试
				delay: (error, retryCount) => {
					const attempts = retryAttempts < 0 ? Infinity : retryAttempts;
					if (retryCount <= attempts) {
						console.error(
							`Unable to connect to the database. Retrying (
							${retryCount})...`,
							error.stack,
						);
						return timer(retryDelay);
					} else {
						return throwError(() => new Error('Reached max retries'));
					}
				},
			}),
			catchError(error => {
				console.error(`Failed to connect to the database after retries ${retryAttempts} times`, error.stack || error,);
				return throwError(() => error);
			})
		) 
}

  • 定义工具类,主要用于处理数据库连接的协议解析和重试逻辑
  • 它结合了 RxJS(响应式编程库)的功能,实现了在数据库连接失败时的自动重试机制,并在重试失败后抛出错误

4 ) prisma.module.ts

import { Module, DynamicModule } from '@nestjs/common';
import { PrismaModuleOptions, PrismaModuleAsyncOptions } from './prisma-options.interface';
import { PrismaCoreModule } from './prisma-core.module';

@Module({})
export class PrismaModule {
  static forRoot(options: PrismaModuleOptions): DynamicModule;
  static forRoot(url: string): DynamicModule;
  static forRoot(url: string, name: string): DynamicModule;
  static forRoot(arg: PrismaModuleOptions | string, ...args): DynamicModule {
    let _options: PrismaModuleOptions;
    if (args?.length) {
      _options = { url: arg, name: args[0]} as PrismaModuleOptions;
    } else if (typeof arg === 'string') {
      _options = { url: arg };
    } else {
      _options = arg;
    }

    return {
      module: PrismaModule,
      imports: [PrismaCoreModule.forRoot(_options)],
    }
  }

  static forRootAsync(options: PrismaModuleAsyncOptions) {
    return {
      module: PrismaModule,
      imports: [PrismaCoreModule.forRootAsync(options)],
      providers: [],
      exports: [],
    }
  }
}
  • 它是一个模块封装,使用 @Module({}) 装饰器声明。

  • 它本身是一个无内部逻辑的模块(@Module({}) 是空的),但通过静态方法 forRoot 和 forRootAsync 提供了动态模块的构建能力,用于在应用启动时配置 Prisma 数据库连接。

    @Module({})
    export class PrismaModule {
      static forRoot(...): DynamicModule;
      static forRootAsync(...): DynamicModule;
    }
    
  • forRoot() 方法(同步配置)该方法用于同步配置 Prisma 模块的数据库连接。它有多个函数签名重载,支持灵活的参数传递方式:

    • forRoot(options: PrismaModuleOptions)
    • forRoot(url: string)
    • forRoot(url: string, name: string)
    • 支持多种传参方式:
      • 全部参数传入一个对象(推荐)
      • 只传入数据库连接 URL
      • 同时传入 URL 和一个命名标识(name)
    • 内部将参数统一转换为 PrismaModuleOptions 类型,然后交给 PrismaCoreModule.forRoot() 处理
    • 返回一个动态模块,注册 PrismaModule 并导入 PrismaCoreModule,实现核心功能注入
  • forRootAsync() 方法(异步配置)用于异步配置 Prisma 模块,通常用于需要依赖注入(DI)或异步加载配置(如从配置文件或服务中获取数据库连接信息)的场景

    static forRootAsync(options: PrismaModuleAsyncOptions) {
      return {
        module: PrismaModule,
        imports: [PrismaCoreModule.forRootAsync(options)],
        providers: [],
        exports: [],
      }
    }
    
  • 通常包含以下字段(由 NestJS 惯例和 Prisma 模块设计决定):

    • useFactory:用于异步生成配置的工厂函数
    • inject:注入的依赖项数组
    • imports:其他模块导入(用于获取依赖)
    • extraProviders:额外提供者(如服务类)
  • 依赖模块:PrismaCoreModule

    • PrismaCoreModule 是实际处理 Prisma 初始化逻辑的模块。它提供了两个静态方法:
      • forRoot():同步初始化 Prisma 客户端
      • forRootAsync():异步初始化 Prisma 客户端
    • 这个模块才是真正负责创建 Prisma 实例、注册服务、管理连接池等核心任务的地方
  • 接口依赖类型说明

    • PrismaModuleOptions:配置数据库连接的接口,通常包括:
      • url: 数据库连接字符串
      • name: 模块实例的标识名(用于多数据库连接场景)
    • PrismaModuleAsyncOptions:异步初始化配置接口,支持 DI 和异步加载

5 )prisma-core.module

import { Global, Module, OnApplicationShutdown, Provider, Type } from '@nestjs/common';
import { PrismaModuleOptions, PrismaModuleAsyncOptions, PrismaOptionsFactory } from './prisma-options.interface';
import { PrismaClient as MySQLClient } from 'prisma-mysql';
import { PrismaClient as PgClient } from 'prisma-postgresql';
import { getDBType, handleRetry } from './prisma.utils';
import { PRISMA_CONNECTION_NAME, PRISMA_MODULE_OPTIONS, PRISMA_CONNECTIONS } from './prisma.constants';
import { lastValueFrom, defer, catchError } from 'rxjs';
import { DynamicModule } from '@nestjs/common';

@Global()
@Module({})
export class PrismaCoreModule implements OnApplicationShutdown {
  private static connections: Record<string, any> = {};

  /**
   * 应用程序关闭时触发,用于优雅关闭 Prisma 客户端连接
   */
  async onApplicationShutdown(signal?: string): Promise<void> {
    if (PrismaCoreModule.connections && Object.keys(PrismaCoreModule.connections.length > 0)) {
      for (const key of Object.keys(PrismaCoreModule.connections)) {
        const connection = PrismaCoreModule.connections[key];
        if (connection && typeof connection.$disconnect === 'function') {
          connection.$disconnect();
        }
      }
    }
  }
 
  static forRoot(_options: PrismaModuleOptions): any {
    const { 
      url, options = {}, name,
      retryAttempts = 10,
      retryDelay = 3000,
      connectionFactory,
      connectionErrorFactory,
    } = _options;
    let newOptions = {
      datasourceUrl: url
    };
    if (!Object.keys(options).length) {
      newOptions = { ...newOptions, ...options };
    }

    let _prismaClient;
    const dbType = getDBType(url!);
    if (dbType === 'mysql') {
      _prismaClient = MySQLClient;
    } else if (dbType === 'postgresql') {
      _prismaClient = PgClient;
    } else {
      throw new Error(`Unsupported database type: ${dbType}`);
    }

    const providerName = name || PRISMA_CONNECTION_NAME;
    const prismaConnectionErrorFactory = connectionErrorFactory || ((err) => err);
    const prismaConnectionFactory = connectionFactory ||
      (async (clientOptions) => await new _prismaClient(clientOptions));

    const prismaClientProvider: Provider = {
      provide: providerName,
      useFactory: async () => {
        if (this.connections[url!]) {
          return this.connections[url!];
        }
        // 加入错误重试
        const client = await prismaConnectionFactory(newOptions, name!);
        this.connections[url!] = client;
        return lastValueFrom(
          defer(async () => await client.$connect()).pipe(
            handleRetry(retryAttempts, retryDelay),
            catchError(err => { throw prismaConnectionErrorFactory(err) })
          )
        ).then(() => client);
      },
    };

    const connectionsProvider = {
        provide: PRISMA_CONNECTIONS,
        useValue: this.connections
    };
 
    return {
      module: PrismaCoreModule,
      providers: [prismaClientProvider, connectionsProvider],
      exports: [prismaClientProvider, connectionsProvider],
    };
  }
 
  static forRootAsync(_options: PrismaModuleAsyncOptions): DynamicModule {
    const providerName = _options.name || PRISMA_CONNECTION_NAME;

    const prismaClientProvider: Provider = {
      provide: providerName,
      useFactory: (prismaModuleOptions: PrismaModuleOptions) => {
        const {
          url, options = {},
          retryAttempts = 10,
          retryDelay = 3000,
          connectionFactory,
          connectionErrorFactory,
        } = prismaModuleOptions;
        let newOptions = {
          datasourceUrl: url
        };
        if (!Object.keys(options).length) {
          newOptions = { ...newOptions, ...options };
        }

        let _prismaClient;
        const dbType = getDBType(url!);
        if (dbType === 'mysql') {
          _prismaClient = MySQLClient;
        } else if (dbType === 'postgresql') {
          _prismaClient = PgClient;
        } else {
          throw new Error(`Unsupported database type: ${dbType}`);
        }

        const prismaConnectionErrorFactory = connectionErrorFactory || ((err) => err);
        const prismaConnectionFactory = connectionFactory ||
           (async (clientOptions) => await new _prismaClient(clientOptions));

        return lastValueFrom(
          defer(async () => {
            const url = newOptions.datasourceUrl;
            if (this.connections[url!]) {
              return this.connections[url!];
            }
            const client = await prismaConnectionFactory(
              newOptions,
              _prismaClient
            );
            this.connections[url!] = client;
            return client;
          }).pipe(
            handleRetry(retryAttempts, retryDelay),
            catchError(err => { throw prismaConnectionErrorFactory(err) })
          )
        );

      },
      inject: [PRISMA_MODULE_OPTIONS]
    }

    const asyncProviders = this.createAsyncProviders(_options);

    const connectionsProvider = {
        provide: PRISMA_CONNECTIONS,
        useValue: this.connections
    };

    return {
      module: PrismaCoreModule,
      providers: [...asyncProviders, prismaClientProvider, connectionsProvider],
      exports: [prismaClientProvider, connectionsProvider],
    }
  }

  private static createAsyncProviders(options: PrismaModuleAsyncOptions) {
    if (options.useExisting || options.useFactory) {
      return [this.createAsyncOptionsProviders(options)]
    }
    const useClass = options.useClass as Type<PrismaOptionsFactory>

    return [
      this.createAsyncOptionsProviders(options),
      {
        provide: useClass,
        useClass,
      }
    ]
  }

  // 创建 PRISMA_MODULE_OPTIONS的Provider
  private static createAsyncOptionsProviders(options: PrismaModuleAsyncOptions):Provider {
    if (options.useFactory) {
      return {
        provide: PRISMA_MODULE_OPTIONS,
        useFactory: options.useFactory,
        inject: options.inject || [],
      }
    }

    const inject = [
      (options.useClass || options.useExisting) as Type<PrismaOptionsFactory>,
    ]

    return {
      provide: PRISMA_MODULE_OPTIONS,
      inject,
      useFactory: async (optionsFactory: PrismaOptionsFactory) => optionsFactory.createPrismaModuleOptions(),
    }
  }
}
  • 用于封装并统一管理 Prisma Client 的连接逻辑,适配多种数据库类型(如 MySQL、PostgreSQL),并支持同步和异步配置

  • 该 PrismaCoreModule 是 NestJS 中的 全局模块(@Global()),用于集中管理 Prisma 客户端的连接、配置、重试、错误处理、连接池管理等逻辑,确保整个应用中对数据库的访问是统一、高效和稳定的

  • 静态连接池管理

    private static connections: Record<string, any> = {};
    
    • 作用:作为模块内的静态属性,用于缓存已实例化的 Prisma 客户端实例,避免重复连接,提升性能
    • 生命周期:模块加载时初始化,应用关闭时通过 onApplicationShutdown 优雅销毁
  • 应用关闭时自动断开连接

    async onApplicationShutdown(signal?: string): Promise<void>
    
    • 功能:实现 OnApplicationShutdown 生命周期接口,确保应用关闭前释放所有数据库连接
    • 细节:遍历 connections 对象,调用每个连接的 $disconnect() 方法
    • 意义:防止连接泄漏,提升系统健壮性
  • 同步注册:forRoot()

    static forRoot(_options: PrismaModuleOptions): DynamicModule
    
    • 功能:用于同步配置 Prisma 模块的入口方法
    • 参数解析:
      • url: 数据库连接 URL
      • options: 额外 Prisma 客户端配置
      • retryAttempts, retryDelay: 重试机制配置
      • connectionFactory: 自定义连接创建逻辑
      • connectionErrorFactory: 自定义错误处理逻辑
  • 核心逻辑:

    • 根据 URL 判断数据库类型(MySQL / PostgreSQL)
    • 实例化对应的 Prisma Client
    • 使用 defer + handleRetry + catchError 实现连接失败的重试逻辑
    • 提供可注入的 Prisma 客户端服务(providerName)供其他模块使用
  • 异步注册:forRootAsync()

    static forRootAsync(_options: PrismaModuleAsyncOptions): DynamicModule
    
    • 功能:用于异步方式配置 Prisma 模块,更适用于需要依赖注入的场景
    • 支持方式:
      • useClass: 提供一个实现 PrismaOptionsFactory 接口的类
      • useFactory: 自定义异步工厂函数
      • useExisting: 使用一个已存在的服务
    • 优势:支持异步初始化,如从数据库或远程服务加载配置
  • 创建异步依赖项:createAsyncProviders() 与 createAsyncOptionsProviders()

    • 作用:构建异步配置所需的依赖注入提供者(Providers)
    • createAsyncOptionsProviders():
      • 若使用 useFactory,则直接注册该工厂
      • 若使用类(useClass 或 useExisting),则注入该类并调用 createPrismaModuleOptions() 方法获取配置
  • 关键技术点与设计思想

      1. 支持多数据库类型
        const dbType = getDBType(url);
        
        • 实现方式:通过 URL 判断数据库类型(mysql、postgresql)
        • 扩展性:未来可轻松支持更多数据库类型(如 SQLite、MongoDB 等)
        • 容错机制:不支持的数据库类型抛出错误
      1. 重试机制
        defer(...).pipe(handleRetry(...), catchError(...))
        
        • 实现方式:通过 RxJS 的 defer() 和自定义 handleRetry() 操作符实现连接失败重试。
        • 配置项:retryAttempts 和 retryDelay 可由用户配置。
        • 优势:提高连接的健壮性,应对网络波动或数据库短暂不可用的场景
    1. 自定义连接与错误处理
    • connectionFactory:允许用户自定义 Prisma Client 的创建逻辑
    • connectionErrorFactory:允许用户自定义连接失败的错误响应逻辑
    • 灵活性:极大增强了模块的可定制性,满足不同业务场景需求
    1. 模块设计模式:单例 + 依赖注入
    • 模块使用了 NestJS 的 DI(依赖注入)机制,支持同步与异步配置
    • 所有 Prisma 客户端以单例形式缓存,提升性能,避免资源浪费
    • 提供 @Inject(providerName) 方式注入 Prisma 客户端到服务中

6 ) prisma.service.ts

import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { PrismaOptionsFactory, PrismaModuleOptions } from './prisma-options.interface';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import { tenantMap, defaultTenant } from './prisma.constants';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class PrismaService implements OnModuleInit, PrismaOptionsFactory {
  constructor(
    @Inject(REQUEST) private request: Request,
    private configService: ConfigService
  ) {}

  createPrismaModuleOptions(): PrismaModuleOptions | Promise<PrismaModuleOptions> {
    const headers = this.request.headers;
    const tenantId = headers['x-tenant-id'] as string || 'default';

    if (tenantId && !tenantMap.has(tenantId)) {
        throw new Error('invalid tenantId');
    }
    const t_prefix = !tenantId ? defaultTenant : tenantMap.get(tenantId);
    const db_url = this.configService.get<string>(`${t_prefix}_DATABASE_URL`);
    return { url: db_url };
  }

  async onModuleInit() {}
}
  • 从请求头中获取 x-tenant-id,若不存在则使用默认租户(如 ‘default’)
  • 校验该租户是否存在于 tenantMap(一个 Map 结构,用于映射租户 ID 与数据库标识)
  • 获取对应租户的环境变量前缀(如 TENANT1_DATABASE_URL)
  • 返回 Prisma 模块所需的连接 URL

应用封装服务

上面核心模块设计好了,之后,就可以开始应用了,上面提供了 forRoot 的方式,也提供了 forRootAsync 动态配置和注入的方式,现在我们使用后者的方式因为更方便

1 )app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PrismaModule } from './database/prisma/prisma.module';
import { PrismaService } from './database/prisma/prisma.service';

const connections = new Map<string, DataSource>();

@Module({
  imports: [
    // 1. 下面这个后续可以封装一个新的模块,来匹配 .env 和 其他配置
    ConfigModule.forRoot({  // 配置环境变量模块
      envFilePath: '.env', // 指定环境变量文件路径
      isGlobal: true, // 全局可用
    }),

    // 2. 集成 Prisma
    PrismaModule.forRootAsync({
        name: 'prismaClient',
        useClass: PrismaService
    })
  ],
  controllers: [AppController]
})

export class AppModule {}

这里用 forRootAsync 来演示, forRoot的形式在此不演示了

2 ) app.controller.ts

import { Controller, Get, Inject } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Controller()
export class AppController {
  constructor(
    @Inject('prismaClient') private readonly prismaClientService: PrismaClient,
  ) {}

  @Get('/multi-prisma')
  async getMultiPrisma(): Promise<any> {
    const rs = await this.prismaClientService.user.findMany({});
    return rs;
  }
}

测试结果


1 ) 测试1

请求

	curl --request GET \
	  --url http://localhost:3000/multi-prisma \
	  --header 'x-tenant-id: 1'

响应

[
  {
    "id": 1,
    "username": "mysql",
    "password": "123456"
  }
]

2 )测试2

请求

curl --request GET \
  --url http://localhost:3000/multi-prisma \
  --header 'x-tenant-id: 2'

响应

[
  {
    "id": 1,
    "username": "postgresql",
    "password": "123456"
  }
]
TypeScript类型安全短链服务:NestJS+Prisma+PostgreSQL实战 短链接服务本质是高并发、强一致性的Web API基础设施,其核心挑战在于字段契约统一、数据操作原子性与错误语义化。TypeScript类型安全并非仅限于接口定义,而是需贯穿请求校验(DTO)、ORM映射(Prisma自动生成精确类型)、数据库约束(PostgreSQL唯一索引与upsert)及异常处理(P2002/P2025错误码映射)的全链路保障。NestJS通过装饰器驱动的模块化架构,天然支持守卫(Guard)、管道(Pipe)和过滤器(Filter)等切面能力,使租户隔离、审计日志、灰度路由等企业级需 阅读详情

相关推荐

fp-ts交互式学习平台:垂直切片架构与浏览器端Vitest执行

函数式编程教学平台的核心在于即时反馈闭环,其技术本质是将类型安全、代码执行、测试验证与状态持久化在端到端链路中无缝集成。基于 fp-ts 的练习系统需突破传统分层开发范式,转而采用垂直切片设计——以单题为单元,贯通前端渲染、沙盒执行、实时类型校验与轻量后端托管。其中,Vitest 浏览器版实现零依赖运行,Next.js App Router 支撑静态路由预生成与 Server Actions 状态同步,Prisma 保障 TypeScript 接口与数据库 schema 的强一致映射。该架构特别适用于在线编

weixin_32308101的博客 293

Prisma 多租户解决方案介绍

Prisma 多租户解决方案介绍 1. 项目基础介绍 Prisma Multi-Tenant 是一个开源项目,旨在帮助开发者利用 Prisma 实现多租户架构的应用程序。该项目基于 TypeScript 和 JavaScript 编程语言,通过为每个租户提供独立的数据库实例,支持多租户环境下数据、配置、用户管理以及功能和非功能属性的隔离。 2. 核心功能 项目的核心功能包括: 多租户数据库管理:...

gitblog_00710的博客 479

prisma-multi-tenant::compass:将Prisma用作您的应用程序的多租户提供程序

棱镜多租户 :compass: :compass: 使用Prisma作为您的应用程序的多租户提供程序 什么是多租户应用程序? 应用程序是指应用程序的单个实例在服务器上运行并为多个租户提供服务的情况。 借助多租户体系结构,软件应用程序旨在为每个租户提供实例的专用共享-包括其数据,配置,用户管理,租户的单个功能和非功能属性。 例如,您可以为公司运行一个社交网络,其中每个公司都有自己的数据和用户。 为什么Prisma非常适合多租户? Prisma为您提供了处理数据库所需的所有工具:数据建模,数据库模式迁移,键入安全的数据库访问等。这样做, prisma-multi-tenant可以使这些过程自动化,并帮助您制作多租户应用程序。 为什么我需要prisma-multi-tenant ? 因为prisma-multi-tenant不仅允许您仅使用服务器中的几行代码即可无缝访问多个数据库,而且还使您可以使用CLI创建新

【亲测免费】 探索多租户应用的新里程碑:Prisma-multi-tenant

探索多租户应用的新里程碑:Prisma-multi-tenant 当你的应用程序需要为多个独立的组织或客户群体提供服务时,多租户架构便是理想的选择。Prisma-multi-tenant是一个专为Prisma设计的强大工具,它让多租户数据库管理变得简单易行,让你可以在单一实例上无缝切换和操作不同的数据子集。 项目介绍 Prisma-multi-tenant,正如其名,是将Prisma扩展成一个支持...

gitblog_00006的博客 530

VisionMaster4.3.0+C#开发环境搭建实战:如何快速跑通第一个视觉检测demo?

本文详细介绍了海康VisionMaster 4.3.0机器视觉平台与C#的开发环境搭建实战。从软件安装、示例程序导入、解决NuGet包冲突,到配置并成功运行第一个视觉检测Demo,提供了完整的步骤指南和常见问题排查方法,帮助开发者快速上手二次开发。

weixin_29219189的博客 406

Tesseract OCR 字库训练教程:提高识别率进阶版

通过 Tesseract 字库训练,你可以提高 OCR 对特定文本或字体的识别能力。训练过程包括准备训练数据、生成标签文件、运行训练工具,以及将训练结果集成到实际项目中。虽然训练过程需要一些时间和精力,但对于需要高精度文本识别的场景来说,字库训练是非常有效的提升识别率的方法。

小蜗牛的珍贵百宝箱 1872

Prisma 多租户解决方案常见问题解决方案

Prisma 多租户解决方案常见问题解决方案 项目基础介绍 Prisma 多租户(Prisma-multi-tenant)是一个开源项目,它允许开发者使用 Prisma 作为多租户应用的数据提供者。多租户应用指的是单一应用实例在服务器上运行,为多个租户提供服务。每个租户都有自己独立的数据、配置、用户管理和功能。该项目主要是用 JavaScript 编写的,并且使用了 TypeScript 进行类型...

gitblog_00425的博客 495

C# 读取图片和扫描PDF中的文本

本博客将探讨如何使用C#和OCR库实现从图片和扫描的 PDF 中提取文本。Spire.OCR for .NET 是一个OCR文字识别库,专为在 .NET 应用程序中识别图片的文本而设计。首先,使用 Spire.PDF 将扫描的 PDF 转换为图片,然后利用 Spire.OCR 从这些图片中提取文本。由于该OCR库并不支持直接处理PDF,因此要从扫描的 PDF 中提取文本,我们首先需要将 PDF 文档转换为图片。转换完成后,再利用 Spire.OCR 从生成的图片中提取文本。方法来识别图片中的文本。

nuclear2011的博客 5405

多租户的 4 种常用方案

某中型电商平台的报表系统曾在深夜突然崩溃,起因竟是运营误删了共享表中的某租户数据列。运维团队排查发现,因为缺乏有效租户隔离,一条误操作的语句导致全平台数据混乱。选择多租户方案的每一步,都是安全与成本的权衡。今天这篇文章就跟大家一起聊聊,多租户的4种常用方案,希望对你会有所帮助。我最近开源了一个基于 SpringBoot+Vue+uniapp 的商城项目,里面的技术亮点挺多的,欢迎访问和star。susan_mall: susan_mall(苏三商城) 是基于当前流行技术组合的前后端分离商城系统。

2311

DeepSeek+Coze扣子搭建智能体(保姆级教程)

DeepSeek+Coze扣子搭建智能体(保姆级教程)

yizhen91y的博客 1万+

强网杯Web题ezphp解题全记录:利用PHP反序列化与条件竞争拿下服务器

本文详细解析了强网杯Web题目ezphp的解题过程,通过PHP反序列化与条件竞争漏洞的组合利用,成功获取服务器权限。文章深入剖析了反序列化链的构造、文件上传绕过技巧以及条件竞争的实际应用,为CTF选手和Web安全研究者提供了宝贵的实战经验。

weixin_29006187的博客 389

SaaS蓝图项目解析:从多租户架构到生产部署的完整实践

SaaS(软件即服务)是一种通过互联网提供软件应用的现代交付模式,其核心在于一套代码库服务多个租户(客户)。实现这一模式的关键在于多租户架构,它通过数据隔离技术确保不同客户的数据安全与隐私。从技术原理上看,多租户通常借助数据库层面的Schema隔离或Row-Level Security(行级安全)来实现,这要求开发者在设计数据访问层时进行周密规划。其技术价值在于显著降低了运维成本和硬件资源开销,同时便于统一进行功能更新。典型的应用场景包括CRM、ERP、项目管理等企业级工具。本文以GitHub上的SaaS-

weixin_30682415的博客 729

从原理到实践:手把手教你如何校准和使用六维力传感器(附常见问题排查)

本文详细解析六维力传感器的工作原理与校准方法,提供从选型到故障排查的完整指南。重点介绍电阻式与电容式技术对比、六步校准法实战以及温度补偿技巧,帮助工程师精准测量空间力场。适用于机器人触觉反馈、航空航天测试等高精度场景。

weixin_29309475的博客 388

终极Prisma数据库开发指南:从入门到精通的完整路径

Prisma是一个开源ORM(对象关系映射)工具,它通过类型安全和自动生成的查询构建器,为TypeScript和Node.js开发者提供了简单而强大的数据库访问方式。本指南将带你全面了解Prisma生态系统,从基础安装到高级应用,让你快速掌握这一现代数据库开发工具的核心技能。 ## 为什么选择Prisma? **Prisma的核心优势**在于它解决了传统ORM的诸多痛点: - **类型安全*

gitblog_00323的博客 497

innovus:set_clock_latency与insertion_delay

当1ns clock latency设置在capture上,在report_timing报告中体现在other end arrival time以及capture clock path的Arrival Time上。因为-1ns的insertion delay存在,所以skew group报告中显示的Arrival (ns)是减1ns的结果,即1.819-1=0.819 ns。CTS阶段create_ccopt_clock_tree_spec之后,spec中会产生对应的insertion_delay命令。

m0_61544122的博客 1626

【零基础入门unity游戏开发——unity通用篇12】导入导出unity资源包、设置编程工具、创建脚本(组件)规则、MonoBehaviour基类介绍、脚本模板(基于unity6开发介绍)

导入导出unity资源包、设置编程工具、创建脚本(组件)规则、MonoBehaviour基类介绍、脚本模板(基于unity6开发介绍)

向宇的博客,专注php/web全栈 unity游戏开发,欢迎大家评论纠错 764

Gitroom数据库设计:Prisma与PostgreSQL的最佳实践

Gitroom作为一个社交媒体调度和文章交换平台,其数据库设计采用了现代技术栈Prisma与PostgreSQL的完美结合。通过精心设计的数据库架构,Gitroom实现了高效的数据管理、强大的关系建模和优秀的可扩展性。本文将深入探讨Gitroom数据库设计的核心原则和最佳实践。 ## 数据库架构概览 Gitroom使用PostgreSQL作为主要数据库,通过Prisma ORM进行数据访问和管

gitblog_01048的博客 443

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

AI工程化开发:Superpowers与ui-ux-pro-max实战解析

在AI辅助开发领域,工程化结构和产品级质量是开发者面临的核心挑战。通过引入流程约束系统和设计决策支持机制,可以有效提升AI生成代码的质量和一致性。Superpowers作为开发流程控制系统,通过七个关键阶段(从需求澄清到交付收尾)实现结构化开发,特别适合中等复杂度以上的功能开发。而ui-ux-pro-max则通过任务类型识别、设计模式检索等机制,显著降低界面返工率。这两种技术的结合在RBAC系统开发等场景中展现出巨大价值,能将传统1-2周的工作量压缩至2-3天。本文以实际项目为例,详细解析如何通过工程化方法

weixin_30781107的博客 384

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

SaaS开发工具包核心架构解析:多租户、认证计费与工程实践

在构建现代SaaS应用时,多租户架构和模块化设计是两大核心技术基石。多租户通过数据库隔离策略实现数据安全与资源优化,其核心原理是在共享或独立的数据库结构中,通过租户标识实现逻辑隔离。这种架构能显著降低运维成本,提升资源利用率,其技术价值在于为规模化服务提供了可扩展的基础。在实际应用场景中,多租户常与用户认证、订阅计费等模块结合,形成完整的SaaS解决方案。本文聚焦的saascode-kit工具包,正是基于这些通用概念,将多租户实现与RBAC权限模型、Stripe支付集成等工程实践封装成开箱即用的模块,帮助开

weixin_28699983的博客 578

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

如何一次跑通 Windows/Office 的 KMS激活:KMS_VL_ALL_AIO 批量授权完整指南

Windows 桌面出现未激活水印、Office 反复弹出激活提醒时,KMS_VL_ALL_AIO 能一次处理这两类提示:这个单文件批处理脚本在本机回环地址上模拟一台 KMS 服务器,让系统与 Office 走批量授权(Volume License)通道完成 KMS激活,并可挂上周期任务做自动续期,全程不需要企业内网节点。对单机用户,它意味着功能受限的状态可以立刻恢复;对批量运维,它把逐台重复处理

gitblog_00817的博客 14

自托管状态面板全栈技术选型:TypeScript+Hono+SQLite实践

技术选型是每个全栈项目绕不开的决策节点,尤其在个人项目与MVP阶段,需要以边界清晰、可快速验证为原则。文章从单一语言与类型共享的优势出发,在React与Hono的组合中构建了自托管状态面板的前端与后端,数据层选用SQLite配合Prisma,以单文件数据库满足小规模状态聚合场景,降低运维成本。同时,通过数据源适配器、卡片注册表与定时调度器的设计,形成可扩展的系统骨架。文章完整记录了从选型逻辑到实施推进中的关键细节,包括联调代理、SQLite并发、模块格式与时区处理等典型坑点,为类似的全栈项目提供可复用的工程

weixin_30691649的博客 219

为什么选择WeWe RSS:打造高效微信公众号订阅的3大技术优势

在信息过载的时代,微信公众号已成为技术从业者获取高质量内容的重要渠道,但官方订阅体验的局限性让许多开发者感到困扰。今天,我们将深入探索WeWe RSS——一款基于微信读书接口的开源微信公众号RSS生成工具,看看它如何通过现代化技术栈解决内容聚合的痛点,为开发者提供优雅的私有化部署解决方案。 WeWe RSS不仅仅是一个简单的RSS生成器,它是一个完整的微信公众号内容聚合平台,支持多格式输出、智能

gitblog_00310的博客 420

从技术架构到实战部署:深入解析wewe-rss微信公众号RSS订阅引擎

在信息过载的时代,微信公众号作为高质量技术内容的重要来源,却因其封闭的订阅机制而让开发者们备受困扰。无法批量管理订阅、历史文章难以追溯、缺少个性化过滤等问题,都成为技术人获取信息的障碍。今天,我们将深入剖析wewe-rss——一款基于微信读书接口的现代化RSS订阅引擎,看看它如何通过优雅的技术架构解决这些痛点。 ## 技术架构深度解析:三层分离的现代化设计 wewe-rss采用清晰的三层架构设

gitblog_00559的博客 398

如何构建高效完整的微信公众号RSS聚合系统:wewe-rss架构深度解析

wewe-rss作为一个现代化的微信公众号RSS聚合工具,通过创新的**系统设计**理念和模块化**架构模式**,为开发者提供了一个完整的私有化部署解决方案。本文将深入探讨其**技术实现**细节,分析如何通过优雅的**扩展性**设计来满足不同场景的需求。 ## ✨ 核心理念:数据聚合与用户控制权的平衡 ### 🎯 去中心化内容聚合的设计哲学 wewe-rss的核心价值在于打破微信生态的封闭性

gitblog_00138的博客 427

WeWe RSS:终极微信公众号RSS订阅引擎深度解析与实战指南

在信息爆炸的时代,微信公众号已成为技术从业者获取高质量内容的重要渠道。然而,官方订阅体验存在诸多痛点:无法批量管理订阅源、历史文章难以追溯、缺少个性化过滤机制。WeWe RSS项目应运而生,为开发者提供了一套完整的微信公众号内容聚合解决方案,通过私有化部署和多格式输出,实现了优雅的RSS订阅体验。本文将深度解析这款基于微信读书接口的RSS生成工具的技术架构、核心实现和部署实践。 ## 技术架构解

gitblog_00786的博客 383

终极微信公众号RSS订阅方案:wewe-rss让信息聚合变得简单高效

在信息碎片化的时代,微信公众号已成为技术人获取高质量内容的重要渠道,但官方的订阅体验却让人头疼——无法批量管理、历史文章难以追溯、缺少个性化过滤。今天,我们为您介绍一款革命性的开源工具:wewe-rss,这是一个基于微信读书接口的微信公众号RSS生成工具,支持私有化部署和多格式输出,让您的信息聚合体验焕然一新。 wewe-rss采用现代化的技术栈,通过NestJS框架构建后端服务,结合Prism

gitblog_00143的博客 280
上一篇: Nestjs框架: RxJS 核心方法实践与错误处理详解
下一篇: Nestjs框架: 基于Mongodb的多租户功能集成和优化
Wang's Blog
博客等级 码龄12年 7438粉丝 2422原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值