Nestjs使用MySql的基本CRUD搭建

前边我们已经搭建了一个Nest项目,现在我们来看一下怎么写一个CRUD的项目。

NestJs基础使用

我们通过运行npm run start:dev将搭建好的NestJs项目运行起来,然后看一下main.ts的内容:

1
2
3
4
5
6
7
8
9
// main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
const app = await NestFactory.create(AppModule)
await app.listen(3000)
}
bootstrap()

其他内容看不懂可以不关注,只看一行await app.listen(3000)我们可以知道服务是运行在3000端口上的,此时我们通过postman访问localhost:3000看一下:

1
Hello World!

服务返回了一句字符串Hello World!,我们看一下app.controller.tsapp.service.ts,可以知道这个请求被app.controller.ts处理的,在内部调用了app.service.ts中的方法获得内容,这是一个解耦的做法,所以在后边我们也一样在controller中处理请求,在service中来处理逻辑。

我们的接口的路径肯定不能都是/,所以需要定义,在app.controller.ts中进行如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
// app.controller.ts
import { Controller, Get } from '@nestjs/common'
import { AppService } from './app.service'

@Controller('app')
export class AppController {
constructor(private readonly appService: AppService) {}

@Get('hello')
getHello(): string {
return this.appService.getHello()
}
}

可以发现我们在@Controller@Get两个装饰器中添加了内容:

  • @Controller('app'): 这个Controller下的所有请求都有app这个前缀
  • @Get('hello'): 这个请求接口的路径是hello

所以现在我们要访问这个接口应该访问的路径是http://localhost:3000/app/hello

现在我们只是控制了整个Controller的前缀,假如我们要对整个项目添加一个公共前缀比如nest怎么做呢?

全局的操作需要在main.ts中进行处理了:

1
2
3
4
5
6
7
8
9
10
11
// main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
const app = await NestFactory.create(AppModule)
+ app.setGlobalPrefix('nest')
await app.listen(3000)
}
bootstrap()

现在我们就需要通过http://localhost:3000/nest/app/hello来访问之前的那个接口了。

搭建CRUD代码结构

我们可以通过nest g res 模块名快速生成REST CRUD代码。生成的内容默认是有测试文件的,可以添加--no-spec参数来忽略测试文件,避免文件太多干扰我这里就不生成测试文件了,在项目根目录执行nest g res books --no-spec命令,选择REST API生成entity,我们会看到在src目录下生成了一个books目录,其内容如下:

1
2
3
4
5
6
7
8
9
books
| -- books.module.ts
| -- books.controller.ts
| -- books.service.ts
| -- dto
| | -- create-book.dto.ts
| | -- update-book.dto.ts
| -- entities
| | -- book.entity.ts

而且我们此时去查看app.module.ts的内容,发现我们新建的books模块已经自动引入了:

