前言
作为一个老前端,日常工作CRUD页面仔,沉浸在自己的舒适区。首次直面盲区入手Nest.js,痛苦体验,从 0 开始,下载数据库可视化工具,连接数据库… 直到完成开发一个 api 接口。本文记录开发全流程,以此记录,也可作为新手入手 Next.js 开发的一份参考。
需求背景是在参与 Earthworm 开源项目,完成其中一个需求(github.com/cuixueshe/e…),简单来说,需要完成的效果图如下:
左侧是目前课程列表的UI样式,没有显示当前课程,没有显示学习进度,没有课程完成次数情况。
而右侧则是期待实现的UI效果,通过颜色区分了课程的状态,也显式的显示了课程进度和完成次数情况。
因为本文主要记录的是 Nest.js 的开发过程,对于页面效果上的实现就不赘述了。 Earthworm 项目的仓库地址:github.com/cuixueshe/e… 可以对照代码,食用本文更加。
思路
目标实现的需求是记录每个用户的每节课的完成次数情况。数据库中已经存在了课程表courses
,用户表users
。新建一张表course-history
,用来存每个用户刷过的课程,例如,
- 刷的次数
- 刷的平均时长
- 最快用时
- 最慢用时
- 第一次刷的时间
- 最后一次刷的时间
- 一天最早刷的时间
- 一天中最晚刷的时间……
当然这都是后话了。
分析业务流程,想要记录每个用户的每节课的完成次数,就需要从用户确定课程,从课程确定刷的次数,这样表结构就有了。
表字段为user_id
,course_id
,completion_count
,这样就关联了用户表和课程表,completion_count
表示课程完成次数。
然后就是在课程结束的时候进行记录。基于userId
和courseId
去查course-history
表,查询结果分为两种:
- 发现不存在记录,那么就创建
user_id
,course_id
,completion_count = 1
- 发现有值,那么就更新
completion_count
的值,即在原有completion_count
值的基础上+1
数据库
Earthworm 项目采用的是 Mysql 数据库,使用 Docker 安装启动。
数据库可视化工具,我用的是 Navicat。数据库连接信息,也就是数据库DSN,在项目 .env 文件中获取。
据库 DSN(数据源名称)是一个用于标识和定位数据库连接的字符串。它包含了连接数据库所需的信息,如数据库类型、主机名、端口号、数据库名称以及认证凭据等。
“DSN” 通常用于描述数据库连接配置的字符串格式,用于在应用程序中指定如何连接到数据库。不同的数据库系统和编程语言可能会有不同的 DSN 格式。
查看项目根目录下 .env 文件
DATABASE_URL="mysql://root:password@127.0.0.1:3306/earthworm_nest"
看不懂的信息丢给 gtp,
有了连接信息,就可以可视化的看到数据库中具体数据。这里原本并没有course-history
表,这张表也是本文需要建立的。
Drizzle ORM
项目中使用了 Drizzle ORM 来简化对数据库的操作。
libs/shared 下 schema 中是对数据库里每张表的构建,使用 drizzle 避免了我们再用原始的 sql 语句进行表创建。
新建 courseHistory.ts 代码如下:
import { mysqlTable, int } from "drizzle-orm/mysql-core";
export const courseHistory = mysqlTable("course-history", {
id: int("id").autoincrement().primaryKey(),
userId: int("user_id").notNull(),
courseId: int("course_id").notNull(),
completionCount: int("completion_count").notNull().default(0),
});
以上代码,通过mysqlTable
创建了一张 mysql 表,其中字段:
id
是自增的主键,int
类型user_id
,int
类型,不可为空course_id
,int
类型,不可为空completion_count
,int
类型,不可为空,默认值为0
编写完表创建的语句,就需要去执行在数据库中真实创建。查看更目录下 package.json 脚本命令,其中schema:build
是执行的 shared 打包命令,db:init
执行数据库初始化操作。
这里可以依次执行命令
pnpm schema:build
pnpm db:init
在 Navicat 中查看,已经新增了一张表course-history
,说明已经创建成功。
开发接口
apps/api 下编写接口代码,src下新建 course-history,其中分别创建 course-history.controller.ts,course-history.module.ts 和 course-history.service.ts。
也可以通过 nest/cli 脚手架快速创建,nest --help
查询创建命令。但是我还是推荐手动创建,比较作为新手,亲历亲为记忆更深刻。
基本概念
不仅仅是 nest.js,很多后端语言中多存在”controller”、”module” 和 “service” 这些基本概念,用它们于组织和管理应用程序的逻辑和功能。
区别:
- Controller(控制器):Controller 是处理传入请求的一部分,它负责路由请求并返回响应。Controller 接收请求并将其传递给适当的服务(service)进行处理。它通常与特定的路由或端点相关联,并处理该路由的逻辑。Controller 用于定义路由和请求处理的行为。
- Module(模块):Module 是一组相关的功能和组件的集合,用于封装和组织应用程序的不同部分。它可以包含多个 Controller、Service 和其他提供程序(providers)。Module 提供了一种逻辑上的隔离,它将相关的代码、配置和依赖项组织在一起,使得应用程序更易于管理和维护。
- Service(服务):Service 是一个可复用的业务逻辑的集合,它包含了应用程序的核心功能。Service 负责处理具体的业务逻辑,例如数据访问、数据转换、业务规则和其他相关的操作。它通常被 Controller 调用,并可以与数据库、外部服务或其他依赖项进行交互。
关联:
- Controller 是负责处理传入请求并返回响应的组件,它可以调用 Service 中的方法来处理具体的业务逻辑。
- Service 是负责实际处理业务逻辑的组件,它可以被 Controller 调用或其他 Service 调用,用于封装和管理核心功能。
- Module 是一种组织和管理相关功能和组件的方式,它可以包含多个 Controller 和 Service,并提供对外的接口。
开发
了解基本概念,更多的还是参考已有的其他接口实现,照葫芦画瓢,反复查看学习模仿 course,user-progress 这两个接口,同时查看官方文档。
之前分析的思路,具体的实现也就是业务逻辑的实现,是在 service 中开发。再来回顾一下思路,通过 userId
和 courseId
去查 course-history
表,查询的结果分两种,查到和没查到。查到了就更新,没查到就创建。
主要就是一个查询方法,find
方法用于在课程结束的时候执行。
async find(userId: number, courseId: number) {
const result = await this.db
.select()
.from(courseHistory)
.where(
and(
eq(courseHistory.userId, userId),
eq(courseHistory.courseId, courseId),
),
);
if (result && result.length) {
// find it
} else {
// not find
}
}
以上代码,通过 db
查询数据库,至于为什么要这么写,我也是查看 course
,user-progress
中也是这么写的。select
查询所有字段,from
从courseHistory
这张表中查询,而courseHistory
的来源就是从 shared 中获取。where
限制条件,要同时满足 userId
和 courseId
都相同。
没查询到结果就创建新纪录,
async create(userId: number, courseId: number) {
await this.db.insert(courseHistory).values({
courseId,
userId,
completionCount: 1,
});
}
查询到结果,就更新目标记录中 completionCount
加 1
async update(userId: number, courseId: number, count: number) {
await this.db
.update(courseHistory)
.set({
completionCount: count + 1,
})
.where(
and(
eq(courseHistory.userId, userId),
eq(courseHistory.courseId, courseId),
),
);
}
查询方法find()
就开发完成了,但是 service 中还需要一个查询 course-history
表所有数据的方法,这个方法以供 controller 中使用,即在 controller 中提供一个 get 方法的路由接口。
给出完整的 service 代码:
import { Injectable, Inject } from '@nestjs/common';
import { DB, DbType } from '../global/providers/db.provider';
import { courseHistory } from '@earthworm/shared';
import { eq, and } from 'drizzle-orm';
@Injectable()
export class CourseHistoryService {
constructor(@Inject(DB) private db: DbType) {}
async create(userId: number, courseId: number) {
await this.db.insert(courseHistory).values({
courseId,
userId,
completionCount: 1,
});
}
async update(userId: number, courseId: number, count: number) {
await this.db
.update(courseHistory)
.set({
completionCount: count + 1,
})
.where(
and(
eq(courseHistory.userId, userId),
eq(courseHistory.courseId, courseId),
),
);
}
async find(userId: number, courseId: number) {
const result = await this.db
.select()
.from(courseHistory)
.where(
and(
eq(courseHistory.userId, userId),
eq(courseHistory.courseId, courseId),
),
);
if (result && result.length) {
// find it
this.update(userId, courseId, result[0].completionCount);
} else {
// not find
this.create(userId, courseId);
}
}
async findCompletionCount() {
return await this.db
.select({
courseId: courseHistory.courseId,
completionCount: courseHistory.completionCount,
})
.from(courseHistory);
}
}
controller 代码:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { CourseHistoryService } from './course-history.service';
import { AuthGuard } from '../auth/auth.guard';
@Controller('course-history')
export class CourseHistoryController {
constructor(private readonly courseHistoryService: CourseHistoryService) {}
@UseGuards(AuthGuard)
@Get('count')
courseCompletionCount() {
return this.courseHistoryService.findCompletionCount();
}
}
测试
在 apifox 验证一下接口 /course-history/count,需要注释掉@UseGuards(AuthGuard)
绕过权限认证。
总结
数据库 dsn 中获取数据库连接信息,可视化工具有很多可以选择的软件。drizzle简化sql
编写,表创建使用mysqlTable
,其中可以确定表中字段。在表创建之前更重要的工作是确定思路,业务流程从用户到课程再到课程完成次数,基于这样的关联关系,确定表字段需要哪些。Nest.js 中业务逻辑都放在 service,主要实现的逻辑只有两个:
- 基于
userId
和courseId
查询 course-history 表,根据查询结果做插入或是更新操作 - 查询 course-history 表所有记录,在 controller 中调用对外提供查询接口
其他
第一次基于真实业务写接口,第一次写 Nest.js,第一次写 Drizzle ORM,过程是很痛苦,但是很开心收获也很大。
认识到自己入手新知识存在一种误区,在没接触过 Nest,Drizzle 之前想着了解一番,我习惯性的会去网上查询资料,B站找相关的视频教程,没有耐心去看官方文档。这的确会浪费很多时间,网上的二手资料很多都是过期的甚至带着个人理解存在误区的。相对于官方文档,其他的资料其实都可以称为二手,也包括本文,所以仅限参考,因此还是要习惯适应去官方文档中学习,这也体现了英文的重要性,坚持使用 Earthworm 是个不错的学习英文方式。
参与开源项目比较宝贵的经历是可以接触到工作内容以外的知识面,不同于工作中前后端的严格分离,在开源项目中只要你想,你也可以转变角色成为一名后端开发,产品经理,或者UI设计…但对我来说更宝贵的是认识一群热心的大佬,在经历学习区的痛苦过程中,他们会提供很多帮助,崔哥的code review也是受益匪浅,例如github.com/cuixueshe/e…
原文链接:https://juejin.cn/post/7343139490892628006 作者:wendZzoo