Nestjs 最佳实践教程:2 基本数据操作

2022-07-10 03:41:49 +08:00
 lichnow

学习目标

安装 Mysql

实际生产环境中建议使用 PostgreSQL,因为教程以学习为主,所以直接使用相对来说比较通用和简单的 Mysql

使用以下命令安装 Mysql

如果本机不是使用 linux(比如使用 wsl2),请到mysql官网点击 download 按钮下载安装包后在 chrome 查看下载地址,然后在开发机用wget下载

如果本机使用 MacOS,使用brew install mysql,如果本机使用 Arch 系列,使用sudo pacman -Syy mysql

# 下载镜像包
cd /usr/local/src
sudo wget sudo wget https://repo.mysql.com/mysql-apt-config_0.8.22-1_all.deb
# 添加镜像(其它选项不用管,直接 OK 就可以)
sudo apt-get install ./mysql-apt-config_0.8.22-1_all.deb
# 升级包列表
sudo apt-get update
# 开始安装,输入密码后,有一个密码验证方式,因为是开发用,所以选择第二个弱验证即可
sudo apt-get install mysql-server 
# 初始化,在是否加载验证组件时选择 No,在是否禁用远程登录时也选择 No
sudo mysql_secure_installation
# 因为是远程 SSH 连接开发所以需要开启远程数据库链接,如果是本地或者 wsl2 则不需要开启
mysql -u root -p 
CREATE USER 'root'@'%' IDENTIFIED BY '密码';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;

接着使用 Navicat 等客户端就可以连接了

预装依赖

~ pnpm add class-transformer \
  @nestjs/platform-fastify \
  class-validator \
  lodash \
  @nestjs/swagger \
  fastify-swagger \
  mysql2 \
  typeorm \
  @nestjs/typeorm

 ~ pnpm add @types/lodash cross-env @types/node typescript -D

生命周期

要合理的编写应用必须事先了解清楚整个程序的访问流程,本教程会讲解如何一步步演进每一次访问流,作为第一步课时,我们的访问流非常简单,可以参考下图

文件结构

我们通过整合typeorm来连接 mysql 实现一个基本的 CRUD 应用,首先我们需要创建一下文件结构

建议初学者手动创建,没必要使用 CLI 去创建,这样目录和文件更加清晰

  1. 创建模块
  2. 编写模型
  3. 编写 Repository(如果有需要的话)
  4. 编写数据验证的 DTO
  5. 编写服务
  6. 编写控制器
  7. 在每个以上代码各自的目录下建立一个index.ts并导出它们
  8. 在各自的Module里进行注册提供者,导出等
  9. AppModule中导入这两个模块

编写好之后的目录结构如下

.
├── app.module.ts                           # 引导模块           
├── config                                  # 配置文件目录
│   ├── database.config.ts                  # 数据库配置
│   └── index.ts
├── main.ts                                 # 应用启动器
├── modules
    ├── content                             # 内容模块目录
    │   ├── content.module.ts               # 内容模块
    │   ├── controllers                     # 控制器
    │   ├── dtos                            # DTO 访问数据验证
    │   ├── entities                        # 数据实体模型
    |   ├── index.ts              
    │   ├── repositories                    # 自定义 Repository
    │   ├── services                        # 服务
    └──  core
        ├── constants.ts                    # 常量
        ├── core.module.ts                  # 核心模块
        ├── decorators                      # 装饰器
        └── types.ts                        # 公共类型

应用编码

在开始编码之前需要先更改一下package.jsonnestjs-cli.json两个文件

package.json中修改一下启动命令,以便每次启动可以自动配置运行环境并兼容windows环境

"prebuild": "cross-env rimraf dist",
"start": "cross-env NODE_ENV=development nest start",
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",

为了在每次重新编译前自动删除上次的产出,在nestjs-cli.json中配置 "deleteOutDir": true

main.ts

把适配器由express换成更快的fastify,并把监听的 IP 改成0.0.0.0方便外部访问.为了在使用class-validatorDTO类中也可以注入 nestjs 容器的依赖,需要添加useContainer

// main.ts
import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { useContainer } from 'class-validator';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter()
  );
  useContainer(app.select(AppModule), { fallbackOnErrors: true });
  await app.listen(3000,'0.0.0.0');
}
bootstrap();

连接配置

