2020年7月30日

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

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

Backend with TypeScript PostgreSQL & Prisma: Data Modeling & CRUD

引言

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

上述提供了直播录像,其内容与本文相同。

本系列将涵盖的内容

本系列将聚焦数据库在后端开发各个方面的作用,包括:

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

今日您将学习到的内容

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

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

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

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

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

先决条件

假定知识

本系列假定您具备TypeScript、Node.js和关系型数据库的基础知识。如果您熟悉JavaScript但尚未尝试TypeScript,您仍然应该能够跟上。本系列将使用PostgreSQL,但大多数概念也适用于其他关系型数据库,如MySQL。除此之外,无需具备Prisma的先验知识,因为本系列将对此进行介绍。

开发环境

您应该安装以下软件:

  • Node.js
  • Docker(将用于运行开发用的PostgreSQL数据库)

如果您使用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 schema

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

Prisma schema被用作生成Prisma Client和Prisma Migrate以创建数据库schema的单一事实来源。

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

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

定义模型

Prisma schema的基本组成部分是model。每个模型都映射到一个数据库表。

以下是显示模型基本签名的示例:

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

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

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

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

对问题领域和使用Prisma建模数据有了很好的理解后,您现在可以将以下模型添加到您的prisma/schema.prisma文件中:

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

定义关系

一对多

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

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

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

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

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

多对多关系

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

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

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

要创建隐式多对多关系,请在关系的两侧将关系字段定义为列表:

通过此操作,Prisma将创建关系表,以便评分系统可以维护上述定义的属性:

  • 一门课程可以关联多个用户。
  • 一个用户可以关联多门课程。

然而,评分系统的一个要求是允许用户以教师学生的身份关联到课程。这意味着我们需要一种在数据库中存储有关关系的“元信息”的方式。

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

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

关于CourseEnrollment模型需要注意的事项:

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

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

完整schema

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

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

迁移数据库

定义了Prisma schema后,您现在将使用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将推断出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新闻通讯

© . All rights reserved.