likes
comments
collection
share

NestJS最佳实践--#2整合Typeorm实现基本的CRUD操作及分页数据查询

作者站长头像
站长
· 阅读数 125

代码: github.com/slashspaces…

学习目标

  • yaml多环境配置
  • 使用NestJS、TypeORM、MySQL、Docker完成基本的CRUD操作及分页查询

依赖库安装

  • typeorm: 一个TS编写的ORM框架
  • @nestjs/typeorm:NestJS的TypeOrm整合模块
  • mysql2:是Node的Mysql操作库
  • @nestjs/config: NestJS的配置模块
  • yaml:yaml配置文件读取
  • cross-env: 设置当前环境
pnpm add mysql2 typeorm @nestjs/typeorm @nestjs/config yaml cross-env

创建MySQL数据库

以下是使用Docker Compose来安装MySQL的步骤:

  1. 创建一个名为docker-compose.yml的文件,并添加以下内容:

    # docker-compose 多容器部署
    # 启动命令 docker compose up --detach 后台启动
    version: '3.1'
    
    services:
      db:
        image: mysql
        container_name: mysql
        ports:
          - "3306:3306"
        restart: always
        environment:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: nest-best-practice
        volumes:
          - .volumes/db:/var/lib/mysql
    
      # 网页版数据库管理工具phpmyadmin(<http://localhost:8080>)
      phpmyadmin:
        container_name: mysql-phpmyadmin
        image: phpmyadmin
        restart: always
        ports:
          - 8080:80
    

    在这里,我们使用了MySQL的官方镜像,并在environment中配置了数据库的名称、用户名和密码。我们还将MySQL的端口映射到了主机的3306端口,并将数据卷挂载到了.volumes目录中(别忘了把.volumes添加到.gitignore中)。

  2. 运行以下命令来启动Docker容器:

    docker-compose up -d
    

    这个命令会在后台启动MySQL容器。如果一切正常,你应该能够通过http://localhost:8080访问phpmyadmin

配置模块—yaml多环境配置

一般在项目开发中,至少会经历 Dev -> Test -> Prod 三个环境。如果再富余一点的话,还会再多一个 Pre 环境。那么每个环境使用的数据库、Redis 或者其他的配置项都会随着环境的变换而改变,所以在实际项目开发中,多环境的配置非常必要。

NestJS 默认使用dotenv 读取.env 文件的形式来读取配置,但我们这里要介绍的是另外一种更简洁明了的配置方式—yaml

  1. 安装依赖

    pnpm add @nestjs/config yaml cross-env
    
  2. 在根目录下创建.config 文件夹,并创建相应环境的yaml文件

    +-- .config
    |   +-- dev.yml
    |   +-- test.yml
    |   +-- prod.yml
    
    # dev.yml
    DB:
      type: "mysql"
      host: "localhost"
      port: 3306
      username: "root"
      password: "root"
      database: "nest-best-practice"
      logging:  ['error']
      # 启动数据库时自动根据加载的模型(Entity)来同步数据表到数据库
      synchronize: true
      # 这样我们就不需要把每个模块的Entity逐个定死地添加到配置中的entities数组中了,
      # 因为你可以在每个模块中使用TypeOrmModule.forFeature来动态的加入Entity
      autoLoadEntities: true
    
  3. 新建 src/common/utils/config.ts 文件,添加读取 YAML 配置文件的方法:

    import * as fs from 'fs';
    import * as path from 'path';
    
    import { parse } from 'yaml';
    
    /**
     * 根据process.env.NODE_ENV读取配置文件:
     * 1. dev-> dev.yaml
     * 2. test-> test.yaml
     * 3. prod-> prod.yaml, 切记 prod.yaml不要暴露在网络上
     */
    export const getConfig = () => {
        const environment = process.env.NODE_ENV;
        const projectPath = process.cwd();
        const yamlPath = path.join(projectPath, `./.config/${environment}.yml`);
        const file = fs.readFileSync(yamlPath, 'utf8');
        const config = parse(file);
        return config;
    };
    
  4. app.module.ts 中导入ConfigModule , 后面可以通过ConfigService读取yaml中的配置

    @Module({
        imports: [
            ConfigModule.forRoot({
                isGlobal: true, // 全局模块
                ignoreEnvFile: true, // 忽略.env相关配置文件
                load: [getConfig], // 读取自定义文件
            }),
        ],
    })
    export class AppModule {}
    
  5. 使用cross-envpackage.jsonscripts中指定运行环境

    {
      "start": "cross-env NODE_ENV=dev nest start",
      "start:dev": "cross-env NODE_ENV=dev nest start --watch",
      "start:debug": "cross-env NODE_ENV=dev nest start --debug --watch",
      "start:prod": "cross-env NODE_ENV=prod node dist/main",
    }
    