创建一个src/config/database.config.ts文件

export const database: () => TypeOrmModuleOptions = () => ({
    // ...
    // 此处 entites 设置为空即可,我们直接通过在模块内部使用`forFeature`来注册模型
    // 后续魔改框架的时候,我们会通过自定义的模块创建函数来重置 entities,以便给自己编写的 CLI 使用
    // 所以这个配置后面会删除
    entities: [], 
    // 自动加载模块中注册的 entity
    autoLoadEntities: true,
    // 可以在开发环境下同步 entity 的数据结构到数据库
    // 后面教程会使用自定义的迁移命令来代替,以便在生产环境中使用,所以以后这个选项会永久 false
    synchronize: process.env.NODE_ENV !== 'production',
});

CoreModule

核心模块用于挂载一些全局类服务,比如整合typeorm的``TypeormModule`

注意: 这里不要使用@Global()装饰器来构建全局模块,因为后面在CoreModule类中添加一些其它方法

返回值中添加global: true来注册全局模块,并导出metadata.

// src/core/core.module.ts
export class CoreModule {
    public static forRoot(options?: TypeOrmModuleOptions) {
        const imports: ModuleMetadata['imports'] = [TypeOrmModule.forRoot(options)];
        return {
            global: true,
            imports,
            module: CoreModule,
        };
    }
}

AppModule导入该模块,并注册数据库连接

// src/app.module.ts
@Module({
    imports: [CoreModule.forRoot(database())],
  ...
})
export class AppModule {}

自定义存储类

由于原来用于自定义 Repository 的@EntityRepositorytypeorm0.3 版本后已经不可用,特别不方便,所以根据这里的示例来自定义一个CustomRepository装饰器

// src/modules/core/constants.ts
// 传入装饰器的 metadata 数据标识
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';

// src/modules/core/decorators/repository.decorator.ts
// 定义装饰器
import { CUSTOM_REPOSITORY_METADATA } from '../constants';
export const CustomRepository = <T>(entity: ObjectType<T>): ClassDecorator =>
    SetMetadata(CUSTOM_REPOSITORY_METADATA, entity);

// src/modules/core/decorators/index.ts
export * from './repository.decorator';

定义静态方法用于注册自定义 Repository

 public static forRepository<T extends Type<any>>(
        repositories: T[],
        dataSourceName?: string,
    ): DynamicModule {
        const providers: Provider[] = [];

        for (const Repo of repositories) {
            const entity = Reflect.getMetadata(CUSTOM_REPOSITORY_METADATA, Repo);

            if (!entity) {
                continue;
            }

            providers.push({
                inject: [getDataSourceToken(dataSourceName)],
                provide: Repo,
                useFactory: (dataSource: DataSource): typeof Repo => {
                    const base = dataSource.getRepository<ObjectType<any>>(entity);
                    return new Repo(base.target, base.manager, base.queryRunner);
                },
            });
        }

        return {
            exports: providers,
            module: CoreModule,
            providers,
        };
    }

ContentModule

内容模块用于存放CRUD操作的逻辑代码

// src/modules/content/content.module.ts
@Module({})
export class ContentModule {}

AppModule中注册

// src/app.module.ts
@Module({
    imports: [CoreModule.forRoot(database()),ContentModule],
  ...
})
export class AppModule {}

实体模型

创建一个PostEntity用于文章数据表

PostEntity继承``BaseEntity,这样做是为了我们可以进行ActiveRecord操作,例如PostEntity.save(post),因为纯DataMapper`的方式有时候代码会显得啰嗦,具体请查看此处

@CreateDateColumn @UpdateDateColumn是自动字段,会根据创建和更新数据的时间自动产生,写入后不必关注

// src/modules/content/entities/post.entity.ts
// 'content_posts'是表名称
@Entity('content_posts')
export class PostEntity extends BaseEntity {
...
    @CreateDateColumn({
        comment: '创建时间',
    })
    createdAt!: Date;

    @UpdateDateColumn({
        comment: '更新时间',
    })
    updatedAt!: Date;
}

存储类

本节存储类是一个空类,后面会添加各种操作方法

这里用到我们前面定义的自定义 CustomRepository 装饰器

// src/modules/content/repositories/post.repository.ts
@CustomRepository(PostEntity)
export class PostRepository extends Repository<PostEntity> {}

