Nestjs框架: 数据库多租户模式与动态模块初探

概述

  • TypeORM 可以通过 TypeORM 中的 forRootAsync 方法读取点位文件中的配置信息,随后它会自动加载驱动,以便与各类数据库进行对接
  • Prisma 部分依托于 PrismaClient,无需手动安装数据库驱动。我们直接扩展 PrismaClient 就能得到一个 PrismaService,进而实现对数据库的读写操作
  • 至于 Mongoose 与 上面两个类似
  • 我们的目标是让项目既能与单库对接,也能应对多库场景

关于多租户

  • 以用户进入系统为例,NestJS 会读取当前用户的租户信息
  • 每个用户都携带不同的标签,不同的标签用于区分读取不同的数据库
  • 根据用户标签读取不同数据库的配置信息从而获取不同数据库的实例来操作对应的数据库
  • 这时候,就需要依赖 动态模块 (Dynamic Module)
    • 动态模块提供一种API, 将一个模块导入到另外一个模块
    • 并且定义该模块属性的时候可以 影响到 它的属性和行为
    • 动态模块 可以让人非常方便的自定义模块,并且注册和配置它
    • 在注册模块的过程中,对其配置,传入参数,注册逻辑进行定制
    • 这是分层化编程的思想, AOP 编程
  • 单库读取相对简单,而通用框架通常会涵盖单库功能
  • 所以,我们着重针对多库场景进行数据库设计
  • 要把程序封装起来,通过依赖注入的方式读取配置来定义 url

Prisma 的多租户系统集成

  • 之前,我们集成Prisma的时候,是用了一套配置,也就是一个数据库的配置
  • 在相关service里面,如下
    import { Injectable, OnModuleInit } from '@nestjs/common';
    import { PrismaClient } from '@prisma/client';
    
    @Injectable()
    export class PrismaService extends PrismaClient implements OnModuleInit {
      async onModuleInit() {
        await this.$connect();
      }
    }
    
    • 上面,PrismaService 继承了 PrismaClient 类,并实现了 OnModuleInit 这个生命周期方法
    • 该方法内部会调用 Prisma$connect 方法来连接对应的数据库
  • 如果应用场景需要通过 Prisma 操作多个数据库,就不能使用同一套配置
  • 因为不同用户可能属于不同租户,所以要以租户为单元,让不同用户读取不同的数据库配置
  • 这就引出了一个问题:Prisma 如何动态接收配置?
  • 参考: accessing-environment-variables-from-the-schema
  • 就是用程序的方式,动态的写这个 url, 也就是把 datasouce url 拆分成不同字段,这样的目的就是把 url 拆分成 host, user, password 等字段分开管理
  • 生成不同的 PrismaClient 存放到 map 中, 后续,当不同的 用户 过来之后,通过 map 定位到 不同的 PrismaClient 从而操作不同数据库
  • 普通的写法只能让 Prisma 读取配置文件中的一个数据库,如果要实现动态配置,需要参考 之前 TypeOrmModule.forRootAsync 的方法 传递给 prisma.module.ts,并通过依赖注入的方式将 ConfigService 注入其中
  • 接收到数据后,我们需要将其拼接成 Prisma 要求的结构,这就需要了解 connectionURLs 的格式,通常前面是数据库类型、用户密码、主机和端口,后面可能还有查询参数,参考:connection-urls

如何实现动态模块

  • 我们知道 TypeOrmModule.forRootTypeOrmModule.forRootAsync 都是 Dynamic modules 即动态模块
  • 我们探索其源码,如下
    import { DynamicModule } from '@nestjs/common';
    import { DataSource, DataSourceOptions } from 'typeorm';
    import { EntityClassOrSchema } from './interfaces/entity-class-or-schema.type';
    import { TypeOrmModuleAsyncOptions, TypeOrmModuleOptions } from './interfaces/typeorm-options.interface';
    export declare class TypeOrmModule {
        static forRoot(options?: TypeOrmModuleOptions): DynamicModule;
        static forFeature(entities?: EntityClassOrSchema[], dataSource?: DataSource | DataSourceOptions | string): DynamicModule;
        static forRootAsync(options: TypeOrmModuleAsyncOptions): DynamicModule;
    }
    
  • forRootAsync 能接收参数并返回一个 DynamicModule,这是 NestJS 中的重要概念
  • 这样,通过 注入 一个 configService 实例可以读取不同的配置, 不需要通过函数式编程 new 出来 多个 configService 实例导致性能问题
  • 关于动态模块,参考官网:dynamic-modules
  • 按照这个文档上的 modules#dynamic-modules
    import { Module, DynamicModule } from '@nestjs/common';
    import { createDatabaseProviders } from './database.providers';
    import { Connection } from './connection.provider';
    
    @Module({
      providers: [Connection],
      exports: [Connection],
    })
    export class DatabaseModule {
      static forRoot(entities = [], options?): DynamicModule {
        const providers = createDatabaseProviders(options, entities);
        return {
          module: DatabaseModule,
          providers: providers,
          exports: providers,
        };
      }
    }
    
  • 定义一个全新的动态模块,这个模块可以接受参数,它返回的也是一个模块结构
  • 它的作用是:让程序员方便的自定义模块,并且注册和配置它,也就是在注册模块的过程中,对其配置,传入参数,注册逻辑等进行定制,这就是AOP编程思想