数据库模块

  1. 新建 src/modules/database/database.module.ts

    @Module({})
    export class DataBaseModule {
        static forRoot(): DynamicModule {
            return {
                global: true,
                module: DataBaseModule,
                imports: [
                    TypeOrmModule.forRootAsync({
                        imports: [ConfigModule],
                        inject: [ConfigService],
                        useFactory(configService: ConfigService): TypeOrmModuleOptions {
                            const mysqlConnectionOptions = configService.get('DB');
                            return { ...mysqlConnectionOptions };
                        },
                    }),
                ],
            };
        }
    }
    
  2. app.module.ts中导入 DataBaseModule

    @Module({
        imports: [
            ConfigModule.forRoot({
                isGlobal: true, // 全局模块
                ignoreEnvFile: true, // 忽略.env相关配置文件
                load: [getConfig], // 读取自定义文件
            }),
            DataBaseModule.forRoot(),
        ],
    })
    export class AppModule {}
    
  3. 执行 pnpm start:dev 启动项目,如果能看到下图所示输出,就证明项目已经能够跑起来了。

    NestJS最佳实践--#2整合Typeorm实现基本的CRUD操作及分页数据查询

业务模块

创建完数据库模块之后,就可以开始业务代码的编写了。

  • 创建 src/modules/business 目录,此目录下存放业务模块(用户模块,文章模块,分类模块…)

  • 借助 Nest CLI 命令 nest g res post 快速生成文章模块的模板代码

    NestJS最佳实践--#2整合Typeorm实现基本的CRUD操作及分页数据查询

  • src/post 目录 迁移到 src/modules/business/post

Entity

使用TypeORM时最重要的概念是实体。它是一个映射到数据库表的类。你可以通过定义一个新类来创建一个实体,并用@Entity()来标记。一个Entity对应数据库中的一张表。@Column代表表中的一列。

  • PrimaryGeneratedColumn("uuid")创建一个主列,该值将使用uuid自动生成。 Uuid 是一个独特的字符串 id。 你不必在保存之前手动分配其值,该值将自动生成。
  • @CreateDateColumn 是一个特殊列,自动为实体插入日期。无需设置此列,该值将自动设置。
  • @UpdateDateColumn 是一个特殊列,在每次调用实体管理器或存储库的save时,自动更新实体日期。无需设置此列,该值将自动设置。
  • @Column({ comment: '关键字', type: 'simple-array', nullable: true }) 可以@Column的列选项中指定列类型
// 创建post表
@Entity('post')
export class Post extends BaseEntity {
		// 主键,非空且唯一
    @PrimaryGeneratedColumn('uuid')
    id!: string;

    @Column({ comment: '文章标题' })
    title!: string;

    @Column({ comment: '文章内容', type: 'longtext' })
    body!: string;

    @Column({ comment: '文章描述', nullable: true })
    summary?: string;

		//simple-array 将原始数组值存储在单个字符串列中。 所有值都以逗号分隔
		// ['JavaScript', 'TypeScript'] ==> JavaScript,TypeScript
    @Column({ comment: '关键字', type: 'simple-array', nullable: true })
    keywords?: string[];

    @Column({
        comment: '发布时间',
        type: 'varchar',
        nullable: true,
    })
    publishedAt?: Date | null;

    @Column({ comment: '文章排序', default: 0 })
    customOrder!: number;

    @CreateDateColumn({
        comment: '创建时间',
    })
    createdAt!: Date;

    @UpdateDateColumn({
        comment: '更新时间',
    })
    updatedAt!: Date;
}

Repositories

通过使用Repositories,我们可以管理特定的实体。Repositories封装了对实体的APIs,如insert, update, delete, load 等。

为了使用它,我们要做些设置

  1. 注册Post实体类
@Module({
    imports: [TypeOrmModule.forFeature([Post])],
    controllers: [PostController],
    providers: [PostService],
})
export class PostModule {}
  1. PostService中注入 Post Repository
import { InjectRepository } from '@nestjs/typeorm';

