2020 年 7 月 30 日

使用 TypeScript PostgreSQL 和 Prisma 的后端:数据建模和 CRUD

本文是关于使用 TypeScript、PostgreSQL 和 Prisma 构建后端的直播和文章系列的一部分。在本文中,我们将总结第一次直播,并了解如何设计数据模型、执行 CRUD 操作以及使用 Prisma 查询聚合。

Backend with TypeScript PostgreSQL & Prisma: Data Modeling & CRUD

简介

本系列的目标是通过解决一个具体问题来探索和演示现代后端的不同模式、问题和架构:在线课程的评分系统。 这是一个很好的例子,因为它具有多样的关系类型,并且足够复杂,可以代表真实的用例。

上面的直播录像可用,内容与本文相同。

本系列将涵盖的内容

本系列将重点关注数据库在后端开发的各个方面的作用,包括

  • 数据建模
  • CRUD
  • 聚合
  • API 层
  • 验证
  • 测试
  • 身份验证
  • 授权
  • 与外部 API 集成
  • 部署

今天您将学到的内容

本系列的第一篇文章将首先阐述问题域,并开发后端的以下方面

  1. 数据建模: 将问题域映射到数据库模式
  2. CRUD: 使用 Prisma Client 对数据库执行创建、读取、更新和删除查询
  3. 聚合: 使用 Prisma 实现聚合查询以计算平均值等。

到本文结束时,您将拥有一个 Prisma 模式、一个由 Prisma Migrate 创建的相应数据库模式,以及一个使用 Prisma Client 执行 CRUD 和聚合查询的种子脚本。

本系列的后续部分将详细介绍列表中其他方面。

注意: 在整个指南中,您会发现各种检查点,使您能够验证是否正确执行了步骤。

先决条件

假设的知识

本系列假设您具备 TypeScript、Node.js 和关系数据库的基本知识。如果您有 JavaScript 经验但没有机会尝试 TypeScript,您仍然应该能够跟上。本系列将使用 PostgreSQL,但是,大多数概念适用于其他关系数据库,例如 MySQL。除此之外,无需任何 Prisma 先验知识,因为本系列将涵盖这些内容。

开发环境

您应该安装以下内容

如果您使用的是 Visual Studio Code,建议使用 Prisma 扩展,以获得语法高亮、格式化和其他帮助。

注意:如果您不想使用 Docker,您可以设置一个 本地 PostgreSQL 数据库 或一个 Heroku 上托管的 PostgreSQL 数据库

克隆存储库

本系列的源代码可以在 GitHub 上找到。

要开始使用,请克隆存储库并安装依赖项

注意: 通过检出 part-1 分支,您将能够从相同的起点开始阅读本文。

启动 PostgreSQL

要启动 PostgreSQL,请从 real-world-grading-app 文件夹运行以下命令

注意: Docker 将使用 docker-compose.yml 文件来启动 PostgreSQL 容器。

在线课程评分系统的数据模型

定义问题域和实体

在构建后端时,首要关注的问题之一是正确理解问题域。问题域(或问题空间)是指定义问题并约束解决方案的所有信息(约束是问题的一部分)。通过理解问题域,数据模型的形状和结构应该变得清晰。

在线评分系统将具有以下实体

  • 用户: 拥有帐户的人。用户可以通过与课程的关系成为教师或学生。换句话说,同一用户可以是一门课程的教师,也可以是另一门课程的学生。
  • 课程: 一门学习课程,拥有一名或多名教师和学生,以及一项或多项测试。例如:“TypeScript 入门”课程可以有两名教师和十名学生。
  • 测试: 一门课程可以有多次测试来评估学生的理解程度。测试有日期,并且与课程相关联。
  • 测试结果: 每次测试可以有每个学生的多个测试结果记录。此外,TestResult 还与评分测试的教师相关联。

注意:实体代表物理对象或无形概念。例如,用户代表一个人,而课程是一个无形的概念。

这些实体可以可视化,以演示它们在关系数据库(在本例中为 PostgreSQL)中的表示方式。下面的图表添加了每个实体的相关列和外键,以描述实体之间的关系。

