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 字段可以分解如下

名称类型标量 vs 关系类型修饰符属性
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 将获取您 schema 的快照,并计算出执行迁移所需的 SQL。包含 SQL 的迁移文件将被保存到 prisma/migrations
  • 运行迁移: Prisma Migrate 将执行迁移文件中的 SQL 以运行迁移并更改(或创建)数据库 schema

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

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

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

生成 Prisma Client

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

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

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

  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 将推断出在 @prisma/client 中定义的 User 类型

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

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

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

在这一步中,您将创建一个 *course* 并使用嵌套写入来创建相关的 *tests*。

将以下内容添加到 main 函数

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

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

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

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

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

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

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

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

对于 members,同时使用了 createconnect。这是必要的,因为即使 user 已经存在,也需要在关系表(CourseEnrollment,由 members 引用)中创建一个*新的*行,该行使用 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 新闻通讯