1
2
3
4
5
6
7
8
9
10
11
12
// app.module.ts
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { BooksModule } from './books/books.module'
@Module({
imports: [BooksModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

目前books.controller.tsbooks.service.ts都已经有一些预置内容了,Nestjs帮我们生成了REST API,但是在这里为了后续的内容讲解并不准备用REST API,而是只用GETPOST请求来实现逻辑,所以我们将这两个文件的内容做一些删减变成如下模样,后边我们自己来实现CRUD接口:

1
2
3
4
5
6
7
8
9
10
// books.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common'
import { BooksService } from './books.service'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'

@Controller('books')
export class BooksController {
constructor(private readonly booksService: BooksService) {}
}
1
2
3
4
5
6
7
// books.service.ts
import { Injectable } from '@nestjs/common'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'

@Injectable()
export class BooksService {}
1
2
3
4
5
6
7
8
9
10
// books.module.ts
import { Module } from '@nestjs/common'
import { BooksService } from './books.service'
import { BooksController } from './books.controller'

@Module({
controllers: [BooksController],
providers: [BooksService],
})
export class BooksModule {}

如果你想使用REST API,那么在这个生成代码的基础上直接扩展是更方便的

Mysql和Typeorm引入

接下来我们先不急于去实现我们的CRUD接口,后端总是要操作数据库,所以我们先来引入数据库的配置。这里用到了MysqlTypeorm

Typeorm后续只会有一些简单的应用,不必担心无法理解。

如果想全面了解Typeorm内容,请访问其官方文档进行学习。

安装所需依赖:

1
npm install --save @nestjs/typeorm typeorm mysql2

接下来在app.module.ts中配置TypeOrmModule

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
// app.module.ts
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { BooksModule } from './books/books.module'
// 引入entity
import { Book } from './books/entities/book.entity'
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: '你的ip',
port: 3306,
username: 'username',
password: 'password',
database: 'dbName',
// 注册实体
entities: [Book],
synchronize: true,
}),
BooksModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

现在我们还没有定义实体内容,等entity定义完成之后运行项目,会自动在数据库中创建book这个表。如果表不存在会自动创建,如果字段进行了修改也会自动同步

现在我们来定义实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// book.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
@Entity()
export class Book {
@PrimaryGeneratedColumn('uuid')
id: string

@Column()
title: string

@Column()
anchor: string

@Column({
nullable: true,
default: null,
})
pageSize: number

@Column({ default: true })
isActive: boolean
}

通过@Entity()装饰器声明这是一个实体,@Column()用于声明列,@PrimaryGeneratedColumn('uuid')用于声明这是一个自动生成的主键且由uuid填充。

此时等待项目运行,我们查看数据库的book这个表desc book;

1
2
3
4
5
6
7
8
9
10
+----------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| id | varchar(36) | NO | PRI | NULL | |
| title | varchar(255) | NO | | NULL | |
| anchor | varchar(255) | NO | | NULL | |
| pageSize | int | YES | | NULL | |
| isActive | tinyint | NO | | 1 | |
+----------+--------------+------+-----+---------+-------+
5 rows in set (0.00 sec)

此时表已经成功创建出来了。我们知道Typeorm可以通过实体来获取对应的存储库,通过这个存储库来进行数据库操作,我们在books.module.ts定义用注册的存储库:

1
2
3
4
5
6
7
8
9
10
11
12
// books.module.ts
import { Module } from '@nestjs/common'
import { BooksService } from './books.service'
import { BooksController } from './books.controller'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { Book } from './entities/book.entity'
@Module({
+ imports: [TypeOrmModule.forFeature([Book])],
controllers: [BooksController],
providers: [BooksService],
})
export class BooksModule {}

接下来我们可以通过@InjectRepository()装饰器将存储库注入到service中,这样我们就可以在service中进行数据库操作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// books.service.ts
import { Injectable } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { Repository } from 'typeorm'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'
+import { Book } from './entities/book.entity'
@Injectable()
export class BooksService {
+ constructor(
+ @InjectRepository(Book)
+ private booksRepository: Repository<Book>,
+ ) {}
}

CRUD操作

写在最前边,这里我们不考虑各种校验等内容,只是把CRUD操作完成

新增数据

在新增之前我们实现一下create-book.dto.ts

1
2
3
4
5
6
7
8
9
// create-book.dto.ts

export class CreateBookDto {
title: string
anchor: string
pageSize: number
isActive: boolean
}

books.controller.ts中添加接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// books.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common'
import { BooksService } from './books.service'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'

@Controller('books')
export class BooksController {
constructor(private readonly booksService: BooksService) {}

+ @Post('create')
+ createBook(@Body() createBookDto: CreateBookDto) {
+ return this.booksService.createBook(createBookDto)
+ }
}

books.service.ts进行处理:

typeorm可以通过save()创建数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// books.service.ts
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'
import { Book } from './entities/book.entity'
@Injectable()
export class BooksService {
constructor(
@InjectRepository(Book)
private booksRepository: Repository<Book>,
) {}

+ createBook(createBookDto: CreateBookDto) {
+ return this.booksRepository.save(createBookDto)
+ }
}

此时我们使用postman访问http://localhost:3000/nest/books/create接口发送POST请求,请求体如下:

1
2
3
4
5
6
{
"title": "三国演义",
"anchor": "罗贯中",
"pageSize": 30,
"isActive": true
}

响应结果为:

1
2
3
4
5
6
7
{
"title": "三国演义",
"anchor": "罗贯中",
"pageSize": 30,
"isActive": true,
"id": "096dd811-770d-49bb-a160-be11813e6942"
}

此时我们去数据库进行查询

1
2
3
4
5
6
7
mysql> select * from book;
+--------------------------------------+--------------+-----------+----------+----------+
| id | title | anchor | pageSize | isActive |
+--------------------------------------+--------------+-----------+----------+----------+
| 096dd811-770d-49bb-a160-be11813e6942 | 三国演义 | 罗贯中 | 30 | 1 |
+--------------------------------------+--------------+-----------+----------+----------+
1 row in set (0.00 sec)

查询数据

在进行查询之前,我们往数据库又添加了几条数据:

1
2
3
4
5
6
7
8
+--------------------------------------+--------------+-----------+----------+----------+
| id | title | anchor | pageSize | isActive |
+--------------------------------------+--------------+-----------+----------+----------+
| 096dd811-770d-49bb-a160-be11813e6942 | 三国演义 | 罗贯中 | 30 | 1 |
| 5176c094-7c78-4aaf-ba16-488965583d09 | 西游记 | 吴承恩 | 300 | 1 |
| 95a5143d-f521-42ae-88cc-e5b09943db34 | 红楼梦 | 曹雪芹 | 300 | 1 |
| 9617e315-1f6f-48f9-b9b0-2ad9baec26ba | 水浒传 | 施耐庵 | 300 | 1 |
+--------------------------------------+--------------+-----------+----------+----------+

books.controller.ts添加接口:

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
// books.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common'
import { BooksService } from './books.service'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'

@Controller('books')
export class BooksController {
constructor(private readonly booksService: BooksService) {}

@Post('create')
createBook(@Body() createBookDto: CreateBookDto) {
return this.booksService.createBook(createBookDto)
}

+ @Get('findAll')
+ findAllBook() {
+ return this.booksService.findAllBook()
+ }

+ @Get('findOne/:id')
+ findOneBook(@Param('id') id: string) {
+ return this.booksService.findOneBook(id)
+ }
}

我们通过@Param装饰器来接收Param参数,如果有多个可以分开定义:

@Get(‘findOne/:id/:name’)

findOneBook(@Param(‘id’) id: string, @Param(‘name’) name: string) {}

@Param接收两个参数,第一个参数是字段名,第二个是可选字段,用来传递管道

books.service.ts添加处理逻辑

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
// books.service.ts
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'
import { Book } from './entities/book.entity'
@Injectable()
export class BooksService {
constructor(
@InjectRepository(Book)
private booksRepository: Repository<Book>,
) {}

createBook(createBookDto: CreateBookDto) {
return this.booksRepository.save(createBookDto)
}

+ findAllBook() {
+ return this.booksRepository.find()
+ }

+ findOneBook(id: string) {
+ return this.booksRepository.findOneBy({
+ id,
+ })
+ }
}

调用http://localhost:3000/nest/books/findAll接口可得:

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
[
{
"id": "096dd811-770d-49bb-a160-be11813e6942",
"title": "三国演义",
"anchor": "罗贯中",
"pageSize": 30,
"isActive": true
},
{
"id": "5176c094-7c78-4aaf-ba16-488965583d09",
"title": "西游记",
"anchor": "吴承恩",
"pageSize": 300,
"isActive": true
},
{
"id": "95a5143d-f521-42ae-88cc-e5b09943db34",
"title": "红楼梦",
"anchor": "曹雪芹",
"pageSize": 300,
"isActive": true
},
{
"id": "9617e315-1f6f-48f9-b9b0-2ad9baec26ba",
"title": "水浒传",
"anchor": "施耐庵",
"pageSize": 300,
"isActive": true
}
]

调用http://localhost:3000/nest/books/findOne/5176c094-7c78-4aaf-ba16-488965583d09接口可得:

1
2
3
4
5
6
7
{
"id": "5176c094-7c78-4aaf-ba16-488965583d09",
"title": "西游记",
"anchor": "吴承恩",
"pageSize": 300,
"isActive": true
}

没有实现分页操作,分页可以通过findBy传入分页条件进行查询

更新数据

在更新操作之前也完善一下update-book.dto.ts

1
2
3
4
5
6
7
8
9
// update-book.dto.ts

import { PartialType } from '@nestjs/mapped-types'
import { CreateBookDto } from './create-book.dto'

export class UpdateBookDto extends PartialType(CreateBookDto) {
id: string
}

books.controller.ts增加接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// books.controller.ts

import { Controller, Get, Post, Body, Param } from '@nestjs/common'
import { BooksService } from './books.service'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'

@Controller('books')
export class BooksController {
constructor(private readonly booksService: BooksService) {}

// ...

+ @Post('update')
+ updateBook(@Body() updateBookDto: UpdateBookDto) {
+ return this.booksService.updateBook(updateBookDto)
+ }
}

books.service.ts中操作数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// books.service.ts

import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'
import { Book } from './entities/book.entity'
@Injectable()
export class BooksService {
constructor(
@InjectRepository(Book)
private booksRepository: Repository<Book>,
) {}

// ...

+ updateBook(updateBookDto: UpdateBookDto) {
+ return this.booksRepository.save(updateBookDto)
+ }
}

我们访问http://localhost:3000/nest/books/update并传入数据:

1
2
3
4
5
6
7
{
"id": "5176c094-7c78-4aaf-ba16-488965583d09",
"title": "道德经",
"anchor": "老子",
"pageSize": 300,
"isActive": true
}

此时我们查询数据库内容:

1
2
3
4
5
6
7
8
9
10
mysql> select * from book;
+--------------------------------------+--------------+-----------+----------+----------+
| id | title | anchor | pageSize | isActive |
+--------------------------------------+--------------+-----------+----------+----------+
| 096dd811-770d-49bb-a160-be11813e6942 | 三国演义 | 罗贯中 | 30 | 1 |
| 5176c094-7c78-4aaf-ba16-488965583d09 | 道德经 | 老子 | 300 | 1 |
| 95a5143d-f521-42ae-88cc-e5b09943db34 | 红楼梦 | 曹雪芹 | 300 | 1 |
| 9617e315-1f6f-48f9-b9b0-2ad9baec26ba | 水浒传 | 施耐庵 | 300 | 1 |
+--------------------------------------+--------------+-----------+----------+----------+
4 rows in set (0.00 sec)

这里我们update的时候传递的是全量数据,所以直接save就可以了。

如果我们update只传递了几个字段,那么就需要通过id找到数据库中的数据,再把对应的字段进行修改,然后将修改后的数据进行save

删除数据

books.controller.ts增加接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// books.controller.ts

import { Controller, Get, Post, Body, Param } from '@nestjs/common'
import { BooksService } from './books.service'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'

@Controller('books')
export class BooksController {
constructor(private readonly booksService: BooksService) {}

// ...

+ @Get('delete/:id')
+ deleteBook(@Param('id') id: string) {
+ return this.booksService.deleteBook(id)
+ }
}

books.service.ts添加逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// books.service.ts

import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'
import { Book } from './entities/book.entity'
@Injectable()
export class BooksService {
constructor(
@InjectRepository(Book)
private booksRepository: Repository<Book>,
) {}

// ...

+ deleteBook(id: string) {
+ return this.booksRepository.delete(id)
+ }
}

我们请求http://localhost:3000/nest/books/delete/5176c094-7c78-4aaf-ba16-488965583d09来删除名为道德经的数据,此时查询数据库:

1
2
3
4
5
6
7
8
9
mysql> select * from book;
+--------------------------------------+--------------+-----------+----------+----------+
| id | title | anchor | pageSize | isActive |
+--------------------------------------+--------------+-----------+----------+----------+
| 096dd811-770d-49bb-a160-be11813e6942 | 三国演义 | 罗贯中 | 30 | 1 |
| 95a5143d-f521-42ae-88cc-e5b09943db34 | 红楼梦 | 曹雪芹 | 300 | 1 |
| 9617e315-1f6f-48f9-b9b0-2ad9baec26ba | 水浒传 | 施耐庵 | 300 | 1 |
+--------------------------------------+--------------+-----------+----------+----------+
3 rows in set (0.00 sec)

这里用的是真删除,实际生产中请考虑需求,一般来说都是做逻辑删除的

在上边的CRUD示例中,展示了几个常用的typeorm操作,其实还有很多api,在使用过程中自己查询文档吧。

字段验证

前边的C和U操作,请求体我们并没有进行校验,这里我们简单提一下,Nestjs的管道操作配合class-validator可以很好的帮我们实现校验功能

安装依赖:

1
npm i --save class-validator class-transformer

我们对create-book.dto.ts中进行字段约束

1
2
3
4
5
6
7
8
9
10
11
12
// create-book.dto.ts
+import { IsNotEmpty, IsString, IsOptional } from 'class-validator'
export class CreateBookDto {
+ @IsString({ message: '标题必须是字符串' })
+ @IsNotEmpty({ message: '标题不能为空' })
title: string
+ @IsOptional()
+ @IsString({ message: '作者名是字符串' })
anchor: string
pageSize: number
isActive: boolean
}

注意我们有一个IsOptional()装饰器,该装饰器可以把这个字段标记为可选的,即anchor===null或者anchor===undefined的话,就会忽略其他的验证规则,如果anchor有值则必须为string类型

我们不能通过anchor?: string这种方式来标记可选字段

然后我们在当前目录下新建一个管道validate.pipe.ts

关于管道的内容这里不展开说,留到后边再讲,这里主要是对dto的字段校验

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
// validate.pipe.ts

import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToInstance } from 'class-transformer'

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value
}
const object = plainToInstance(metatype, value)
// 验证字段,如果有错误会抛出错误,错误内容可自定义,BadRequestException是一个nestjs内置异常
const errors = await validate(object)
if (errors.length > 0) {
// console.log('errors:', errors)
const message = errors[0].constraints
const info = message[Object.keys(message)[0]]
// console.log('message:', info)

throw new BadRequestException(info ? info : 'Validation failed')
}
return value
}

