【问题标题】:How can i setup multitenant in NESTJS我如何在 NESTJS 中设置多租户
【发布时间】:2019-09-28 20:11:43
【问题描述】:

我想连接到基于子域(多租户)的任何数据库,但我不知道该怎么做。

我的代码在应用启动时运行,但我不知道如何根据子域更改数据源。

PS:我为每个请求创建了中间件,但我不知道如何更改源。

我的数据库有以下代码:

import { connect, createConnection } from 'mongoose';
import { SERVER_CONFIG, DB_CONNECTION_TOKEN } from '../server.constants';

 const opts = {
    useCreateIndex: true,
    useNewUrlParser: true,
    keepAlive: true,
    socketTimeoutMS: 30000,
    poolSize: 100,
    reconnectTries: Number.MAX_VALUE,
    reconnectInterval: 500,
    autoReconnect: true,
  };
export const databaseProviders = [
  {
    provide: DB_CONNECTION_TOKEN,
    useFactory: async () => {
      try {
        console.log(`Connecting to ${ SERVER_CONFIG.db }`);
        return await createConnection(`${SERVER_CONFIG.db}`, opts);
      } catch (ex) {
        console.log(ex);
      }

    },
  }
];

我想根据子域(多租户)更改每个请求中的数据源

【问题讨论】:

    标签: mongodb multi-tenant nestjs


    【解决方案1】:

    这是我与猫鼬一起使用的解决方案

    1. TenantsService 用于管理应用中的所有租户
    @Injectable()
    export class TenantsService {
        constructor(
            @InjectModel('Tenant') private readonly tenantModel: Model<ITenant>,
        ) {}
    
        /**
         * Save tenant data
         *
         * @param {CreateTenantDto} createTenant
         * @returns {Promise<ITenant>}
         * @memberof TenantsService
         */
        async create(createTenant: CreateTenantDto): Promise<ITenant> {
            try {
                const dataToPersist = new this.tenantModel(createTenant);
                // Persist the data
                return await dataToPersist.save();
            } catch (error) {
                throw new HttpException(error, HttpStatus.BAD_REQUEST);
            }
        }
    
        /**
         * Find details of a tenant by name
         *
         * @param {string} name
         * @returns {Promise<ITenant>}
         * @memberof TenantsService
         */
        async findByName(name: string): Promise<ITenant> {
            return await this.tenantModel.findOne({ name });
        }
    }
    
    
    1. TenantAwareMiddleware 中间件从请求上下文中获取 tenant id。您可以在此处创建自己的逻辑以从请求标头或请求 url 子域中提取 tenant id。请求头提取方法如下所示。

    如果您想提取子域,也可以通过调用req.subdomainsRequest 对象中提取子域来完成,这将为您提供子域列表,然后您可以从中获取您正在寻找的子域那个。

    @Injectable()
    export class TenantAwareMiddleware implements NestMiddleware {
        async use(req: Request, res: Response, next: NextFunction) {
            // Extract from the request object
            const { subdomains, headers } = req;
    
            // Get the tenant id from header
            const tenantId = headers['X-TENANT-ID'] || headers['x-tenant-id'];
    
            if (!tenantId) {
                throw new HttpException('`X-TENANT-ID` not provided', HttpStatus.NOT_FOUND);
            }
    
            // Set the tenant id in the header
            req['tenantId'] = tenantId.toString();
    
            next();
        }
    }
    
    1. TenantConnection 此类用于使用 tenant id 创建新连接,如果现有连接可用,它将返回相同的连接(以避免创建额外的连接)。
    @Injectable()
    export class TenantConnection {
        private _tenantId: string;
    
        constructor(
            private tenantService: TenantsService,
            private configService: ConfigService,
        ) {}
    
        /**
         * Set the context of the tenant
         *
         * @memberof TenantConnection
         */
        set tenantId(tenantId: string) {
            this._tenantId = tenantId;
        }
    
        /**
         * Get the connection details
         *
         * @param {ITenant} tenant
         * @returns
         * @memberof TenantConnection
         */
        async getConnection(): Connection {
            // Get the tenant details from the database
            const tenant = await this.tenantService.findByName(this._tenantId);
    
            // Validation check if tenant exist
            if (!tenant) {
                throw new HttpException('Tenant not found', HttpStatus.NOT_FOUND);
            }
    
            // Get the underlying mongoose connections
            const connections: Connection[] = mongoose.connections;
    
            // Find existing connection
            const foundConn = connections.find((con: Connection) => {
                return con.name === `tenantDB_${tenant.name}`;
            });
    
            // Check if connection exist and is ready to execute
            if (foundConn && foundConn.readyState === 1) {
                return foundConn;
            }
    
            // Create a new connection
            return await this.createConnection(tenant);
        }
    
        /**
         * Create new connection
         *
         * @private
         * @param {ITenant} tenant
         * @returns {Connection}
         * @memberof TenantConnection
         */
        private async createConnection(tenant: ITenant): Promise<Connection> {
            // Create or Return a mongo connection
            return await mongoose.createConnection(`${tenant.uri}`, this.configService.get('tenant.dbOptions'));
        }
    }
    
    
    1. TenantConnectionFactory 这是自定义提供程序,可为您提供 tenant id 并帮助创建连接
    // Tenant creation factory
    export const TenantConnectionFactory = [
        {
            provide: 'TENANT_CONTEXT',
            scope: Scope.REQUEST,
            inject: [REQUEST],
            useFactory: (req: Request): ITenantContext => {
                const { tenantId } = req as any;
                return new TenantContext(tenantId);
            },
        },
        {
            provide: 'TENANT_CONNECTION',
            useFactory: async (context: ITenantContext, connection: TenantConnection): Promise<typeof mongoose>  => {
                // Set tenant context
                connection.tenantId = context.tenantId;
    
                // Return the connection
                return connection.getConnection();
            },
            inject: ['TENANT_CONTEXT', TenantConnection],
        },
    ];
    
    1. TenantsModule - 在这里您可以看到作为提供程序添加的TenantConnectionFactory,并且正在导出以在其他模块中使用。
    @Module({
      imports: [
        CoreModule,
      ],
      controllers: [TenantsController],
      providers: [
        TenantsService,
        TenantConnection,
        ...TenantConnectionFactory,
      ],
      exports: [
        ...TenantConnectionFactory,
      ],
    })
    export class TenantsModule {}
    
    1. TenantModelProviders - 由于您的租户模型依赖于租户连接,因此您的模型必须通过提供程序定义,然后包含在您初始化它们的模块中。
    export const TenantModelProviders = [
        {
            provide: 'USER_MODEL',
            useFactory: (connection: Connection) => connection.model('User', UserSchema),
            inject: ['TENANT_CONNECTION'],
        },
    ];
    
    1. UsersModule - 此类将使用模型。您还可以看到此处配置的中间件以作用于您的 Tenand db 路由。这种情况下,所有user 路由都是租户的一部分,将由租户数据库提供服务。
    @Module({
      imports: [
        CoreModule,
        TenantsModule,
      ],
      providers: [
        UsersService,
        ...TenantModelProviders,
      ],
      controllers: [UsersController],
    })
    export class UsersModule implements NestModule {
      configure(context: MiddlewareConsumer) {
        context.apply(TenantAwareMiddleware).forRoutes('/users');
      }
    }
    
    1. UsersService - 从用户模块访问租户数据库的示例实现
    @Injectable()
    export class UsersService {
    
        constructor(
            @Inject('TENANT_CONTEXT') readonly tenantContext: ITenantContext,
            @Inject('USER_MODEL') private userModel: Model<IUser>,
        ) {
            Logger.debug(`Current tenant: ${this.tenantContext.tenantId}`);
        }
    
        /**
         * Create a new user
         *
         * @param {CreateUserDto} user
         * @returns {Promise<IUser>}
         * @memberof UsersService
         */
        async create(user: CreateUserDto): Promise<IUser> {
            try {
                const dataToPersist = new this.userModel(user);
                // Persist the data
                return await dataToPersist.save();
            } catch (error) {
                throw new HttpException(error, HttpStatus.BAD_REQUEST);
            }
        }
    
        /**
         * Get the list of all users
         *
         * @returns {Promise<IUser>}
         * @memberof UsersService
         */
        async findAll(): Promise<IUser> {
            return await this.userModel.find({});
        }
    }
    
    

    【讨论】:

    • 使用范围请求会导致所有使用工厂的服务的性能发生变化?难道没有不涉及再次实例化服务的解决方案吗?
    • @ontimond 我想你可以,但你使用的策略只会改变你获取租户 ID 的方式。发布连接创建过程将保持不变。
    【解决方案2】:

    我们的 NestJS 设置也有一个多租户设置。
    您可以有一个中间件,根据请求决定使用哪个数据源。在我们的示例中,我们使用了 TypeORM,它在 NestJS 中有很好的集成。 TypeORM 包中有一些有用的功能。

    中间件

    export class AppModule {
      constructor(private readonly connection: Connection) {
      }
    
      configure(consumer: MiddlewareConsumer): void {
        consumer
          .apply(async (req, res, next) => {
            try {
              getConnection(tenant);
              next();
            } catch (e) {
              const tenantRepository = this.connection.getRepository(tenant);
              const tenant = await tenantRepository.findOne({ name: tenant });
              if (tenant) {
                const createdConnection: Connection = await createConnection(options);
                if (createdConnection) {
                  next();
                } else {
                  throw new CustomNotFoundException(
                    'Database Connection Error',
                    'There is a Error with the Database!',
                  );
                }
              }
            }
          }).forRoutes('*');
       }
    

    这是我们中间件的一个例子。 TypeORM 在内部管理连接。因此,您首先要尝试为该特定租户加载连接。如果有,那就好,否则就创建一个。这里的好处是,一旦创建连接,在 TypeORM 连接管理器中仍然可用。这样,您始终可以在路线中建立联系。
    在您的路线中,您需要为您的租户提供身份证明。在我们的例子中,它只是一个从 url 中提取的字符串。无论它是什么值,您都可以将其绑定到中间件中的请求对象。在您的控制器中,您再次提取该值并将其传递给您的服务。然后,您必须为您的租户加载存储库,然后一切顺利。

    服务类

    @Injectable()
    export class SampleService {
    
      constructor() {}
    
      async getTenantRepository(tenant: string): Promise<Repository<Entity>> {
        try {
          const connection: Connection = await getConnection(tenant);
          return connection.getRepository(Property);
        } catch (e) {
          throw new CustomInternalServerError('Internal Server Error', 'Internal Server Error');
        }
      }
    
      async findOne(params: Dto, tenant: string) {
    
        const entityRepository: Repository<Entity> = await this.getTenantRepository(tenant);
    
        return await propertyRepository.findOne({ where: params });
    
      }
    

    这就是我们应用程序中服务的样子。

    希望这会激励你,让你解决问题:)

    【讨论】:

    • 我希望看到它的完整 GIT 存储库以及 getConnection 方法的示例以及 MiddlewareConsumer。
    【解决方案3】:

    您应该使用带有工厂的提供程序在每个服务中注入连接详细信息,并相应地切换存储库。

    这里是连接工厂(假设请求包含租户 id):

    const connectionFactory = {
      provide: CONNECTION,
      scope: Scope.REQUEST,
      useFactory: (request: ExpressRequest) => {
        const { tenantId } = request;
    
        if (tenantId) {
          return getTenantConnection(tenantId);
        }
    
        return null;
      },
      inject: [REQUEST],
    };
    

    每个租户的连接可以这样得到:

    export function getTenantConnection(tenantId: string): Promise<Connection> {
      const connectionName = `tenant_${tenantId}`;
      const connectionManager = getConnectionManager();
    
      if (connectionManager.has(connectionName)) {
        const connection = connectionManager.get(connectionName);
        return Promise.resolve(connection.isConnected ? connection : connection.connect());
      }
    
      return createConnection({
        ...(tenantsOrmconfig as PostgresConnectionOptions),
        name: connectionName,
        schema: connectionName,
      });
    }
    

    然后就可以在每个服务中使用连接了:

    @Injectable()
    export class CatsService {
      private readonly catsRepository: Repository<Cat>;
    
      constructor(
        @Inject(CONNECTION) connection: Connection,
      ) {
        this.catsRepository = connection.getRepository(Cat);
      }
    
      create(createCatDto: CreateCatDto): Promise<Cat> {
        const cat = new Cat();
        cat.name = createCatDto.name;
    
        return this.catsRepository.save(cat);
      }
    
      async findAll(): Promise<Cat[]> {
        return this.catsRepository.find();
      }
    }
    

    请注意,本文提供了一个完整的存储库,其中包含所有相关的服务和设置,它进行了演练; https://thomasvds.com/schema-based-multitenancy-with-nest-js-type-orm-and-postgres-sql/.

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-09-03
      • 1970-01-01
      • 2020-06-23
      • 1970-01-01
      • 1970-01-01
      • 2021-06-07
      • 2021-07-21
      相关资源
      最近更新 更多