使用 Prisma 介紹 JavaScript 中的類型安全
譯文【51CTO.com快譯】
如果經(jīng)常使用 JavaScript,可能會(huì)遇到與類型相關(guān)的問題。例如,可能不小心將值從整數(shù)轉(zhuǎn)換為字符串:
- console.log("User's cart value:", "500" + 100)
- [Log] User's cart value: "500100"
看似十分簡(jiǎn)單的問題,但是看似無害的錯(cuò)誤存在于應(yīng)用程序的有效代碼中,則可能會(huì)成為一個(gè)真正的問題。隨著 JavaScript 越來越多地用于關(guān)鍵服務(wù),這種情況很可能在現(xiàn)實(shí)生活中發(fā)生。幸運(yùn)的是,像
Prisma這樣的數(shù)據(jù)庫工具為 JavaScript 項(xiàng)目的數(shù)據(jù)庫訪問層提供了類型安全。
在本文中,我們提供了一個(gè)關(guān)于輸入 JavaScript 的背景知識(shí),并強(qiáng)調(diào)了對(duì)實(shí)際項(xiàng)目的影響。然后,通過一個(gè)使用Prisma、Fastify和MySQL構(gòu)建的示例應(yīng)用程序,該應(yīng)用程序?qū)崿F(xiàn)了自動(dòng)檢查以提高類型安全性,下文就進(jìn)行詳細(xì)講解。
靜態(tài)與動(dòng)態(tài)類型
在不同的編程語言中,變量和值的類型檢查可以在程序編譯或執(zhí)行的不同階段進(jìn)行。語言還可以允許或不允許某些操作,以及允許或禁止類型組合。
根據(jù)類型檢查發(fā)生的具體時(shí)間,編程語言可以是靜態(tài)類型,也可以是動(dòng)態(tài)類型的。靜態(tài)類型語言,如 C++、Haskell 和 Java,通常都在編譯時(shí)檢查類型錯(cuò)誤。
動(dòng)態(tài)類型語言,比如 JavaScript,在程序執(zhí)行期間檢查類型錯(cuò)誤。類型錯(cuò)誤在JavaScript 中并不容易,因?yàn)檫@種語言的變量沒有類型。但是,如果我們?cè)噲D“意外”將變量作為函數(shù),我們將在程序運(yùn)行時(shí)得到一個(gè) TypeError:
- // types.js
- numBakers = 2;
- console.log(numBakers());
在我們的控制臺(tái)中,錯(cuò)誤如下所示:
- TypeError: numBakers is not a function
強(qiáng)打字 與 弱打字
類型檢查的另一個(gè)關(guān)鍵點(diǎn)是強(qiáng)類型與弱類型。在這里,強(qiáng)弱之間的界限是模糊的,取決于開發(fā)者或社區(qū)的意見。
有人說,如果語言允許隱式類型轉(zhuǎn)換,那么它就是弱類型的。在 JavaScript 中,即使我們正在計(jì)算整數(shù)和字符串的總和,以下代碼也是有效的:
- numBakers = 1;
- numWaiters = "many";
- totalStaff = numBakers + numWaiters; // 1many
這段代碼執(zhí)行時(shí)沒有錯(cuò)誤,并且在計(jì)算 totalStaff 的值時(shí),值 1 被隱式轉(zhuǎn)換為字符串。
基于這種行為,JavaScript 可以被認(rèn)為是一種弱類型語言。但實(shí)際上,弱類型意味著什么呢?
對(duì)于許多開發(fā)人員來說,在編寫 JavaScript 代碼時(shí),類型弱點(diǎn)會(huì)導(dǎo)致不適和不確定性,尤其是在使用嚴(yán)謹(jǐn)系統(tǒng)時(shí),例如計(jì)費(fèi)代碼或會(huì)計(jì)數(shù)據(jù)。變量中的意外類型,如果不加以控制,可能會(huì)導(dǎo)致混亂甚至造成實(shí)際損害。
下面是一個(gè)烘焙設(shè)備網(wǎng)站的案例代碼,實(shí)現(xiàn)購買一個(gè)商業(yè)級(jí)攪拌機(jī)和一些替代零件的功能:
- // types.js
- mixerPrice = "1870";
- mixerPartsPrice = 100;
- console.log("Total cart value:", mixerPrice + mixerPartsPrice);
請(qǐng)注意,在如何定義價(jià)格之間存在類型不匹配。可能是之前發(fā)送到后端的類型不正確,結(jié)果導(dǎo)致信息錯(cuò)誤地存儲(chǔ)在數(shù)據(jù)庫中。如果我們執(zhí)行這段代碼會(huì)發(fā)生什么?讓我們運(yùn)行示例來說明結(jié)果
$ node types.js Total cart value: 1870100
JavaScript 缺乏類型安全性如何降低速度
為了避免上述情況,開發(fā)人員尋求類型安全性——保證他們操作的數(shù)據(jù)屬是特定類型的,并會(huì)導(dǎo)致可預(yù)測(cè)的行為。
作為一種弱類型語言,JavaScript 不提供類型安全性。盡管如此,許多處理銀行余額、保險(xiǎn)金額和其他敏感數(shù)據(jù)的生產(chǎn)系統(tǒng)都是用 JavaScript 開發(fā)的。
開發(fā)人員對(duì)意外行為保持警惕,不僅因?yàn)樗赡軐?dǎo)致錯(cuò)誤的交易金額。由于各種其他原因,JavaScript 中缺乏類型安全可能會(huì)帶來不便,例如:
• 生產(chǎn)力降低:如果你必須處理類型錯(cuò)誤,調(diào)試它們并思考所有類型交互出錯(cuò)的可能性可能需要很長(zhǎng)時(shí)間。
• 處理類型不匹配的樣板代碼:在類型敏感的操作中,開發(fā)人員經(jīng)常需要添加代碼來檢查類型并協(xié)調(diào)任何可能的差異。此外,工程師必須編寫許多測(cè)試來處理未定義的數(shù)據(jù)類型。添加與應(yīng)用程序的業(yè)務(wù)價(jià)值沒有直接關(guān)系的額外代碼,對(duì)于保持代碼庫的可讀性和清潔度來說并不理想。
• 缺乏明確的錯(cuò)誤消息:有時(shí)類型不匹配會(huì)在類型錯(cuò)誤的位置產(chǎn)生神秘的錯(cuò)誤。在這種情況下,類型錯(cuò)誤可能難以調(diào)試。
• 寫入數(shù)據(jù)庫時(shí)出現(xiàn)意外問題:類型錯(cuò)誤可能會(huì)導(dǎo)致寫入數(shù)據(jù)庫時(shí)出現(xiàn)問題。例如,隨著應(yīng)用程序數(shù)據(jù)庫的發(fā)展,開發(fā)人員經(jīng)常需要添加新的數(shù)據(jù)庫字段。在臨時(shí)環(huán)境中添加字段,但忘記將其推出到生產(chǎn)環(huán)境中,可能會(huì)導(dǎo)致生產(chǎn)部署上線時(shí)出現(xiàn)意外類型錯(cuò)誤。
由于應(yīng)用程序的數(shù)據(jù)庫層中的類型錯(cuò)誤會(huì)因數(shù)據(jù)損壞而造成很多危害,因此開發(fā)人員必須針對(duì)缺乏類型安全性引入的問題提出解決方案。在下一節(jié)中,我們將討論引入 Prisma 之類的工具如何幫助您解決 JavaScript 項(xiàng)目中的類型安全問題。
使用 Prisma 進(jìn)行類型安全的數(shù)據(jù)庫訪問
雖然 JavaScript 本身不提供內(nèi)置類型安全,但 Prisma 允許您在應(yīng)用程序中選擇類型安全檢查。Prisma 是一種新的ORM 工具,它由一個(gè)用于 JavaScript 和 TypeScript 的類型安全查詢構(gòu)建器(Prisma Client)、一個(gè)遷移系統(tǒng)(Prisma Migrate)和一個(gè)用于與數(shù)據(jù)庫交互的 GUI (Prisma Studio)組成。
Prisma 中類型檢查的核心是Prisma 模式,它是您建模數(shù)據(jù)的唯一真實(shí)來源。這是最小架構(gòu)的樣子:
- // prisma/schema.prisma
- model Baker {
- id Int @id @default(autoincrement())
- email String @unique
- name String?
- }
在這個(gè)例子中,模式描述了一個(gè) Baker 實(shí)體,其中每個(gè)實(shí)例,一個(gè)單獨(dú)的面包師,有一個(gè)電子郵件(一個(gè)字符串)、一個(gè)名字(也是一個(gè)字符串,可選)和一個(gè)自動(dòng)遞增的標(biāo)識(shí)符(一個(gè)整數(shù))。“模型”一詞用于描述映射到后端數(shù)據(jù)庫表的數(shù)據(jù)實(shí)體。
在幕后,Prisma CLI從您的 Prisma 模式生成Prisma 客戶端。生成的代碼允許您在 JavaScript 中方便地訪問您的數(shù)據(jù)庫,并實(shí)現(xiàn)了一系列檢查和實(shí)用程序,使您的代碼類型安全。Prisma 模式中定義的每個(gè)模型都被轉(zhuǎn)換為一個(gè) JavaScript 類,其中包含用于訪問單個(gè)記錄的函數(shù)。
通過在項(xiàng)目中使用 Prisma 之類的工具,您可以在使用庫(其對(duì)象關(guān)系映射層,或 ORM)生成的數(shù)據(jù)類型訪問數(shù)據(jù)庫中的記錄時(shí)開始利用額外的類型檢查。
在 Fastify 應(yīng)用中實(shí)現(xiàn)類型安全的示例
讓我們看一個(gè)在 Fastify 應(yīng)用程序中使用 Prisma 模式的例子。Fastify 是 Node.js 的 Web 框架,專注于性能和簡(jiǎn)單性。
我們將使用prisma-fastify-bakery項(xiàng)目,它實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的系統(tǒng)來跟蹤面包店的運(yùn)營(yíng)。
初步設(shè)置
要運(yùn)行該項(xiàng)目,我們需要在我們的開發(fā)機(jī)器上設(shè)置一個(gè) 最新的 Node.js 版本。第一步是克隆 repo 并安裝所有必需的依賴項(xiàng):
$ git pull https://github.com/chief-wizard/prisma-fastify-bakery.git $ cd prisma-fastify-bakery $ npm install
我們還需要確保我們有一個(gè) MySQL 服務(wù)器正在運(yùn)行。如果您在安裝和設(shè)置 MySQL 方面需要幫助,請(qǐng)查看Prisma 的有關(guān)該主題的指南。
為了記錄可以訪問數(shù)據(jù)庫的位置,我們將在存儲(chǔ)庫的根目錄中創(chuàng)建一個(gè) .env 文件:
$ touch .env
現(xiàn)在,我們可以將數(shù)據(jù)庫 URL 添加到 .env 文件中。以下是示例文件的外觀:
DATABASE_URL = 'mysql://root:bakingbread@localhost/mydb?schema=public'
設(shè)置完成后,讓我們繼續(xù)創(chuàng)建 Prisma 模式的步驟。
創(chuàng)建架構(gòu)
實(shí)現(xiàn)類型安全的第一步是添加模式。在我們的prisma/schema.prisma 文件中,我們定義了數(shù)據(jù)源,在本例中是我們的MySQL 數(shù)據(jù)庫。請(qǐng)注意,我們不是在架構(gòu)文件中硬編碼我們的數(shù)據(jù)庫憑據(jù),而是從 .env 文件中讀取數(shù)據(jù)庫 URL。從環(huán)境中讀取敏感數(shù)據(jù)在安全性方面更安全:
- datasource db {
- provider = "mysql"
- url = env("DATABASE_URL")
- }
然后我們定義與我們的應(yīng)用程序相關(guān)的類型。在我們的例子中,讓我們看看我們將在面包店銷售的產(chǎn)品的模型。我們想要記錄法式長(zhǎng)棍面包和羊角面包等物品,并使跟蹤咖啡袋和果汁瓶等物品成為可能。項(xiàng)目將具有“糕點(diǎn)”、“面包”或“咖啡”等類型,以及“甜”或“咸”等類別(如適用)。我們還將存儲(chǔ)每個(gè)產(chǎn)品的銷售參考,以及產(chǎn)品的價(jià)格和成分。
在 Prisma 模式文件中,我們首先命名我們的 Product 模型:
- model Product {
- ...
- }
我們可以添加一個(gè) id 屬性——這將幫助我們快速識(shí)別 products 表中的每條記錄,并將用作索引:
- model Product {
- ...
- id Int @id @default(autoincrement())
- ...
- }
然后我們可以添加我們希望每個(gè)項(xiàng)目包含的所有其他屬性。在這里,我們希望每個(gè)項(xiàng)目的名稱都是唯一的,每個(gè)產(chǎn)品只給我們一個(gè)條目。為了參考成分和銷售,我們使用成分和銷售類型,我們分別定義:
- model Product {
- ...
- name String @unique
- type String
- category String
- ingredients Ingredient[]
- sales Sale[]
- price Float
- ...
- }
現(xiàn)在,我們?cè)?Prisma 模式中擁有完整的產(chǎn)品模型。以下是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?
- }
為了將我們的模型轉(zhuǎn)換為實(shí)時(shí)數(shù)據(jù)庫表,我們指示 Prisma 運(yùn)行遷移。遷移包含用于在數(shù)據(jù)庫中創(chuàng)建表、索引和外鍵的 SQL 代碼。我們還傳遞了此遷移所需的名稱 init,它代表“初始遷移”:
$ npx prisma migrate dev --name init
我們看到以下輸出表明已根據(jù)我們的架構(gòu)創(chuàng)建了數(shù)據(jù)庫:
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
此時(shí),我們已準(zhǔn)備好在我們的應(yīng)用程序中使用我們的模式定義的對(duì)象。
創(chuàng)建一個(gè)使用我們 Prisma Schema 的 REST API
在本節(jié)中,我們將開始使用 Prisma 模式中的類型,從而為實(shí)現(xiàn)類型安全奠定基礎(chǔ)。如果您想查看實(shí)際的類型安全檢查,請(qǐng)直接跳到下一部分。
由于我們?cè)谑纠惺褂?Fastify,因此我們?cè)?fastify/routes 目錄下創(chuàng)建了一個(gè) product.js 文件。我們從 Prisma 模式中添加產(chǎn)品模型,如下所示:
- const { PrismaClient } = require("@prisma/client")
- const { products } = new PrismaClient()
然后我們可以定義一個(gè) Fastify 路由,它在模型上使用 Prisma 提供的 findMany 函數(shù)。我們將參數(shù) take: 100 傳遞給查詢以將結(jié)果限制為最多 100 個(gè)項(xiàng)目,以避免我們的 API 過載:
- async function routes (fastify, options) {
- fastify.get('/products', async (req, res) => {
- const list = await product.findMany({
- take: 100,
- })
- res.send(list)
- })
- ...
當(dāng)我們嘗試為烘焙產(chǎn)品添加創(chuàng)建端點(diǎn)時(shí),類型安全的真正價(jià)值就發(fā)揮了作用。通常,我們需要檢查每個(gè)輸入的類型。但是在我們的示例中,我們可以完全跳過檢查,因?yàn)?Prisma Client 將首先通過模式運(yùn)行它們:
- ...
- // 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'})
- }
- })
- ...
在上面的示例中,我們?cè)?/product/create 端點(diǎn)中執(zhí)行以下步驟:
• 將請(qǐng)求的正文分配給變量 addProduct。該變量包含請(qǐng)求中提供的所有詳細(xì)信息。
• 使用findUnique函數(shù)找出我們是否已經(jīng)有同名的產(chǎn)品。where 子句允許我們過濾結(jié)果以僅包含具有我們提供的名稱的產(chǎn)品。如果在運(yùn)行此查詢后 productExists 變量非空,那么我們已經(jīng)有一個(gè)同名的現(xiàn)有產(chǎn)品。
• 如果產(chǎn)品不存在:
• 我們使用請(qǐng)求中收到的所有字段創(chuàng)建它。我們通過使用 product.create 函數(shù)來實(shí)現(xiàn),其中新產(chǎn)品的詳細(xì)信息位于數(shù)據(jù)部分下。
• 如果產(chǎn)品已經(jīng)存在,我們返回一個(gè)錯(cuò)誤。
下一步,讓我們使用cURL測(cè)試 /product 和 /product/create 端點(diǎn)。
使用 Prisma Studio 填充數(shù)據(jù)庫并測(cè)試我們的 API
我們可以通過運(yùn)行以下命令來啟動(dòng)我們的開發(fā)服務(wù)器:
$ npm run dev
讓我們打開Prisma Studio并查看當(dāng)前數(shù)據(jù)庫中的內(nèi)容。我們將運(yùn)行以下命令來啟動(dòng) Prisma Studio:
$ npx prisma studio
啟動(dòng)后,我們將看到應(yīng)用程序中的不同模型以及每個(gè)模型在本地 URL http://localhost:5555 上的記錄數(shù):
當(dāng)前在 Product 模型下沒有條目,因此讓我們通過單擊“添加新記錄”按鈕創(chuàng)建幾條記錄:
添加這些數(shù)據(jù)點(diǎn)后,讓我們使用以下 cURL 命令測(cè)試我們的產(chǎn)品端點(diǎn):
$ 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":[]}]
讓我們通過我們的產(chǎn)品創(chuàng)建 API 創(chuàng)建另一個(gè)產(chǎn)品:
$ 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":[]}
另一個(gè)項(xiàng)目成功創(chuàng)建!接下來,讓我們看看我們的示例在類型安全方面的表現(xiàn)。
在我們的 API 中嘗試類型安全
請(qǐng)記住,我們目前沒有在我們的產(chǎn)品創(chuàng)建端點(diǎn)上檢查請(qǐng)求的內(nèi)容。如果我們錯(cuò)誤地使用字符串而不是浮點(diǎn)數(shù)指定價(jià)格會(huì)發(fā)生什么?讓我們來了解一下:
$ 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 檢查阻止了我們創(chuàng)建定價(jià)不正確的項(xiàng)目——我們不必為這種特殊情況添加任何明確的檢查!
為現(xiàn)有項(xiàng)目添加類型安全的提示
至此,我們已經(jīng)很清楚類型檢查可以添加到 JavaScript 項(xiàng)目中的價(jià)值。如果您想嘗試將此類檢查添加到現(xiàn)有項(xiàng)目中,這里有一些提示可幫助您開始。
內(nèi)省數(shù)據(jù)庫以生成初始模式
使用 Prisma 時(shí),數(shù)據(jù)庫內(nèi)省允許您查看數(shù)據(jù)庫中表的當(dāng)前布局,并根據(jù)您已有的信息生成新的模式。如果您不想手動(dòng)編寫模式,此功能是一個(gè)有用的起點(diǎn)。
嘗試運(yùn)行 npxprisma introspect,只需幾秒鐘,您的項(xiàng)目目錄中就會(huì)自動(dòng)生成一個(gè)新的 schema.prisma 文件。
VS Code 中的類型檢查
如果Visual Studio Code是您選擇的編程環(huán)境,您可以利用 ts-check 指令直接在您的代碼中獲取類型檢查建議。在使用 Prisma 客戶端的 JavaScript 文件中,在每個(gè)文件的頂部添加以下注釋:
- // @ts-check
啟用此檢查后,如
突出顯示類型錯(cuò)誤可以更容易地及早發(fā)現(xiàn)與類型相關(guān)的問題。在這篇 Productive Development with Prisma 文章中了解有關(guān)此功能的更多信息。
在您的持續(xù)集成環(huán)境中進(jìn)行類型檢查
上面使用 @ts-check 的技巧有效,因?yàn)?Visual Studio Code 通過 TypeScript 編譯器運(yùn)行您的 JavaScript 文件。您還可以直接運(yùn)行 TypeScript 編譯器,例如,在您的持續(xù)集成環(huán)境中。在逐個(gè)文件的基礎(chǔ)上添加類型檢查可能是啟動(dòng)類型安全工作的可行方法。
要開始檢查文件中的類型,請(qǐng)將 TypeScript 編譯器添加為開發(fā)依賴項(xiàng):
$ npm install typescript --save-dev
安裝依賴項(xiàng)后,您現(xiàn)在可以在 JavaScript 文件上運(yùn)行編譯器,如果有任何異常,編譯器將發(fā)出警告。我們建議開始對(duì)一個(gè)或幾個(gè)文件運(yùn)行 TypeScript 檢查:
- $ npx tsc --noEmit --allowJs --checkJs fastify/routes/product.js
上面的例子將在我們的 Fastify 產(chǎn)品路由文件上運(yùn)行 TypeScript 編譯器。
了解有關(guān)在 JavaScript 中實(shí)現(xiàn)類型安全的更多信息
準(zhǔn)備好將一些類型安全的代碼烘焙到您自己的代碼庫中了嗎?在prisma-fastify-bakery 存儲(chǔ)庫中查看我們完整的代碼示例并嘗試自己運(yùn)行該項(xiàng)目。
【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文譯者和出處為51CTO.com】