1 )官方示例

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}
  • 这里有一个 UserModule, 里面定义了 UsersService

  • 下面又定义了一个 AuthModule 模块,里面使用了 UsersModule

    import { Module } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { UsersModule } from '../users/users.module';
    
    @Module({
      imports: [UsersModule],
      providers: [AuthService],
      exports: [AuthService],
    })
    export class AuthModule {}
    
  • 进入 AuthService 中, 里面使用了 UsersService 的实例

    import { Injectable } from '@nestjs/common';
    import { UsersService } from '../users/users.service';
    
    @Injectable()
    export class AuthService {
      constructor(private usersService: UsersService) {}
      /*
        Implementation that makes use of this.usersService
      */
    }
    
  • 这里做了3件事

    • 实例化 UsersModule, 传递导入 UsersModule的本身的其他模块,包含解析Providers依赖项,相当于初始化了 UsersService的实例,并且导出出来,这个实例被放入了 DI 系统中
    • 实例化 AuthModule, 使 UsersModule的导出服务实例可用于 AuthModule 中的组件,类似于参数的形式来注入和调用
    • 在 AuthModule 中注入 UsersModule 的实例

2 )动态模块的使用场景

  • 通过静态模块的绑定,消费模块时无法影响主机模块的配置。
  • 比如,我们有一个通用模块,这个模块要在不同场景中表现出不同的情况
  • 我们需要可定制化地传递 databaseURL,就像系统中的插件概念
  • 动态模块提供了一种 API,可以将一个模块导入另一个模块,并在定义该模块属性时影响其属性和行为
  • 与静态模块相比,动态模块多了定义模块属性和改变模块行为的功能
  • 动态模块并不是说是动态注册的,而是在注册的过程中,通过参数的方式影响其注册的行为

3 )另外的官方示例

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • 我们之前使用 ConfigService 是写死的,后续我们可以如上定制下此类ConfigModule

  • 下面开始调用 ConfigModule

    import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { ConfigModule } from './config/config.module';
    
    @Module({
      imports: [ConfigModule.register({ folder: './config' })],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
    
  • 这里调用了 ConfigModule.register 方法,我们看下这个 register 方法的初步面貌 (下面仅为过渡演示代码)

    import { DynamicModule, Module } from '@nestjs/common';
    import { ConfigService } from './config.service';
    
    @Module({})
    export class ConfigModule {
      static register(): DynamicModule {
        return {
          module: ConfigModule,
          providers: [ConfigService],
          exports: [ConfigService],
        };
      }
    }
    
  • 这里 register 是一个静态方法,返回的是 DynamicModule 类型

  • 内部 register 逻辑在 ConfigService 部分初步面貌如下 (下面仅为过渡演示代码)

    import { Injectable } from '@nestjs/common';
    import * as dotenv from 'dotenv';
    import * as fs from 'fs';
    import * as path from 'path';
    import { EnvConfig } from './interfaces';
    
    @Injectable()
    export class ConfigService {
      private readonly envConfig: EnvConfig;
    
      constructor() {
        const options = { folder: './config' };
    
        const filePath = `${process.env.NODE_ENV || 'development'}.env`;
        const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
        this.envConfig = dotenv.parse(fs.readFileSync(envFile));
      }
    
      get(key: string): string {
        return this.envConfig[key];
      }
    }
    
  • 完整的 ConfigModule 及其 register 如下

    import { DynamicModule, Module } from '@nestjs/common';
    import { ConfigService } from './config.service';
    
    @Module({})
    export class ConfigModule {
      static register(options: Record<string, any>): DynamicModule {
        return {
          module: ConfigModule,
          providers: [
            {
              provide: 'CONFIG_OPTIONS',
              useValue: options,
            },
            ConfigService,
          ],
          exports: [ConfigService],
        };
      }
    }
    
  • 完整的 ConfigService 如下

    import * as dotenv from 'dotenv';
    import * as fs from 'fs';
    import * as path from 'path';
    import { Injectable, Inject } from '@nestjs/common';
    import { EnvConfig } from './interfaces';
    
    @Injectable()
    export class ConfigService {
      private readonly envConfig: EnvConfig;
    
      constructor(@Inject('CONFIG_OPTIONS') private options: Record<string, any>) {
        const filePath = `${process.env.NODE_ENV || 'development'}.env`;
        const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
        this.envConfig = dotenv.parse(fs.readFileSync(envFile));
      }
    
      get(key: string): string {
        return this.envConfig[key];
      }
    }
    
  • 这里用了 @Inject 的装饰器,是为了让 ConfigModule 中的 ConfigService 接收到 上面 register 方法传递进来的 options 参数,利用了 providers 的写法

  • 上面的 providers 写法是在 nestjs 的 DI 容器里,全局给了 options 的参数,参数的名称为里面 provide 定义的 CONFIG_OPTIONS 字符串,后面,我们应该将这个字符串抽离成常量,如下

    export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';
    
  • 这样,就不会出错,容易统一修改

  • 后续使用的时候,就向在上面的 ConfigService 的 constructor 中,通过 @Inject 依赖注入的方式,注入到私有的变量中去, 下面就正常使用了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Wang's Blog

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

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

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

打赏作者

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

抵扣说明:

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

余额充值