当前位置:   article > 正文

Nestjs成长足迹(二):TypeORM操作数据库、Pipe校验参数_nestjs + typeorm

nestjs + typeorm

前言

本文章记录自己学习Nest的过程,适于前端及对后端没有基础但对Nest感兴趣的同学,如有错误,欢迎各位大佬指正

  • 在开始本文的正式内容前,先梳理一下项目的目录结构,本项目使用了与我前面发文使用koa2写接口的一般步骤相似的目录架构,主要就是controllersservicesmodulesdtoentities这几个重要目录

image.png

  • 在上一篇文章,已经初步介绍了Nest以及其swagger文档的使用,那么本篇便开始讲述Nest如何连接数据库、在创建实体中使用Pipe管道进行参数校验

连接mysql数据库

  • 有很多连接数据库的ORM(对象关系映射器),如TypeORMSequelize,在此我选择TypeORM,它本身是由TypeScript写的,对Nest的支持也会好点,而且Nest也提供了开箱即用的@nestjs/typeorm
  • 我们在使用前需要进行安装依赖
npm install --save @nestjs/typeorm
  • 1
  • 而我们需要使用TypeORM连接mysql数据库,那么我们需要安装依赖
npm install --save typeorm mysql2
  • 1

使用环境配置

  • nest带有环境配置
yarn add @nestjs/config
  • 1
  • @nestjs/config 默认会从项目根目录载入并解析.env 文件,从.env文件和process.env合并环境变量键值对,forRoot() 方法注册了 ConfigService 提供者,后者提供了一个 get() 方法来读取这些解析/合并的配置变量。要注入ConfigService,需要在需要使用的地方先导入ConfigModule
  • 而在app.module中使用了ConfigModule.forRoot(),将isGlobal设置为true,在其他地方使用时不需要做任何事,表示全局使用,此时就可以在全局范围内使用process.env.xxx读取全局变量
  • app.module.ts文件中使用@nestjs/config进行全局配置,以及使用@nestjs/typeorm提供的TypeOrmModule连接数据库如下
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
// 环境配置相关
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true
    }),
    TypeOrmModule.forRootAsync({
      useFactory: () => ({
        type: 'mysql',
        host: process.env.DATABASE_HOST,
        port: +process.env.DATABASE_PORT, // 来自process.env的每个值都是字符串,前面加+转数字
        username: process.env.DATABASE_USER,
        password: process.env.DATABASE_PASSWORD,
        database: process.env.DATABASE_NAME,
        autoLoadEntities: true, // 自动加载模块 推荐
        // entities: [path.join(__dirname, '/../**/*.entity{.ts,.js}')], // 不推荐
        synchronize: true // 开启同步,生产中要禁止
      })
    }),
  ],
  controllers: [],
  providers: []
})
export class AppModule {}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 上文代码中forRootAsync使用了TypeORM的异步工程模式,这样可以解决imports的顺序问题,也就是说,使用了forRootAsync,可以不用在意imports这个数组中使用TypeOrmModule的顺序,可以任意放,不用在意其他模块引入的顺序
  • 因为每个创建的实体必须在连接选项中进行注册,所以TypeORM提供了autoLoadEntities来自动加载创建的数据库实体,使用这种方式也比较推荐,也可以使用entities: [path.join(__dirname, '/../**/*.entity{.ts,.js}')]的方式,这表示,指定包含所有实体的整个目录,该目录下所有实体都将被加载
  • 本文使用的.env如下
DATABASE_USER=root
DATABASE_PASSWORD=root
DATABASE_NAME=nest-series
DATABASE_PORT=3306
DATABASE_HOST=localhost

SERVICE_PORT=1231
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

使用TypeORM

  • 进行注册实体,首先我们创建user用户实体,创建src/entities/user.entity.ts文件
    • Role是角色实体
// user.entity.ts
import {
  Entity,
  Column,
  JoinTable,
  ManyToMany,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn
} from 'typeorm';

import { Role } from './role.entity';

// @Entity()装饰器自动从所有类生成一个SQL表,以及他们包含的元数据
// @Entity('users') // sql表名为users
@Entity() // sql表名为user
export class User {
  // 主键装饰器,也会进行自增
  @PrimaryGeneratedColumn()
  id: number;

  // 列装饰器
  @Column()
  username: string;

  // @Column('json', { nullable: true }) json格式且可为空
  @Column()
  password: string;

  // 定义与其他表的关系
  // name 用于指定创中间表的表名
  @JoinTable({ name: 'user_roles' })
  // 指定多对多关系
  /**
   * 关系类型,返回相关实体引用
   * cascade: true,插入和更新启用级联,也可设置为仅插入或仅更新
   * ['insert']
   */
  @ManyToMany((type) => Role, (role) => role.users, { cascade: true })
  roles: Role[];

  @CreateDateColumn()
  createAt: Date;

  @UpdateDateColumn()
  updateAt: Date;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 创建角色实体,src/entities/role.entity.ts
// role.entity.ts
import {
  Entity,
  Column,
  ManyToMany,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn
} from 'typeorm';

import { User } from './user.entity';

@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany((type) => User, (user) => user.roles)
  users: User[];

  @CreateDateColumn()
  createAt: Date;

