【NestJS应用从0到1】6.创建Entity基类及扩展仓储

544 阅读4分钟

在前面5章中,已经了解了所有的NestJS的基础能力,其实NestJS还有很多最佳实践,如缓存,CROS,CSRF等等,在官方文档中都有具体的使用和描述,这里没必要搬运 。传送门->

好的,那开始进入具体的业务。

背景

在我们日常业务开发中,很多能力都是公共的。

  • 对于Entity,几乎所有的类都包含一些基础字段,如id,createTime, updateTime, creatorId 巴拉巴拉。
  • 对于仓储能力,typeORM原生有很多能力,如findOne等,但是我们经常会有列表分页,查询特定状态的数据等需求,这些能力完全可以通过扩展一个基础仓储来实现不用每个业务类来写这些。

Code it

上面就很清楚实现分为两个部分,实现一个Entity基类和扩展仓库

Entity基类

实现

为了适应有些类是不会存在creator,将包含creatorId的单独扩展一个基类继承自BaseDefaultEntity。如果你的业务所有都拥有creatorId 你完全可以直接在一个基类中体现。

基类请务必使用抽象类 abstract,禁止实例化基类

import { EState } from '@/enums';
import { UserEntity } from '@/modules/user/user.entity';
import {
  BaseEntity,
  Column,
  CreateDateColumn,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

export abstract class BaseDefaultEntity extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  @CreateDateColumn()
  createTime: Date;

  // 定义一个updateTime字段,默认值是当前时间戳
  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  @UpdateDateColumn()
  updateTime: Date;

  @Column({ default: EState.Normal })
  state: EState;
}

// 一个带有creatorId的基类
export abstract class BaseWithCreatorEntity extends BaseDefaultEntity {
  @JoinColumn({ name: 'creatorId' })
  @ManyToOne(() => UserEntity, (user) => user.id, { nullable: true })
  creator: UserEntity;
  @Column({ nullable: true })
  creatorId: string;
}

使用

你会发现在BaseWithCreatorEntity中引用了UserEntity,那我们就实现一个简单的UserEntity。继承自上方的基类BaseDefaultEntity,因为用户是自注册模式,不存在后台创建所以不会存在creatorId,就没必要继承带有creatorId的基类

在实际的业务中,你的用户可能存在角色,所以你的UserEntity可能与UseraRoleEntity存在OneToMany的关联关系。下方不做代码示意

import { Entity, Column, OneToMany } from 'typeorm';
import { BaseDefaultEntity } from '@/db/entities/base.entity';

@Entity('user')
export class UserEntity extends BaseDefaultEntity {
  @Column({ nullable: true, unique: true, length: 20 })
  userName: string;

  @Column({ nullable: true, unique: true, length: 50 })
  phone: string;

  @Column({ nullable: true, length: 100 })
  password: string;

  @Column({ nullable: true, unique: true, length: 50 })
  passwordSalt: string;

  @Column({ nullable: true, unique: true, length: 20 })
  nickName: string;

  @Column({ nullable: true, length: 200 })
  avatorUrl: string;
}

ExtendRepository扩展仓储能力

我们就给所有的仓储赋予分页的能力,在具体的业务使用的时候通过依赖注入ExtendRepository来使用分页。

实现

  • 在实现的时候,需要定义一些统一的interface和enum。
  • 通过泛型实例化时注入具体Entity
import { EState } from '@/enums';
import {
  DataSource,
  EntityTarget,
  Repository,
  SelectQueryBuilder,
} from 'typeorm';

export enum EPaginationOrder {
  ASC = 'ASC',
  DESC = 'DESC',
}

export  class PaginationDto {
  size: number = 20;
  num: number = 1;
  order?: EPaginationOrder = EPaginationOrder.DESC;
  sort?: string = 'createTime';
  where?: Record<string, any>;
}
export interface PaginationResult<T> {
  data: T[];
  count: number;
}

// 这里必须继承自Repository
// 通过T泛型在实例化时注入具体的Entity
// 使用QueryBuilder实现
// 使用onlyQueryBuilder判断直接输出查询结果还是QB

export class ExtendRepository<T> extends Repository<T> {
  // 在具体业务Repository时,会使用构造函数注入实例化哦
  constructor(
    private dataSource: DataSource,
    private entity: EntityTarget<T>,
    private alias: string,
  ) {
    super(entity, dataSource.manager);
  }

  /** 分页查询
   * 分页查询
   * @param pagination 分页参数
   * @param onlyQueryBuilder 仅输出QueryBuilder而不是结果
   * @param queryBuilder  自定义QueryBuilder
   * @returns
   */
  async paginate(
    pagination: PaginationDto,
    onlyQueryBuilder: boolean = false,
    queryBuilder: SelectQueryBuilder<T> = null,
  ): Promise<PaginationResult<T> | SelectQueryBuilder<T>> {
    const { size, num, order, sort, where } = pagination;

    if (!queryBuilder) {
      queryBuilder = this.createQueryBuilder(this.alias);
    }
    if (where) {
      Object.keys(where).forEach((key) => {
        queryBuilder.andWhere(`${queryBuilder.alias}.${key} = :${key}`, {
          [key]: where[key],
        });
      });
    }
    queryBuilder.skip((num - 1) * size).take(size);

    if (sort) {
      queryBuilder.orderBy(`${queryBuilder.alias}.${sort}`, order);
    }
    if (onlyQueryBuilder) {
      return queryBuilder;
    }

    const [items, total] = await queryBuilder.getManyAndCount();

    return {
      data: items,
      count: total,
    };
  }
}

使用

仍然以User为例,我们在user.service中实例化ExtendRepository,并使用扩展的分页功能

import { Injectable } from '@nestjs/common';
import { UserEntity } from './user.entity';
import * as bcrypt from 'bcrypt';
import { CryptoUtil } from 'src/utils/crypto.util';
import { IWechatUserBaseAuthInfo } from '@/interface/wechat.interface';
import { EState } from '@/enums';
import { AuthDisableException } from '@/exception/custom-exception';
// import { ExtendRepository } from '@/db/repository/extend.repository';
import { ExtendRepository } from '@/db/repository/extend.repository';
import { DataSource } from 'typeorm';

@Injectable()
export class UserService {

  // !!!!!!
  // 重点就在这里,构造方法中注入dataSource,使用new关键字实例化ExtendRepository,泛型使用UserEntity
  
  private usersRepository: ExtendRepository<UserEntity>;
  constructor(private dataSource: DataSource) {
    this.usersRepository = new ExtendRepository<UserEntity>(
      dataSource,
      UserEntity,
      'user',
    );
  }
  
  // 使用pagination 获取用户列表。
  // 这里只是示意
 async getUserList() {
    return await this.usersRepository.paginate({
      num: 1,
      size: 10,
      sort: 'id',
      where: {
        state: EState.Normal,
      },
    });
  }
}

千万别忘了,在module中导入ExtendRepository

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserEntity } from './user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ExtendRepository } from '@/db/repository/extend.repository';

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity, ExtendRepository])],
  providers: [UserService],
  exports: [UserService, TypeOrmModule],
})
export class UserModule {}

完结,撒花 ❀❀❀❀❀❀