如果经常使用 JavaScript,可能会遇到与类型相关的问题。例如,可能不小心将值从整数转换为字符串:
- console.log("User's cart value:", "500" + 100)
- [Log] User's cart value: "500100"
看似十分简单的问题,但是看似无害的错误存在于应用程序的有效代码中,则可能会成为一个真正的问题。随着 JavaScript 越来越多地用于关键服务,这种情况很可能在现实生活中发生。幸运的是,像
Prisma这样的数据库工具为 JavaScript 项目的数据库访问层提供了类型安全。
在本文中,我们提供了一个关于输入 JavaScript 的背景知识,并强调了对实际项目的影响。然后,通过一个使用Prisma、Fastify和MySQL构建的示例应用程序,该应用程序实现了自动检查以提高类型安全性,下文就进行详细讲解。
静态与动态类型
在不同的编程语言中,变量和值的类型检查可以在程序编译或执行的不同阶段进行。语言还可以允许或不允许某些操作,以及允许或禁止类型组合。
根据类型检查发生的具体时间,编程语言可以是静态类型,也可以是动态类型的。静态类型语言,如 C++、Haskell 和 Java,通常都在编译时检查类型错误。
动态类型语言,比如 JavaScript,在程序执行期间检查类型错误。类型错误在JavaScript 中并不容易,因为这种语言的变量没有类型。但是,如果我们试图“意外”将变量作为函数,我们将在程序运行时得到一个 TypeError:
- // types.js
- numBakers = 2;
- console.log(numBakers());
在我们的控制台中,错误如下所示:
- TypeError: numBakers is not a function
强打字 与 弱打字
类型检查的另一个关键点是强类型与弱类型。在这里,强弱之间的界限是模糊的,取决于开发者或社区的意见。
有人说,如果语言允许隐式类型转换,那么它就是弱类型的。在 JavaScript 中,即使我们正在计算整数和字符串的总和,以下代码也是有效的:
- numBakers = 1;
- numWaiters = "many";
- totalStaff = numBakers + numWaiters; // 1many
这段代码执行时没有错误,并且在计算 totalStaff 的值时,值 1 被隐式转换为字符串。
基于这种行为,JavaScript 可以被认为是一种弱类型语言。但实际上,弱类型意味着什么呢?
对于许多开发人员来说,在编写 JavaScript 代码时,类型弱点会导致不适和不确定性,尤其是在使用严谨系统时,例如计费代码或会计数据。变量中的意外类型,如果不加以控制,可能会导致混乱甚至造成实际损害。
下面是一个烘焙设备网站的案例代码,实现购买一个商业级搅拌机和一些替代零件的功能:
- // types.js
- mixerPrice = "1870";
- mixerPartsPrice = 100;
- console.log("Total cart value:", mixerPrice + mixerPartsPrice);
请注意,在如何定义价格之间存在类型不匹配。可能是之前发送到后端的类型不正确,结果导致信息错误地存储在数据库中。如果我们执行这段代码会发生什么?让我们运行示例来说明结果
$ node types.js Total cart value: 1870100
JavaScript 缺乏类型安全性如何降低速度
为了避免上述情况,开发人员寻求类型安全性——保证他们操作的数据属是特定类型的,并会导致可预测的行为。
作为一种弱类型语言,JavaScript 不提供类型安全性。尽管如此,许多处理银行余额、保险金额和其他敏感数据的生产系统都是用 JavaScript 开发的。
开发人员对意外行为保持警惕,不仅因为它可能导致错误的交易金额。由于各种其他原因,JavaScript 中缺乏类型安全可能会带来不便,例如:
• 生产力降低:如果你必须处理类型错误,调试它们并思考所有类型交互出错的可能性可能需要很长时间。
• 处理类型不匹配的样板代码:在类型敏感的操作中,开发人员经常需要添加代码来检查类型并协调任何可能的差异。此外,工程师必须编写许多测试来处理未定义的数据类型。添加与应用程序的业务价值没有直接关系的额外代码,对于保持代码库的可读性和清洁度来说并不理想。
• 缺乏明确的错误消息:有时类型不匹配会在类型错误的位置产生神秘的错误。在这种情况下,类型错误可能难以调试。
• 写入数据库时出现意外问题:类型错误可能会导致写入数据库时出现问题。例如,随着应用程序数据库的发展,开发人员经常需要添加新的数据库字段。在临时环境中添加字段,但忘记将其推出到生产环境中,可能会导致生产部署上线时出现意外类型错误。
由于应用程序的数据库层中的类型错误会因数据损坏而造成很多危害,因此开发人员必须针对缺乏类型安全性引入的问题提出解决方案。在下一节中,我们将讨论引入 Prisma 之类的工具如何帮助您解决 JavaScript 项目中的类型安全问题。
使用 Prisma 进行类型安全的数据库访问
虽然 JavaScript 本身不提供内置类型安全,但 Prisma 允许您在应用程序中选择类型安全检查。Prisma 是一种新的ORM 工具,它由一个用于 JavaScript 和 TypeScript 的类型安全查询构建器(Prisma Client)、一个迁移系统(Prisma Migrate)和一个用于与数据库交互的 GUI (Prisma Studio)组成。
Prisma 中类型检查的核心是Prisma 模式,它是您建模数据的唯一真实来源。这是最小架构的样子:
- // prisma/schema.prisma
- model Baker {
- id Int @id @default(autoincrement())
- email String @unique
- name String?
- }
在这个例子中,模式描述了一个 Baker 实体,其中每个实例,一个单独的面包师,有一个电子邮件(一个字符串)、一个名字(也是一个字符串,可选)和一个自动递增的标识符(一个整数)。“模型”一词用于描述映射到后端数据库表的数据实体。
在幕后,Prisma CLI从您的 Prisma 模式生成Prisma 客户端。生成的代码允许您在 JavaScript 中方便地访问您的数据库,并实现了一系列检查和实用程序,使您的代码类型安全。Prisma 模式中定义的每个模型都被转换为一个 JavaScript 类,其中包含用于访问单个记录的函数。
通过在项目中使用 Prisma 之类的工具,您可以在使用库(其对象关系映射层,或 ORM)生成的数据类型访问数据库中的记录时开始利用额外的类型检查。
在 Fastify 应用中实现类型安全的示例
让我们看一个在 Fastify 应用程序中使用 Prisma 模式的例子。Fastify 是 Node.js 的 Web 框架,专注于性能和简单性。
我们将使用prisma-fastify-bakery项目,它实现了一个简单的系统来跟踪面包店的运营。
初步设置
要运行该项目,我们需要在我们的开发机器上设置一个 最新的 Node.js 版本。第一步是克隆 repo 并安装所有必需的依赖项:
$ git pull https://github.com/chief-wizard/prisma-fastify-bakery.git $ cd prisma-fastify-bakery $ npm install
我们还需要确保我们有一个 MySQL 服务器正在运行。如果您在安装和设置 MySQL 方面需要帮助,请查看Prisma 的有关该主题的指南。
为了记录可以访问数据库的位置,我们将在存储库的根目录中创建一个 .env 文件:
$ touch .env
现在,我们可以将数据库 URL 添加到 .env 文件中。以下是示例文件的外观:
DATABASE_URL = 'mysql://root:bakingbread@localhost/mydb?schema=public'
设置完成后,让我们继续创建 Prisma 模式的步骤。
创建架构
实现类型安全的第一步是添加模式。在我们的prisma/schema.prisma 文件中,我们定义了数据源,在本例中是我们的MySQL 数据库。请注意,我们不是在架构文件中硬编码我们的数据库凭据,而是从 .env 文件中读取数据库 URL。从环境中读取敏感数据在安全性方面更安全:
- datasource db {
- provider = "mysql"
- url = env("DATABASE_URL")
- }
然后我们定义与我们的应用程序相关的类型。在我们的例子中,让我们看看我们将在面包店销售的产品的模型。我们想要记录法式长棍面包和羊角面包等物品,并使跟踪咖啡袋和果汁瓶等物品成为可能。项目将具有“糕点”、“面包”或“咖啡”等类型,以及“甜”或“咸”等类别(如适用)。我们还将存储每个产品的销售参考,以及产品的价格和成分。
在 Prisma 模式文件中,我们首先命名我们的 Product 模型:
- model Product {
- ...
- }
我们可以添加一个 id 属性——这将帮助我们快速识别 products 表中的每条记录,并将用作索引:
- model Product {
- ...
- id Int @id @default(autoincrement())
- ...
- }
然后我们可以添加我们希望每个项目包含的所有其他属性。在这里,我们希望每个项目的名称都是唯一的,每个产品只给我们一个条目。为了参考成分和销售,我们使用成分和销售类型,我们分别定义:
- model Product {
- ...
- name String @unique
- type String
- category String
- ingredients Ingredient[]
- sales Sale[]
- price Float
- ...
- }
现在,我们在 Prisma 模式中拥有完整的产品模型。以下是prisma.schema 文件的样子,包括Ingredient 和Sale 模型:
- model Product {
- id Int @id @default(autoincrement())
- name String @unique
- type String
- category String
- ingredients Ingredient[]
- sales Sale[]
- price Float
- }
- model Ingredient {
- id Int @id @default(autoincrement())
- name String @unique
- allergen Boolean
- vegan Boolean
- vegetarian Boolean
- products Product? @relation(fields: [products_id], re
$ npx prisma migrate dev --name init- ferences: [id])
- products_id Int?
- }
- model Sale {
- id Int @id @default(autoincrement())
- date DateTime @default(now())
- item Product? @relation(fields: [item_id], references: [id])
- item_id Int?
- }
为了将我们的模型转换为实时数据库表,我们指示 Prisma 运行迁移。迁移包含用于在数据库中创建表、索引和外键的 SQL 代码。我们还传递了此迁移所需的名称 init,它代表“初始迁移”:
$ npx prisma migrate dev --name init
我们看到以下输出表明已根据我们的架构创建了数据库:
MySQL database mydb created at localhost:3306 The following migration(s) have been applied: migrations/ └─ 20210619135805_init/ └─ migration.sql ... Your database is now in sync with your schema. ✔ Generated Prisma Client (2.25.0) to ./node_modules/@prisma/client in 468ms
此时,我们已准备好在我们的应用程序中使用我们的模式定义的对象。
创建一个使用我们 Prisma Schema 的 REST API
在本节中,我们将开始使用 Prisma 模式中的类型,从而为实现类型安全奠定基础。如果您想查看实际的类型安全检查,请直接跳到下一部分。
由于我们在示例中使用 Fastify,因此我们在 fastify/routes 目录下创建了一个 product.js 文件。我们从 Prisma 模式中添加产品模型,如下所示:
- const { PrismaClient } = require("@prisma/client")
- const { products } = new PrismaClient()
然后我们可以定义一个 Fastify 路由,它在模型上使用 Prisma 提供的 findMany 函数。我们将参数 take: 100 传递给查询以将结果限制为最多 100 个项目,以避免我们的 API 过载:
- async function routes (fastify, options) {
- fastify.get('/products', async (req, res) => {
- const list = await product.findMany({
- take: 100,
- })
- res.send(list)
- })
- ...
当我们尝试为烘焙产品添加创建端点时,类型安全的真正价值就发挥了作用。通常,我们需要检查每个输入的类型。但是在我们的示例中,我们可以完全跳过检查,因为 Prisma Client 将首先通过模式运行它们:
- ...
- // create
- fastify.post('/product/create', async (req, res) => {
- let addProduct = req.body;
- const productExists = await product.findUnique({
- where: {
- name: addProduct.name
- }
- })
- if(!productExists){
- let newProduct = await product.create({
- data: {
- name: addProduct.name,
- type: addProduct.type,
- category: addProduct.category,
- sales: addProduct.sales,
- price: addProduct.price,
- },
- })
- res.send(newProduct);
- } else {
- res.code(400).send({message: 'record already exists'})
- }
- })
- ...
在上面的示例中,我们在 /product/create 端点中执行以下步骤:
• 将请求的正文分配给变量 addProduct。该变量包含请求中提供的所有详细信息。
• 使用findUnique函数找出我们是否已经有同名的产品。where 子句允许我们过滤结果以仅包含具有我们提供的名称的产品。如果在运行此查询后 productExists 变量非空,那么我们已经有一个同名的现有产品。
• 如果产品不存在:
• 我们使用请求中收到的所有字段创建它。我们通过使用 product.create 函数来实现,其中新产品的详细信息位于数据部分下。
• 如果产品已经存在,我们返回一个错误。
下一步,让我们使用cURL测试 /product 和 /product/create 端点。
使用 Prisma Studio 填充数据库并测试我们的 API
我们可以通过运行以下命令来启动我们的开发服务器:
$ npm run dev
让我们打开Prisma Studio并查看当前数据库中的内容。我们将运行以下命令来启动 Prisma Studio:
$ npx prisma studio
启动后,我们将看到应用程序中的不同模型以及每个模型在本地 URL http://localhost:5555 上的记录数:
当前在 Product 模型下没有条目,因此让我们通过单击“添加新记录”按钮创建几条记录:
添加这些数据点后,让我们使用以下 cURL 命令测试我们的产品端点:
$ curl localhost:3000/products # output [{"id":1,"name":"baguette","type":"savory","category":"bread","price":3,"ingredients":[]},{"id":2,"name":"white bread roll","type":"savory","category":"bread","price":2,"ingredients":[]}]
让我们通过我们的产品创建 API 创建另一个产品:
$ curl -X POST -H 'Content-Type: application/json' -d '{"name": "rye bread roll", "type":"savory", "category":"bread", "price": 2}' localhost:3000/product/create # output {"id":3,"name":"rye bread roll","type":"savory","category":"bread","price":2,"ingredients":[]}
另一个项目成功创建!接下来,让我们看看我们的示例在类型安全方面的表现。
在我们的 API 中尝试类型安全
请记住,我们目前没有在我们的产品创建端点上检查请求的内容。如果我们错误地使用字符串而不是浮点数指定价格会发生什么?让我们来了解一下:
$ curl -X POST -H 'Content-Type: application/json' -d '{"name": "whole wheat bread roll", "type":"savory", "category":"bread", "price": "1.50"}' localhost:3000/product/create # output {"statusCode":500,"error":"Internal Server Error","message":"\nInvalid `prisma.product.create()` invocation:\n\n{\n data: {\n name: 'whole wheat bread roll',\n type: 'savory',\n category: 'bread',\n sales: undefined,\n price: '1.50',\n ~~~~~~\n ingredients: {\n connect: undefined\n }\n },\n include: {\n ingredients: true\n }\n}\n\nArgument price: Got invalid value '1.50' on prisma.createOneProduct. Provided String, expected Float.\n\n"}
如您所见,Prisma 检查阻止了我们创建定价不正确的项目——我们不必为这种特殊情况添加任何明确的检查!
为现有项目添加类型安全的提示
至此,我们已经很清楚类型检查可以添加到 JavaScript 项目中的价值。如果您想尝试将此类检查添加到现有项目中,这里有一些提示可帮助您开始。
内省数据库以生成初始模式
使用 Prisma 时,数据库内省允许您查看数据库中表的当前布局,并根据您已有的信息生成新的模式。如果您不想手动编写模式,此功能是一个有用的起点。
尝试运行 npxprisma introspect,只需几秒钟,您的项目目录中就会自动生成一个新的 schema.prisma 文件。
VS Code 中的类型检查
如果Visual Studio Code是您选择的编程环境,您可以利用 ts-check 指令直接在您的代码中获取类型检查建议。在使用 Prisma 客户端的 JavaScript 文件中,在每个文件的顶部添加以下注释:
- // @ts-check
启用此检查后,如
突出显示类型错误可以更容易地及早发现与类型相关的问题。在这篇 Productive Development with Prisma 文章中了解有关此功能的更多信息。
在您的持续集成环境中进行类型检查
上面使用 @ts-check 的技巧有效,因为 Visual Studio Code 通过 TypeScript 编译器运行您的 JavaScript 文件。您还可以直接运行 TypeScript 编译器,例如,在您的持续集成环境中。在逐个文件的基础上添加类型检查可能是启动类型安全工作的可行方法。
要开始检查文件中的类型,请将 TypeScript 编译器添加为开发依赖项:
$ npm install typescript --save-dev
安装依赖项后,您现在可以在 JavaScript 文件上运行编译器,如果有任何异常,编译器将发出警告。我们建议开始对一个或几个文件运行 TypeScript 检查:
- $ npx tsc --noEmit --allowJs --checkJs fastify/routes/product.js
上面的例子将在我们的 Fastify 产品路由文件上运行 TypeScript 编译器。
了解有关在 JavaScript 中实现类型安全的更多信息
准备好将一些类型安全的代码烘焙到您自己的代码库中了吗?在prisma-fastify-bakery 存储库中查看我们完整的代码示例并尝试自己运行该项目。
【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】