  @UpdateDateColumn()
  updateAt: Date;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 在这个实体中,我们用到了很多装饰器,那接下来我们看一下这些装饰器的作用
  • 以下并没有讲述很全,感兴趣的可以看看官网:
Entity(实体)
  • @Entity()来标记通过定义一个新类来创建的实体,装饰器自动从所有类生成一个SQL表,以及他们包含的元数据。
Column(普通列)
  • @Colimn()来标记普通列
  • 为列指定类型:
    • @Column("int")/@Column({ type: "int" }):数字类型
  • 还需要指定其他参数:
    • @Column("varchar", { length: 200 }):字符串类型,长度为200
    • @Column({ type: "int", length: 200 }):数字类型,长度为200
列选项
  • 列选项定义实体列的其他选项。 你可以在@Column()上指定列选项:
    • title: string: 数据库表中的列名。
    • unique: true:将列标记为唯一列,里面的值不可重复
    • nullable: boolean: 在数据库中使列NULLNOT NULL。 默认情况下,列是nullable:false
    • 更多的请看TypeORM中文文档
@Column({
    type: "varchar",
    length: 150,
    unique: true,
    // ...
})
title: string;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
主列
  • 每个实体都必须要有一个主列
  • @PrimaryColumn()来标记主列,需要给它手动分配值
  • @PrimaryGeneratedColumn()来标记主列,该值将使用自动增量值自动生成
  • @PrimaryGeneratedColumn('uuid')来标记主列,该值将使用uuid(通用唯一标识符)自动生成,uuid可以被认为是唯一的
    • uuid是让分布式系统中的所有元素都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的uuid。在这样的情况下,就不需考虑数据库创建时的名称重复问题
    • uuid的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-1232个字符,如:550e8400-e29b-41d4-a716-446655440000
ManyToMany(多对多关系)
  • @ManyToMany()指明多对多关系,多对多是一种A包含多个B实例,而B包含多个A实例的关系。比如:在一个系统中会有用户和角色,用户是会有很多个的,角色也是有很多个的,当然会有一个人是产线的研发Leader,他也可以是一个横向团队的Leader
  • @JoinTable()@ManyToMany()关系所必需的。
  • 保存这种关系,需要启动级联cascade
    • @ManyToMany((type) => Role, (role) => role.users, { cascade: true })
  • 创建用户时也将角色的信息也存到了数据库中,如果你这时去查询用户信息,结果是并没有返回关于角色信息的,那么该怎么查询呢

image.png

  • 使用relations表示需要加载主体,如下是一个查询用户列表所有数据的代码
    • 方式一:relations: { roles: true }
    • 方式二:relations: ['roles']
async getUserList() {
    return await this.userRepository.find({
      // 1
      // relations: {
      //   roles: true
      // },
      // 2
      relations: ['roles'],
    });
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

image.png

JoinTable(定义与其他表的多对多关系)
  • @JoinTable()用于多对多关系,使用后会由TypeORM自动创建一个单独表,这张表的默认名字为这两张关系表表名以下划线_连接的名字,也可以自定义这张表名,设置方法如上代码
  • 如上是一个双向的关系,注意,反向关系没有@JoinTable()@JoinTable()必须只在关系的一边(@JoinTable()user一边,而role则没有)
CreateDateColumn/UpdateDateColumn(创建时间列/更新时间列)
  • CreateDateColumn:自动为实体插入日期
  • UpdateDateColumn:在实体每次更新时,自动更新实体日期
创建的表
  • 表中数据是后面测试时,我造的数据
  • 对于查看数据库,可以自行安装可视化软件,如Navicat Premium,我这使用的是vscode的一款插件

image.png

image.png

image.png

管道Pipe

  • 对参数做校验,可以在Controller里做,但是这种验证逻辑是通用的,每个Controller里都做一遍太麻烦了,所以能不能在Controller之前就做好呢,这里我们可以使用Nest提供的管道Pipe。管道有两个典型的应用场景:
    • 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
    • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
  • 在这两种情况下, 管道 参数(arguments) 会由 控制器(controllers)的路由处理程序 进行处理。Nest 会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行转换或是验证处理,然后用转换好或是验证好的参数调用原方法。

Nest 自带九个开箱即用的管道,即

  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe
  • ParseFilePipe

他们从 @nestjs/common 包中导出。

类验证器

  • 安装一些依赖
npm i --save class-validator class-transformer
  • 1
  • 安装完后就可以在dto中添加装饰器了
    • IsString:检查值是否为字符串
    • IsNotEmpty:检查值是否不为空
装饰器描述
@IsBoolean()是否为布尔值
@IsString()是否为字符串
@IsInt()是否为整数
@IsArray()是否为数组
@IsNotEmpty()检查给定值是否不为空
// create-user.dto.ts
import { ApiProperty } from '@nestjs/swagger';

import { IsString, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  //ApiProperty是对数据类型的描述
  @ApiProperty({ description: '用户名', default: 'Kylin' })
  @IsNotEmpty({ message: '用户名不为空' })
  @IsString()
  username: string;

  @ApiProperty({ description: '密码', default: 'siJue' })
  @IsNotEmpty({ message: '密码不为空' })
  @IsString()
  password: string;

  @ApiProperty({ description: '角色', default: ['admin'] })
  @IsNotEmpty()
  @IsString({ each: true })
  roles: string[];
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • main.ts中使用全局管道
    • app.useGlobalPipes(new ValidationPipe());全局应用管道 对输入数据进行转换或者验证
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from '@/modules/app.module';

import { generateDocument } from './swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 设置全局路由前缀
  app.setGlobalPrefix('api');

  // 全局应用管道 对输入数据进行转换或者验证
  app.useGlobalPipes(new ValidationPipe());

  // 创建swagger文档
  generateDocument(app);

  await app.listen(+process.env.SERVICE_PORT, () => {
    console.log(`项目运行在http:localhost:${process.env.SERVICE_PORT}/api`);
  });
}
bootstrap();

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 以上使用好后,我们进行一个测试:

image.png

image.png

  • 在注册用户时,未输入用户名,就会报错

总结

  • 本篇介绍了使用TypeORM操作数据库以及使用Pipe管道进行参数校验
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/661366
推荐阅读
相关标签
  

闽ICP备14008679号