nestjs接入数据库
TypeORM 集成
安装所需的依赖项
1
| npm install --save @nestjs/typeorm typeorm mysql2
|
安装完成后,我们可以把 TypeORMModule 导入到 AppModule 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './users/user.entity';
@Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'db_nest', entities: [User], synchronize: true, }), ], }) export class AppModule {}
|
实体表如何引入
但是我们发现一个问题,如果表很多怎么办,总不能全部import进来之后写进去
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
| import { User } from './users/user.entity'; import { Todo } from './todo/todo.entity';
@Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'db_nest', entities: [User, Todo, synchronize: true, }), ], })
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm';
@Module({ imports: [ TypeOrmModule.forRoot({ ... autoLoadEntities: true, }), ], })
@Module({ imports: [ TypeOrmModule.forRoot({ ... entities: [__dirname + '/**/*.entity{.ts,.js}'], }), ], })
|
上面两种方式都可以成功集成TypeORM
TypeORM如何定义表结构
我们主要了解一下TypeORM的主要内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; import { Photo } from '../photos/photo.entity';
@Entity() export class User { @PrimaryGeneratedColumn() id: number;
@Column() firstName: string;
@Column() lastName: string;
@Column({ default: true }) isActive: boolean;
@OneToMany(type => Photo, photo => photo.user) photos: Photo[]; }
|
这里就定义了一个users表,如果你想要自定义表名称,可以在@Entity(‘user’)中添加 user,这样就是加载 user 表。
主要涉及到的是主键列,普通列,外键列(一对一、一对多、多对一、多对多),这里表示 users 表中的 photo 字段,关联到的是 Photo 实体,也就是 photos 表,如果它没有显示指定,然后是一对多的关系,表示一个用户会有多张照片,关联的字段是 photos 表中的 user 字段
在column装饰器中有很多有趣的属性,例如字段类型,字段名称,字段长度,是否为空,默认值等等,基本跟数据表的属性差不太多。
外键绑定的注意点
我们需要注意以下两种情况
1:级联删除
默认我们在有外键关联的数据表中,不会去主动设置cascade为true、onDelete为CASCADE
因为一旦设置过,删除数据时,外键关联到的数据也会被删除
2:级联查询
默认我们在查询数据的时候,有的时候我们并不希望将外键关联的数据给查询出来,所以默认查询单条数据时不会自动加载一对多关联的数据
但是当我指定 relations 时,它会去查询关联的数据,只有你加上了 eager为 true 的时候会自动查询
例如查询用户关联的图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const user = await userRepository.findOne({ where: { id: 1 }, relations: ['photos'], });
{ "id": 1, "photos": [ { "id": 10, "url": "xxx" }, { "id": 11, "url": "yyy" } ] }
{ "id": 1 }
|
TypeORM还有一种方式可以支持你查询关联数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Entity() export class User { @PrimaryGeneratedColumn() id: number;
@OneToMany(() => Photo, photo => photo.user) photos: Promise<Photo[]>; }
const user = await userRepository.findOne({ where: { id: 1 } });
const photos = await user.photos;
|
但是 lazy loading 有一个缺点,就是它是单个查询,如果我需要查询一批用户,然后再用循环语句去获取每个用户 photos 的时候,你的查询会变成 N+1 查询。也就是一次用户数据查询加上n次的用户photos查询,此用法会造成性能问题并且SQL很难优化。
所以,我还是更加推荐显示写 relations ,这样 SQL 才会可控。
多个数据库如何连接
通常我们的项目中可能会连接多个数据库,我们可以通过以下方式来创建多个连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Module({ imports: [ TypeOrmModule.forRoot({ ...defaultOptions, name: 'default', host: 'user_db_host', entities: [User], }), TypeOrmModule.forRoot({ ...defaultOptions, name: 'albumsConnection', host: 'album_db_host', entities: [Album], }), ], })
|
我们发现配置中多了name属性,这是因为如果只有一个连接,你可以不写name,默认就是 default,但是如果有多个连接,我们应该区分名称,这样连接不会被覆盖
然后我们在使用表的时候,需要告诉 TypeOrmModule.forFeature() 方法和 @InjectRepository() 装饰器应该使用哪个数据源,如果不写,默认是 default
1 2 3 4 5 6 7
| @Module({ imports: [ TypeOrmModule.forFeature([User]), TypeOrmModule.forFeature([Album], 'albumsConnection'), ], }) export class AppModule {}
|
如何使用TypeORM来编写业务代码
在处理完连接后,我们应该这样使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; import { User } from './user.entity';
@Module({ imports: [TypeOrmModule.forFeature([User])], providers: [UsersService], controllers: [UsersController], }) export class UsersModule {}
|
然后在userService 中我们可以直接使用表
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
| import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity';
@Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User>, ) {}
findAll(): Promise<User[]> { return this.usersRepository.find(); }
findOne(id: number): Promise<User | null> { return this.usersRepository.findOneBy({ id }); }
async remove(id: number): Promise<void> { await this.usersRepository.delete(id); } }
|
其他功能
当然我们后面还有transaction,异步配置等等,等需要的时候可以阅读文档进行使用或接入
nestjs接入验证
validation的集成
1
| $ npm i --save class-validator class-transformer
|
一般参数的验证都是可以放到全局的,有两种方式可以全局应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| { provide: APP_PIPE, useFactory: () => { return new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true, disableErrorMessages: false, }); } }
async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe({ transform: true })); await app.listen(process.env.PORT ?? 3000); } bootstrap();
|
我们发现 ValidationPipe 允许我们传递一些参数
- enableDebugMessages(boolean):在出现问题时向控制台打印额外的警告信息
- skipUndefinedProperties(boolean):跳过验证对象中所有未定义属性的验证
- whitelist(boolean):删除非白名单属性
- disableErrorMessages(boolean):不返回错误信息给客户端
- 还有其他的属性可以控制验证的行为
validation的使用
一般验证都是 dto 文件
1 2 3 4
| @Post() create(@Body() createUserDto: CreateUserDto) { return 'This action adds a new user'; }
|
dto 文件应该是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, } from 'class-validator';
enum Status { OK = 1, NOTOK = 2, }
export class CreateUserDto { @IsString() @IsNotEmpty() name: string; @IsOptional() @IsNumber() @IsEnum(Status) status?: Status; }
|
然后我们在请求创建新用户的时候,就会走 dto 的验证
1 2 3 4 5 6 7 8 9 10 11 12 13
| { status: 1 }
{ name: "Justin", status: 3 }
|
其他用法
当然你还可以针对 CreateUserDto 进行显示的转换,例如比如你还有一个字符串的数值,就可以在service中这么写来进行转换
1 2 3 4 5 6 7
| @Get(':id') findOne( @Param('id', ParseIntPipe) id: number, ) { console.log(typeof id === 'number'); return 'This action returns a user'; }
|
当然你可以创建一个新的用户,用的是 CreateUserDto,那么你也可以更新用户信息,用的就是 UpdateUserDto,我们发现,更新和创建的区别就是是否需要传递主键,并且更新的话,每个字段其实都是可选的,按照restful的风格,应该是传递两个参数,第一个是需要更新的数据的主键id,然后就是需要更新的数据
1
| export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
这样我们相当于直接复用了 CreateUserDto 的字段,并将每个字段变成可选类型
nestjs接入MONGO
mongoose 集成
安装所需的依赖项
1
| npm i @nestjs/mongoose mongoose
|
然后在根模块导入进行 mongoose 的使用
1 2 3 4 5 6 7
| import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose';
@Module({ imports: [MongooseModule.forRoot('mongodb://localhost/nest')], }) export class AppModule {}
|
如何定义mongo collection
使用 Mongoose,一切都是 Schema,每个 Schema 都映射到一个 MongoDB Collection
例如我们的用户表,其实可以放到 mongo 中
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
| import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { HydratedDocument } from 'mongoose';
export type UserDocument = HydratedDocument<User>;
@Schema({ timestamps: true, collection: 'user', toJSON: { virtuals: true, versionKey: false, transform: (_, ret: any) => { ret.id = ret._id.toString(); delete ret._id; return ret; }, }, }) export class User { @Prop() name: string;
@Prop() age: number;
@Prop() hobby: string; }
export const UserSchema = SchemaFactory.createForClass(User);
|
@Prop 装饰器是用来定义文档中的属性的,一些简单的字段类型它可以自动推断,但是一些复杂的需要我们去主动申明
例如一个数组字符串
1 2
| @Prop([String]) tags: string[];
|
Props 中还允许你传递一系列的 options 参数
1 2
| @Prop({ type: String, required: true }) name: string;
|
如何注册mongoose collection
上面我们定义了一个user collection,那么我们应该怎么将它注册进去呢?
1 2 3 4 5 6 7 8 9 10 11 12
| import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { UserController } from './user.controller'; import { UserService } from './user.service'; import { User, UserSchema } from './schemas/user.schema';
@Module({ imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])], controllers: [UserController], providers: [UserService], }) export class UserModule {}
|
如何使用mongoose来编写业务代码
在上面注册了 User Collection 之后,我们可以在service层编写相关的业务代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { Model } from 'mongoose'; import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { User } from './schemas/user.schema'; import { CreateUserDto } from './dto/create-user.dto';
@Injectable() export class UserService { constructor(@InjectModel(User.name) private userModel: Model<User>) {}
async create(createUserDto: CreateUserDto): Promise<User> { const cteatedUser = await this.userModel.create(createUserDto); return cteatedUser.toJSON(); }
async findAll(): Promise<User[]> { const docs = await this.userModel.find().exec(); return docs.map((d) => d.toJSON()); } }
|
[!warning] 注意
我们通过 Model 查询返回的其实是 mongoose 的 document
所以我们最好是通过 toJSON 对 document 数据进行一个处理返回
如何连接多个数据库
我们通常会连接多个mongoose库
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose';
@Module({ imports: [ MongooseModule.forRoot('mongodb://localhost/test', { connectionName: 'test', }), MongooseModule.forRoot('mongodb://localhost/users', { connectionName: 'users', }), ], }) export class AppModule {}
|
然后当我们主动设置了 connectionName 后,我们在注册的时候,应该带上 connectionName
1 2 3 4 5 6
| @Module({ imports: [ MongooseModule.forFeature([{ name: User.name, schema: UserSchema }], 'users'), ], }) export class UserModule {}
|
然后在service中这样使用
1 2 3 4
| @Injectable() export class UserService { constructor(@InjectModel(User.name, 'users') private userModel: Model<User>) {} }
|
其他用法
当然在mongoose中还有事务,插件,子文档等功能,等实际用到了可以查看文档进行补充学习