注册模型和存储类

在编写好entityrepository之后我们还需要通过Typeorm.forFeature这个静态方法进行注册,并把存储类导出为提供者以便在其它模块注入

// src/modules/content/content.module.ts
@Module({
    imports: [
        TypeOrmModule.forFeature([PostEntity]),
        // 注册自定义 Repository
        CoreModule.forRepository([PostRepository]),
    ],
     exports: [
        // 导出自定义 Repository,以供其它模块使用
        CoreModule.forRepository([PostRepository]),
    ],
})
export class ContentModule {}

DTO 验证

DTO配合管道(PIPE)用于控制器的数据验证,验证器则使用class-validator

class-validator 是基于 validator.js 的封装,所以一些规则可以通过 validator.js 的文档查找,后面教程中我们会编写大量的自定义的验证规则,这节先尝试基本的用法

其基本的使用方法就是给DTO类的属性添加一个验证装饰器,如下

groups选项用于配置验证组

// src/modules/content/dtos/create-post.dto.ts
@Injectable()
export class CreatePostDto {
    @MaxLength(255, {
        always: true,
        message: '文章标题长度最大为$constraint1',
    })
    @IsNotEmpty({ groups: ['create'], message: '文章标题必须填写' })
    @IsOptional({ groups: ['update'] })
    title!: string;
    ...
}

更新验证类UpdatePostDto继承自CreatePostDto,为了使CreatePostDto中的属性变成可选,需要使用[@nestjs/swagger][]包中的PartialType方法,请查阅此处文档

// src/modules/content/dtos/update-post.dto.ts
@Injectable()
export class UpdatePostDto extends PartialType(CreatePostDto) {
    @IsUUID(undefined, { groups: ['update'], message: '文章 ID 格式错误' })
    @IsDefined({ groups: ['update'], message: '文章 ID 必须指定' })
    id!: string;
}

服务类

服务一共包括 5 个简单的方法,通过调用PostRepository来操作数据

// src/modules/content/services/post.service.ts
@Injectable()
export class PostService {
    // 此处需要注入`PostRepository`的依赖
    constructor(private postRepository: PostRepository) {}
    // 查询文章列表
    async findList() 
    // 查询一篇文章的详细信息
    async findOne(id: string)
    // 添加文章
    async create(data: CreatePostDto)
    // 更新文章
    async update(data: UpdatePostDto)
    // 删除文章
    async delete(id: string)
}

控制器

控制器的方法通过@GET,@POST,@PUT,@PATCH,@Delete等装饰器对外提供接口,并且通过注入PostService服务来操作数据.在控制器的方法上使用框架自带的ValidationPipe管道来验证请求中的body数据,ParseUUIDPipe来验证params数据

// 控制器 URL 的前缀
@Controller('posts')
export class PostController {
    constructor(protected postService: PostService) {}

    ...
   // 其它方法请自行查看源码
    @Get(':post')
    async show(@Param('post', new ParseUUIDPipe()) post: string) {
        return this.postService.findOne(post);
    }

    @Post()
    async store(
        @Body(
            new ValidationPipe({
                transform: true,
                forbidUnknownValues: true,
                // 不在错误中暴露 target
                validationError: { target: false },
                groups: ['create'],
            }),
        )
        data: CreatePostDto,
    ) {
        return this.postService.create(data);
    }
}

注册控制器等

// src/modules/content/content.module.ts
@Module({
    imports: [
        TypeOrmModule.forFeature([PostEntity]),
        // 注册自定义 Repository
        CoreModule.forRepository([PostRepository]),
    ],
    providers: [PostService, CreatePostDto, UpdatePostDto],
    controllers: [PostController],
    exports: [
        PostService,
        // 导出自定义 Repository,以供其它模块使用
        CoreModule.forRepository([PostRepository]),
    ],
})
export class ContentModule {}
// src/main.ts
...
async function bootstrap() {
    const app = await NestFactory.create<NestFastifyApplication>(
        AppModule,
        new FastifyAdapter(),
    );
    useContainer(app.select(AppModule), { fallbackOnErrors: true });
    await app.listen(3000, '0.0.0.0');
}

最后启动应用在Thunder Client中测试接口

3088 次点击
所在节点    Node.js
1 条回复
pincmancc
2023-01-06 08:59:47 +08:00
test

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/865195

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX