开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
前言
本文章记录自己学习
Nest
的过程,适于前端及对后端没有基础但对Nest
感兴趣的同学,如有错误,欢迎各位大佬指正
Nest
成长足迹系列:
-
在开始本文的正式内容前,先梳理一下项目的目录结构,本项目使用了与我前面发文使用koa2写接口的一般步骤相似的目录架构,主要就是
controllers
、services
、modules
、dto
、entities
这几个重要目录
- 在上一篇文章,已经初步介绍了
Nest
以及其swagger
文档的使用,那么本篇便开始讲述Nest
如何连接数据库、在创建实体中使用Pipe
管道进行参数校验
连接mysql数据库
- 有很多连接数据库的
ORM
(对象关系映射器),如TypeORM
、Sequelize
,在此我选择TypeORM
,它本身是由TypeScript
写的,对Nest
的支持也会好点,而且Nest
也提供了开箱即用的@nestjs/typeorm
包 - 我们在使用前需要进行安装依赖
npm install --save @nestjs/typeorm
- 而我们需要使用
TypeORM
连接mysql
数据库,那么我们需要安装依赖
npm install --save typeorm mysql2
使用环境配置
nest
带有环境配置
yarn add @nestjs/config
@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 {}
- 上文代码中
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
使用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;
}
- 创建角色实体,
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;
}
- 在这个实体中,我们用到了很多装饰器,那接下来我们看一下这些装饰器的作用
- 以下并没有讲述很全,感兴趣的可以看看官网:
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
: 在数据库中使列NULL
或NOT NULL
。 默认情况下,列是nullable:false
。- 更多的请看TypeORM中文文档
@Column({
type: "varchar",
length: 150,
unique: true,
// ...
})
title: string;
主列
- 每个实体都必须要有一个主列
- 用
@PrimaryColumn()
来标记主列,需要给它手动分配值 - 用
@PrimaryGeneratedColumn()
来标记主列,该值将使用自动增量值自动生成 - 用
@PrimaryGeneratedColumn('uuid')
来标记主列,该值将使用uuid
(通用唯一标识符)自动生成,uuid
可以被认为是唯一的uuid
是让分布式系统中的所有元素都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的uuid
。在这样的情况下,就不需考虑数据库创建时的名称重复问题uuid
的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12
的32
个字符,如:550e8400-e29b-41d4-a716-446655440000
。
ManyToMany(多对多关系)
@ManyToMany()
指明多对多关系,多对多是一种A
包含多个B
实例,而B
包含多个A
实例的关系。比如:在一个系统中会有用户和角色,用户是会有很多个的,角色也是有很多个的,当然会有一个人是产线的研发Leader
,他也可以是一个横向团队的Leader
。@JoinTable()
是@ManyToMany()
关系所必需的。- 保存这种关系,需要启动级联
cascade
@ManyToMany((type) => Role, (role) => role.users, { cascade: true })
- 创建用户时也将角色的信息也存到了数据库中,如果你这时去查询用户信息,结果是并没有返回关于角色信息的,那么该怎么查询呢
- 使用
relations
表示需要加载主体,如下是一个查询用户列表所有数据的代码- 方式一:
relations: { roles: true }
- 方式二:
relations: ['roles']
- 方式一:
async getUserList() {
return await this.userRepository.find({
// 1
// relations: {
// roles: true
// },
// 2
relations: ['roles'],
});
}
JoinTable(定义与其他表的多对多关系)
@JoinTable()
用于多对多关系,使用后会由TypeORM
自动创建一个单独表,这张表的默认名字为这两张关系表表名以下划线_
连接的名字,也可以自定义这张表名,设置方法如上代码- 如上是一个双向的关系,注意,反向关系没有
@JoinTable()
。@JoinTable()
必须只在关系的一边(@JoinTable()
在user
一边,而role
则没有)
CreateDateColumn/UpdateDateColumn(创建时间列/更新时间列)
CreateDateColumn
:自动为实体插入日期UpdateDateColumn
:在实体每次更新时,自动更新实体日期
创建的表
- 表中数据是后面测试时,我造的数据
- 对于查看数据库,可以自行安装可视化软件,如
Navicat Premium
,我这使用的是vscode
的一款插件
管道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
- 安装完后就可以在
dto
中添加装饰器了IsString
:检查值是否为字符串IsNotEmpty
:检查值是否不为空
装饰器 | 描述 |
---|---|
@IsBoolean() | 是否为布尔值 |
@IsString() | 是否为字符串 |
@IsInt() | 是否为整数 |
@IsArray() | 是否为数组 |
@IsNotEmpty() | 检查给定值是否不为空 |
- 想要了解更多可以点击class-validator查看
// 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[];
}
- 在
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();
- 以上使用好后,我们进行一个测试:
- 在注册用户时,未输入用户名,就会报错
总结
- 本篇介绍了使用
TypeORM
操作数据库以及使用Pipe
管道进行参数校验 - 下一篇:用户登录颁发
jwt