NestJS最佳实践--#2整合Typeorm实现基本的CRUD操作及分页数据查询
学习目标
- 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
- 安装Docker Compose
以下是使用Docker Compose来安装MySQL的步骤:
-
创建一个名为
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中)。 -
运行以下命令来启动Docker容器:
docker-compose up -d
这个命令会在后台启动MySQL容器。如果一切正常,你应该能够通过
http://localhost:8080
访问phpmyadmin
配置模块—yaml多环境配置
一般在项目开发中,至少会经历 Dev
-> Test
-> Prod
三个环境。如果再富余一点的话,还会再多一个 Pre
环境。那么每个环境使用的数据库、Redis
或者其他的配置项都会随着环境的变换而改变,所以在实际项目开发中,多环境的配置非常必要。
NestJS
默认使用dotenv
读取.env
文件的形式来读取配置,但我们这里要介绍的是另外一种更简洁明了的配置方式—yaml
-
安装依赖
pnpm add @nestjs/config yaml cross-env
-
在根目录下创建
.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
-
新建
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; };
-
在
app.module.ts
中导入ConfigModule
, 后面可以通过ConfigService
读取yaml中的配置@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // 全局模块 ignoreEnvFile: true, // 忽略.env相关配置文件 load: [getConfig], // 读取自定义文件 }), ], }) export class AppModule {}
-
使用
cross-env
在package.json
的scripts
中指定运行环境{ "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", }
数据库模块
-
新建
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 }; }, }), ], }; } }
-
在
app.module.ts
中导入DataBaseModule
@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // 全局模块 ignoreEnvFile: true, // 忽略.env相关配置文件 load: [getConfig], // 读取自定义文件 }), DataBaseModule.forRoot(), ], }) export class AppModule {}
-
执行
pnpm start:dev
启动项目,如果能看到下图所示输出,就证明项目已经能够跑起来了。
业务模块
创建完数据库模块之后,就可以开始业务代码的编写了。
-
创建
src/modules/business
目录,此目录下存放业务模块(用户模块,文章模块,分类模块…) -
借助 Nest CLI 命令
nest g res post
快速生成文章模块的模板代码 -
将
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 等。
为了使用它,我们要做些设置
- 注册Post实体类
@Module({
imports: [TypeOrmModule.forFeature([Post])],
controllers: [PostController],
providers: [PostService],
})
export class PostModule {}
- 在
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相关的路由信息:
现在,我们就可以使用/post
接口来进行用户的增删改查操作了。例如,使用POST /post
接口来创建文章
相应的数据库中post
表也会多出一行记录
分页查询
-
安装依赖库
pnpm install class-transformer class-validator
-
创建
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; }
-
创建
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, }; };
-
在
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和Typeorm来实现基本的CRUD操作和分页查询。如果你有任何问题或建议,请在下面的评论中留言,感谢阅读!
转载自:https://juejin.cn/post/7210918245992955941