初体验后端,第一次Nest.js开发记录

前言

作为一个老前端,日常工作CRUD页面仔,沉浸在自己的舒适区。首次直面盲区入手Nest.js,痛苦体验,从 0 开始,下载数据库可视化工具,连接数据库… 直到完成开发一个 api 接口。本文记录开发全流程,以此记录,也可作为新手入手 Next.js 开发的一份参考。

需求背景是在参与 Earthworm 开源项目,完成其中一个需求(github.com/cuixueshe/e…),简单来说,需要完成的效果图如下:

初体验后端,第一次Nest.js开发记录

左侧是目前课程列表的UI样式,没有显示当前课程,没有显示学习进度,没有课程完成次数情况。

而右侧则是期待实现的UI效果,通过颜色区分了课程的状态,也显式的显示了课程进度和完成次数情况。

因为本文主要记录的是 Nest.js 的开发过程,对于页面效果上的实现就不赘述了。 Earthworm 项目的仓库地址:github.com/cuixueshe/e… 可以对照代码,食用本文更加。

思路

目标实现的需求是记录每个用户的每节课的完成次数情况。数据库中已经存在了课程表courses,用户表users。新建一张表course-history,用来存每个用户刷过的课程,例如,

  • 刷的次数
  • 刷的平均时长
  • 最快用时
  • 最慢用时
  • 第一次刷的时间
  • 最后一次刷的时间
  • 一天最早刷的时间
  • 一天中最晚刷的时间……

当然这都是后话了。

分析业务流程,想要记录每个用户的每节课的完成次数,就需要从用户确定课程,从课程确定刷的次数,这样表结构就有了。

初体验后端,第一次Nest.js开发记录

表字段为user_idcourse_idcompletion_count,这样就关联了用户表和课程表,completion_count表示课程完成次数。

然后就是在课程结束的时候进行记录。基于userIdcourseId去查course-history表,查询结果分为两种:

  • 发现不存在记录,那么就创建 user_idcourse_idcompletion_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,

初体验后端,第一次Nest.js开发记录

有了连接信息,就可以可视化的看到数据库中具体数据。这里原本并没有course-history表,这张表也是本文需要建立的。

初体验后端,第一次Nest.js开发记录

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 表,其中字段:

  1. id是自增的主键,int类型
  2. user_idint类型,不可为空
  3. course_idint类型,不可为空
  4. completion_countint类型,不可为空,默认值为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” 这些基本概念,用它们于组织和管理应用程序的逻辑和功能。

区别:

  1. Controller(控制器):Controller 是处理传入请求的一部分,它负责路由请求并返回响应。Controller 接收请求并将其传递给适当的服务(service)进行处理。它通常与特定的路由或端点相关联,并处理该路由的逻辑。Controller 用于定义路由和请求处理的行为。
  2. Module(模块):Module 是一组相关的功能和组件的集合,用于封装和组织应用程序的不同部分。它可以包含多个 Controller、Service 和其他提供程序(providers)。Module 提供了一种逻辑上的隔离,它将相关的代码、配置和依赖项组织在一起,使得应用程序更易于管理和维护。
  3. Service(服务):Service 是一个可复用的业务逻辑的集合,它包含了应用程序的核心功能。Service 负责处理具体的业务逻辑,例如数据访问、数据转换、业务规则和其他相关的操作。它通常被 Controller 调用,并可以与数据库、外部服务或其他依赖项进行交互。

关联:

  • Controller 是负责处理传入请求并返回响应的组件,它可以调用 Service 中的方法来处理具体的业务逻辑。
  • Service 是负责实际处理业务逻辑的组件,它可以被 Controller 调用或其他 Service 调用,用于封装和管理核心功能。
  • Module 是一种组织和管理相关功能和组件的方式,它可以包含多个 Controller 和 Service,并提供对外的接口。

开发

了解基本概念,更多的还是参考已有的其他接口实现,照葫芦画瓢,反复查看学习模仿 course,user-progress 这两个接口,同时查看官方文档。

之前分析的思路,具体的实现也就是业务逻辑的实现,是在 service 中开发。再来回顾一下思路,通过 userIdcourseId 去查 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 查询数据库,至于为什么要这么写,我也是查看 courseuser-progress中也是这么写的。select查询所有字段,fromcourseHistory这张表中查询,而courseHistory的来源就是从 shared 中获取。where限制条件,要同时满足 userIdcourseId都相同。

没查询到结果就创建新纪录,

 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)绕过权限认证。

初体验后端,第一次Nest.js开发记录

总结

数据库 dsn 中获取数据库连接信息,可视化工具有很多可以选择的软件。drizzle简化sql编写,表创建使用mysqlTable,其中可以确定表中字段。在表创建之前更重要的工作是确定思路,业务流程从用户到课程再到课程完成次数,基于这样的关联关系,确定表字段需要哪些。Nest.js 中业务逻辑都放在 service,主要实现的逻辑只有两个:

  1. 基于 userIdcourseId 查询 course-history 表,根据查询结果做插入或是更新操作
  2. 查询 course-history 表所有记录,在 controller 中调用对外提供查询接口

其他

第一次基于真实业务写接口,第一次写 Nest.js,第一次写 Drizzle ORM,过程是很痛苦,但是很开心收获也很大。

认识到自己入手新知识存在一种误区,在没接触过 Nest,Drizzle 之前想着了解一番,我习惯性的会去网上查询资料,B站找相关的视频教程,没有耐心去看官方文档。这的确会浪费很多时间,网上的二手资料很多都是过期的甚至带着个人理解存在误区的。相对于官方文档,其他的资料其实都可以称为二手,也包括本文,所以仅限参考,因此还是要习惯适应去官方文档中学习,这也体现了英文的重要性,坚持使用 Earthworm 是个不错的学习英文方式。

参与开源项目比较宝贵的经历是可以接触到工作内容以外的知识面,不同于工作中前后端的严格分离,在开源项目中只要你想,你也可以转变角色成为一名后端开发,产品经理,或者UI设计…但对我来说更宝贵的是认识一群热心的大佬,在经历学习区的痛苦过程中,他们会提供很多帮助,崔哥的code review也是受益匪浅,例如github.com/cuixueshe/e…

原文链接:https://juejin.cn/post/7343139490892628006 作者:wendZzoo

(0)
上一篇 2024年3月8日 下午5:13
下一篇 2024年3月9日 上午10:00

相关推荐

发表回复

登录后才能评论