关于图表,首先要注意的是每个实体都映射到一个数据库表。

该图表具有以下关系

  • 一对多(也称为 1-n:
    • TestTestResult
    • CourseTest
    • UserTestResult(通过 graderId
    • UserTestResult(通过 student
  • 多对多(也称为 m-n
    • UserCourse(通过 CourseEnrollment 关系表,其中包含两个外键userIdcourseId)。多对多关系通常需要一个额外的表。这是必要的,以便评分系统可以具有以下属性
      • 单个课程可以有许多关联的用户(作为学生或教师)
      • 单个用户可以与多个课程关联。

注意:关系表(也称为 JOIN 表)连接两个或多个其他表,以在它们之间创建关系。创建关系表是 SQL 中表示不同实体之间关系的常见数据建模实践。本质上,这意味着“一个 m-n 关系在数据库中被建模为两个 1-n 关系”。

了解 Prisma 模式

要在数据库中创建表,您首先需要定义您的 Prisma 模式。Prisma 模式是数据库表的声明性配置,Prisma Migrate 将使用它在数据库中创建表。与上面的实体图类似,它定义了数据库表之间的列和关系。

Prisma 模式用作生成的 Prisma Client 和 Prisma Migrate 的真理来源,以创建数据库模式。

该项目的 Prisma 模式可以在 prisma/schema.prisma 中找到。在模式中,您将找到存根模型,您将在本步骤中定义这些模型,以及一个 datasource 块。datasource 块定义了您将连接的数据库类型和连接字符串。通过 env("DATABASE_URL"),Prisma 将从环境变量加载数据库连接 URL。

注意: 将密钥保存在代码库之外被认为是最佳实践。因此,env("DATABASE_URL")datasource 块中定义。通过设置环境变量,您可以将密钥保存在代码库之外。

定义模型

Prisma 模式的基本构建块是 model。每个模型都映射到一个数据库表。

这是一个显示模型基本签名的示例

在这里,您定义了一个 User 模型,其中包含多个 字段。每个字段都有一个名称,后跟一个类型和可选的字段属性。例如,id 字段可以分解如下

名称类型标量与关系类型修饰符属性
idInt标量-@id(表示主键)和 @default(autoincrement())(设置默认自增值)
emailString标量-@unique
firstNameString标量--
lastNameString标量--
socialJson标量?可选-

Prisma 定义了一组数据类型,这些数据类型根据使用的数据库映射到本机数据库类型。

Json 数据类型允许存储自由格式的 JSON。这对于在 User 记录中可能不一致并且可以在不影响后端核心功能的情况下更改的信息很有用。在上面的 User 模型中,它将用于存储社交链接,例如 Twitter、LinkedIn 等。向 social 添加新的社交个人资料链接不需要数据库迁移。

通过对问题域和使用 Prisma 建模数据的良好理解,您现在可以将以下模型添加到您的 prisma/schema.prisma 文件中

每个模型都具有所有相关字段,同时忽略关系(这将在下一步中定义)。

定义关系

一对多

在此步骤中,您将定义 TestTestResult 之间的一对多关系。

首先,考虑上一步中定义的 TestTestResult 模型

要定义两个模型之间的一对多关系,请添加以下三个字段

  • testId 字段,类型为 Int关系标量),位于关系的“多”侧:TestResult。此字段表示底层数据库表中的外键
  • test 字段,类型为 Test关系字段),带有 @relation 属性,将关系标量 testId 映射到 Test 模型的 id 主键。
  • testResults 字段,类型为 TestResult[]关系字段

关系字段(如 testtestResults)可以通过其值类型指向另一个模型来识别,例如 TestTestResult。它们的名称将影响使用 Prisma Client 以编程方式访问关系的方式,但是,它们不代表真实的数据库列。

多对多关系

在此步骤中,您将定义 UserCourse 模型之间的多对多关系。

多对多关系在 Prisma 模式中可以是隐式显式的。在本部分中,您将学习两者之间的区别以及何时选择隐式或显式。

首先,考虑上一步中定义的 UserCourse 模型

要创建隐式多对多关系,请在关系的双方都定义关系字段作为列表

这样,Prisma 将创建关系表,以便评分系统可以维护上面定义的属性

  • 单个课程可以有许多关联的用户。
  • 单个用户可以与多个课程关联。

但是,评分系统的要求之一是允许将用户与课程关联,并具有教师学生的角色。这意味着我们需要一种在数据库中存储有关关系的“元信息”的方法。

这可以使用显式多对多关系来实现。连接 UserCourse 的关系表需要一个额外的字段来指示用户是课程的教师还是学生。通过显式多对多关系,您可以在关系表上定义额外的字段。

为此,为名为 CourseEnrollment 的关系表定义一个新模型,并更新 User 模型中的 courses 字段和 Course 模型中的 members 字段,使其类型为 CourseEnrollment[],如下所示

关于 CourseEnrollment 模型的注意事项

  • 它使用 UserRole 枚举来表示用户是课程的学生还是教师。
  • @@id[userId, courseId] 定义了两个字段的多字段主键。这将确保每个 User 只能与一个 Course 关联一次,要么作为学生,要么作为教师,但不能两者兼有。

要了解有关关系的更多信息,请查看关系文档

完整模式

现在您已经了解了如何定义关系,请使用以下内容更新 Prisma 模式

请注意,TestResultUser 模型有两个关系:studentgradedBy,以表示评分测试的教师和参加测试的学生。@relation 属性上的 name 参数对于消除关系歧义是必要的,当单个模型与同一模型有多个关系时。

迁移数据库

定义 Prisma 模式后,您现在将使用 Prisma Migrate 在数据库中创建实际的表。

首先,在本地设置 DATABASE_URL 环境变量,以便 Prisma 可以连接到您的数据库。

注意:本地数据库的用户名和密码在 docker-compose.yml 中都定义为 prisma

要使用 Prisma Migrate 创建和运行迁移,请在您的终端中运行以下命令

该命令将执行两件事

  • 保存迁移: Prisma Migrate 将拍摄您的模式快照,并计算出执行迁移所需的 SQL。包含 SQL 的迁移文件将保存到 prisma/migrations
  • 运行迁移: Prisma Migrate 将执行迁移文件中的 SQL 以运行迁移并更改(或创建)数据库模式

注意: Prisma Migrate 目前处于 预览 模式。这意味着不建议在生产环境中使用 Prisma Migrate。

检查点: 您应该在输出中看到类似以下内容

恭喜您,您已成功设计了数据模型并创建了数据库模式。在下一步中,您将使用 Prisma Client 对数据库执行 CRUD 和聚合查询。

生成 Prisma Client

Prisma Client 是一个自动生成的数据库客户端,它根据您的数据库模式量身定制。它的工作原理是解析 Prisma 模式并生成一个 TypeScript 客户端,您可以在代码中导入它。

生成 Prisma Client 通常需要三个步骤

  1. 将以下 generator 定义添加到您的 Prisma 模式

  2. 安装 @prisma/client npm 包

  3. 使用以下命令生成 Prisma Client

检查点: 您应该在输出中看到以下内容:✔ Generated Prisma Client to ./node_modules/@prisma/client in 57ms

种子数据库

在此步骤中,您将使用 Prisma Client 编写种子脚本,以使用一些示例数据填充数据库。

在这种上下文中,种子脚本是一堆使用 Prisma Client 的 CRUD 操作(创建、读取、更新和删除)。您还将使用嵌套写入在单个操作中为相关实体创建数据库行。

打开骨架 src/seed.ts 文件,您将在其中找到导入的 Prisma Client 和两个 Prisma Client 函数调用:一个用于实例化 Prisma Client,另一个用于在脚本完成运行时断开连接。

创建用户

首先,在 main 函数中按如下方式创建一个用户

该操作将在 User 表中创建一行,并返回创建的用户(包括创建的 id)。值得注意的是,user 将推断类型 User,该类型在 @prisma/client 中定义

要执行种子脚本并创建 User 记录,您可以使用 package.json 中的 seed 脚本,如下所示

当您按照后续步骤操作时,您将多次运行种子脚本。为避免遇到唯一约束错误,您可以在 main 函数的开头删除数据库的内容,如下所示

注意: 这些命令删除每个数据库表中的所有行。请谨慎使用,并避免在生产环境中使用!

在此步骤中,您将创建一个课程,并使用嵌套写入来创建相关的测试

将以下内容添加到 main 函数

这将在 Course 表中创建一行,并在 Tests 表中创建三个相关行(CourseTests 具有一对多关系,允许这样做)。

如果您想在以前的步骤中创建的用户与本课程之间创建关系,作为教师,该怎么办?

UserCourse 具有显式多对多关系。这意味着我们必须在 CourseEnrollment 表中创建行,并分配角色以将 User 链接到 Course

可以按如下方式完成(添加到上一步的查询)

注意: include 参数允许您在结果中获取关系。这在稍后的步骤中将很有用,以便将测试结果与测试相关联

当使用嵌套写入时(如 memberstests),有两个选项

  • connect:与现有行创建关系
  • create:创建新行和关系

tests 的情况下,您传递了一个对象数组,这些对象链接到创建的课程。

members 的情况下,createconnect 均被使用。这是必要的,因为即使 user 已经存在,也需要在关系表(由 members 引用的 CourseEnrollment)中创建行,该行使用 connect 与先前创建的用户形成关系。

创建用户并关联到课程

在上一步中,您创建了一个课程,关联了测试,并将教师分配给了该课程。在这一步中,您将创建更多用户并将他们作为学生关联到课程。

添加以下语句

为学生添加测试结果

查看 TestResult 模型,它有三个关系:studentgradedBytest。要为 Shakuntala 和 David 添加测试结果,您将使用与前面步骤类似的嵌套写入。

以下是 TestResult 模型,供您参考

添加单个测试结果如下所示

要为 David 和 Shakuntala 的每个学生为三个测试中的每一个添加一个测试结果,您可以创建一个循环

恭喜您,如果您已达到此步骤,则说明您已成功在数据库中为用户、课程、测试和测试结果创建了示例数据。

要浏览数据库中的数据,您可以运行 Prisma Studio。Prisma Studio 是数据库的可视化编辑器。要运行 Prisma Studio,请在终端中运行以下命令

使用 Prisma Client 聚合测试结果

Prisma Client 允许您对模型的数字字段(例如 IntFloat)执行聚合操作。聚合操作从一组输入值(即表中的多行)计算单个结果。例如,计算一组 TestResult 行中 result 列的最小值最大值平均值

在此步骤中,您将运行两种聚合操作

  1. 对于课程中跨所有学生的每个测试,得出的聚合结果代表测试的难度或班级对测试主题的理解程度

    结果如下

  2. 对于跨所有测试的每个学生,得出的聚合结果代表学生在课程中的表现

    结果如下终端输出

总结和后续步骤

本文涵盖了很多内容,从问题域开始,然后深入研究数据建模、Prisma Schema、使用 Prisma Migrate 进行数据库迁移、使用 Prisma Client 进行 CRUD 以及聚合。

在开始编写代码之前,先规划问题域通常是一个好建议,因为它会影响数据模型的设计,而数据模型会影响后端的方方面面。

虽然 Prisma 旨在使关系数据库的使用变得容易,但更深入地了解底层数据库可能会有所帮助。

查看 Prisma 的数据指南,以了解有关数据库如何工作、如何选择合适的数据库以及如何充分利用数据库应用程序的更多信息。

在本系列的下一部分中,您将了解更多关于

  • API 层
  • 验证
  • 测试
  • 身份验证
  • 授权
  • 与外部 API 集成
  • 部署

加入 下一次直播,直播将于欧洲中部夏令时间 8 月 12 日下午 6:00 在 YouTube 上进行。

不要错过下一篇文章!

注册 Prisma 新闻通讯