使用 Prisma 介紹 JavaScript 中的類型安全
譯文【51CTO.com快譯】
如果經常使用 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】