constructor(
   @InjectRepository(Post)
   private postsRepository: Repository<Post>,
) {}

Service

一般不会在Controller中执行业务逻辑,而是在Service层中通过调用Repository去执行数据库操作

@Injectable()
export class PostService {
    constructor(
        @InjectRepository(Post)
        private postsRepository: Repository<Post>,
    ) {}

    async create(createPostDto: CreatePostDto) {
        const post = this.postsRepository.create(createPostDto);
        await this.postsRepository.save(post);
        return post;
    }

    findAll() {
        return this.postsRepository.find();
    }

    findOne(id: string) {
        return this.postsRepository.findOneBy({ id });
    }

    update(id: string, updatePostDto: UpdatePostDto) {
        this.postsRepository.update({ id }, updatePostDto);
        const updatedPost = this.postsRepository.findOneBy({ id });
        if (updatedPost) return updatePostDto;
        throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
    }

    async remove(id: string) {
        const deleteResponse = await this.postsRepository.delete(id);
        if (!deleteResponse.affected) {
            throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
        }
    }
}

Controller

  • 定义路由
  • 调用Service层逻辑,并将结果返回给用户
@Controller('post')
export class PostController {
    constructor(private readonly postService: PostService) {}

    @Post()
    create(@Body() createPostDto: CreatePostDto) {
        return this.postService.create(createPostDto);
    }

    @Get()
    findAll() {
        return this.postService.findAll();
    }

    @Get(':id')
    findOne(@Param('id') id: string) {
        return this.postService.findOne(id);
    }

    @Patch(':id')
    update(@Param('id') id: string, @Body() updatePostDto: UpdatePostDto) {
        return this.postService.update(id, updatePostDto);
    }

    @Delete(':id')
    remove(@Param('id') id: string) {
        return this.postService.remove(id);
    }
}

此时再启动服务,可以看到控制台输出了与Post相关的路由信息:

NestJS最佳实践--#2整合Typeorm实现基本的CRUD操作及分页数据查询

现在,我们就可以使用/post接口来进行用户的增删改查操作了。例如,使用POST /post接口来创建文章

NestJS最佳实践--#2整合Typeorm实现基本的CRUD操作及分页数据查询

相应的数据库中post表也会多出一行记录

NestJS最佳实践--#2整合Typeorm实现基本的CRUD操作及分页数据查询

分页查询

  1. 安装依赖库

    pnpm install class-transformer class-validator
    
  2. 创建 src/common/dtos/pagination.dto.ts 文件,定义分页查询的参数

    import { Type } from 'class-transformer';
    import { IsNumber, IsOptional, Min } from 'class-validator';
    
    export class PaginationDto {
        /**
         * 查询的页码,默认为 1
         */
        @IsNumber()
        @Min(1)
        @IsOptional()
        @Type(() => Number)
        readonly page: number = 1;
    
        /**
         * 每页显示的条数,默认为 10
         */
        @IsNumber()
        @Min(5)
        @IsOptional()
        @Type(() => Number)
        readonly limit: number = 10;
    }
    
  3. 创建 src/common/utils/database.ts

    /**
     * 分页返回结果类
     */
    type PaginationReturn<Entity extends ObjectLiteral> = {
        items: Entity[];
        count: number;
    };
    /**
     * 分页查询函数
     */
    export const paginate = async <Entity extends ObjectLiteral>(
        repository: Repository<Entity>,
        { limit: take, page }: PaginationDto,
        options?: FindOneOptions<Entity>,
    ): Promise<PaginationReturn<Entity>> => {
        const skip = (page - 1) * take;
        const [items, count] = await repository.findAndCount({
            skip,
            take,
            ...options,
        });
        console.log(items, count);
        return {
            items,
            count,
        };
    };
    
  4. post.controller.ts 中添加分页查询的路由

    // PostController
    @Get('page')
    paginate(@Query() paginationDto: PaginationDto) {
        return this.postService.paginate(paginationDto);
    }
    
    // PostService
    paginate(paginationDto: PaginationDto) {
        return paginate(this.postsRepository, paginationDto);
    }
    

现在,访问 /post/page?page=1&limit=1 就可以进行分页查询了。

NestJS最佳实践--#2整合Typeorm实现基本的CRUD操作及分页数据查询

希望这篇文章能够帮助你了解如何使用NestJS和Typeorm来实现基本的CRUD操作和分页查询。如果你有任何问题或建议,请在下面的评论中留言,感谢阅读!