private toValidate(metatype: any): boolean {
const types: any[] = [String, Boolean, Number, Array, Object]
return !types.includes(metatype)
}
}

这里我们是使用官方提供的示例自定义管道,其实Nestjs内置了一个ValidationPipe管道,功能很强大,放到后边管道章节说(验证的内容挺多的,也可能单独拿出来说)

books.controller.ts中引入这个验证管道并应用于新增接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// books.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common'
import { BooksService } from './books.service'
import { CreateBookDto } from './dto/create-book.dto'
import { UpdateBookDto } from './dto/update-book.dto'
+import { ValidationPipe } from './validate.pipe'
@Controller('books')
export class BooksController {
constructor(private readonly booksService: BooksService) {}

@Post('create')
+ createBook(@Body(new ValidationPipe()) createBookDto: CreateBookDto) {
return this.booksService.createBook(createBookDto)
}

// ...
}

此时我们访问新增接口不传递title字段:

1
2
3
4
{
"anchor": "张三",
"pageSize": 300
}

得到的返回结果是:

1
2
3
4
5
{
"statusCode": 400,
"message": "标题不能为空",
"error": "Bad Request"
}

我们也可以传递一个不是字符串类型的title字段进行测试:

1
2
3
4
5
{
"title": 123,
"anchor": "张三",
"pageSize": 300
}

得到的结果是:

1
2
3
4
5
{
"statusCode": 400,
"message": "标题必须是字符串",
"error": "Bad Request"
}

这样一来,我们就有了一个简单的字段必填和字段类型校验。

基本的CRUD功能已经搭建完成了,但Nestjs的能力还要强悍的多,咱们下次再见

作者

胡兆磊

发布于

2023-02-17

更新于

2023-02-20

许可协议