Prisma ORM 是 ORM 嗎?
簡要回答這個問題:是的,Prisma ORM 是一種新型 ORM,它與傳統 ORM 根本不同,並且不會遇到許多通常與傳統 ORM 相關的問題。
傳統 ORM 透過將表對映到程式語言中的模型類,提供了一種面向物件的方式來處理關係資料庫。這種方法導致了許多問題,這些問題是由物件-關係阻抗不匹配引起的。
Prisma ORM 的工作方式與此根本不同。使用 Prisma ORM,您在宣告性的Prisma schema中定義模型,它作為資料庫 schema 和程式語言中模型的單一事實來源。在應用程式程式碼中,您可以使用 Prisma Client 以型別安全的方式讀取和寫入資料庫中的資料,而無需管理複雜模型例項的開銷。這使得查詢資料的過程更加自然,也更具可預測性,因為 Prisma Client 總是返回普通的 JavaScript 物件。
在本文中,您將更詳細地瞭解 ORM 模式和工作流程、Prisma ORM 如何實現資料對映器模式以及 Prisma ORM 方法的優點。
什麼是 ORM?
如果您已經熟悉 ORM,請隨時跳轉到關於 Prisma ORM 的下一節。
ORM 模式 - Active Record 和 Data Mapper
ORM 提供高階資料庫抽象。它們透過物件暴露一個程式化介面,用於建立、讀取、刪除和操作資料,同時隱藏了資料庫的一些複雜性。
ORM 的理念是,您將模型定義為類,這些類對映到資料庫中的表。這些類及其例項為您提供了程式化 API,用於讀取和寫入資料庫中的資料。
有兩種常見的 ORM 模式:Active Record 和Data Mapper,它們在如何傳輸物件和資料庫之間的資料方面有所不同。雖然這兩種模式都要求您將類定義為主要構建塊,但兩者之間最顯著的區別是,資料對映器模式將應用程式程式碼中的記憶體物件與資料庫分離,並使用資料對映器層在兩者之間傳輸資料。實際上,這意味著使用資料對映器時,記憶體中的物件(表示資料庫中的資料)甚至不知道資料庫的存在。
Active Record
Active Record ORM 將模型類對映到資料庫表,其中兩種表示的結構密切相關,例如模型類中的每個欄位都將在資料庫表中有一個匹配的列。模型類的例項封裝了資料庫行,幷包含資料和訪問邏輯,以處理資料庫中更改的持久化。此外,模型類可以包含特定於模型資料的業務邏輯。
模型類通常具有以下方法:
- 從 SQL 查詢構建模型例項。
- 構建一個新的例項,以便稍後插入表中。
- 封裝常用 SQL 查詢並返回 Active Record 物件。
- 更新資料庫並將 Active Record 中的資料插入其中。
- 獲取和設定欄位。
- 實現業務邏輯。
Data Mapper
與 Active Record 相反,Data Mapper ORM 將應用程式的記憶體中資料表示與資料庫的表示分離。透過要求您將對映責任分離到兩種型別的類中來實現解耦:
- 實體類:應用程式中實體在記憶體中的表示,它們對資料庫一無所知。
- 對映器類:它們有兩個職責:
- 在兩種表示之間轉換資料。
- 生成從資料庫獲取資料並將更改持久化到資料庫所需的 SQL。
資料對映器 ORM 允許在程式碼中實現的問題域和資料庫之間更大的靈活性。這是因為資料對映器模式允許您隱藏資料庫的實現方式,而這並非是考慮整個資料對映層背後的領域問題的理想方式。
傳統資料對映器 ORM 這樣做的原因之一是由於組織結構中,這兩個職責將由不同的團隊處理,例如DBA和後端開發人員。
實際上,並非所有資料對映器 ORM 都嚴格遵守此模式。例如,TypeORM,一個在 TypeScript 生態系統中流行的 ORM,它支援 Active Record 和 Data Mapper,採用以下資料對映器方法:
- 實體類使用裝飾器(
@Column)將類屬性對映到表列,並瞭解資料庫。 - 不使用對映器類,而是使用儲存庫類進行資料庫查詢,並可能包含自定義查詢。儲存庫使用裝飾器來確定實體屬性和資料庫列之間的對映。
給定資料庫中以下 User 表:
相應的實體類將如下所示:
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ name: 'first_name' })
firstName: string
@Column({ name: 'last_name' })
lastName: string
@Column({ unique: true })
email: string
}
Schema 遷移工作流程
開發使用資料庫的應用程式的核心部分是更改資料庫 schema 以適應新功能並更好地適應您正在解決的問題。在本節中,我們將討論schema 遷移是什麼以及它們如何影響工作流程。
由於 ORM 位於開發人員和資料庫之間,大多數 ORM 都提供一個遷移工具來協助建立和修改資料庫 schema。
遷移是將資料庫 schema 從一種狀態轉換到另一種狀態的一系列步驟。第一次遷移通常會建立表和索引。隨後的遷移可能會新增或刪除列、引入新索引或建立新表。根據遷移工具的不同,遷移可能採用 SQL 語句的形式,也可能是將被轉換為 SQL 語句的程式程式碼(如ActiveRecord和SQLAlchemy)。
由於資料庫通常包含資料,因此遷移有助於您將 schema 更改分解為更小的單元,從而有助於避免意外資料丟失。
假設您從頭開始一個專案,那麼完整的工作流程將是:您建立一個遷移,將在資料庫 schema 中建立 User 表,並定義 User 實體類,如上例所示。
然後,隨著專案的進展,當您決定要向 User 表新增一個新的 salutation 列時,您將建立另一個遷移,該遷移將修改表並新增 salutation 列。
讓我們看看 TypeORM 遷移會是什麼樣子:
import { MigrationInterface, QueryRunner } from 'typeorm'
export class UserRefactoring1604448000 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" ADD COLUMN "salutation" TEXT`)
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "salutation"`)
}
}
一旦執行了遷移並且資料庫 schema 已更改,實體類和對映器類也必須更新以適應新的 salutation 列。
使用 TypeORM,這意味著向 User 實體類新增一個 salutation 屬性:
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ name: 'first_name' })
firstName: string
@Column({ name: 'last_name' })
lastName: string
@Column({ unique: true })
email: string
@Column()
salutation: string
}
由於這些更改是手動應用的,並且無法透過程式輕鬆驗證,因此使用 ORM 同步此類更改可能是一個挑戰。重新命名現有列可能更加繁瑣,並且涉及搜尋和替換對該列的引用。
注意: Django 的makemigrations CLI 透過檢查模型中的更改來生成遷移,這與 Prisma ORM 類似,消除了同步問題。
總而言之,schema 演進是構建應用程式的關鍵部分。使用 ORM,更新 schema 的工作流程涉及使用遷移工具建立遷移,然後更新相應的實體和對映器類(取決於實現)。正如您將看到的,Prisma ORM 對此採取了不同的方法。
現在您已經瞭解了遷移是什麼以及它們如何適應開發工作流程,您將進一步瞭解 ORM 的優點和缺點。
ORM 的優點
開發人員選擇使用 ORM 的原因有很多:
- ORM 有助於實現領域模型。領域模型是一個物件模型,它包含了您的業務邏輯的行為和資料。換句話說,它允許您專注於實際業務概念,而不是資料庫結構或 SQL 語義。
- ORM 有助於減少程式碼量。它們使您免於為常見的 CRUD(建立、讀取、更新、刪除)操作編寫重複的 SQL 語句,並轉義使用者輸入以防止 SQL 注入等漏洞。
- ORM 要求您編寫很少或不編寫 SQL(根據您的複雜性,您可能仍然需要編寫一些原始查詢)。這對於不熟悉 SQL 但仍想使用資料庫的開發人員來說是有益的。
- 許多 ORM 抽象了資料庫特定的細節。理論上,這意味著 ORM 可以使從一個數據庫更改到另一個數據庫變得更容易。應該注意的是,在實踐中,應用程式很少更改它們使用的資料庫。
與所有旨在提高生產力的抽象一樣,使用 ORM 也有缺點。
ORM 的缺點
ORM 的缺點在使用它們時並不總是顯而易見的。本節涵蓋了一些公認的缺點:
- 使用 ORM,您會形成資料庫表的物件圖表示,這可能導致物件-關係阻抗不匹配。當您正在解決的問題形成一個複雜的物件圖,而該物件圖無法簡單地對映到關係資料庫時,就會發生這種情況。在關係資料庫中的資料表示與記憶體中的(物件)資料表示之間進行同步是相當困難的。這是因為與關係資料庫記錄相比,物件在它們相互關聯的方式上更加靈活和多樣化。
- 雖然 ORM 處理了與問題相關的複雜性,但同步問題並沒有消失。資料庫 schema 或資料模型的任何更改都需要將更改映射回另一側。這種負擔通常落在開發人員身上。在團隊合作的專案中,資料庫 schema 更改需要協調。
- ORM 往往具有較大的 API 表面積,因為它們封裝了複雜性。不必編寫 SQL 的反面是您需要花費大量時間學習如何使用 ORM。這適用於大多數抽象,但是如果不瞭解資料庫的工作原理,就很難改進慢查詢。
- 由於 SQL 提供的靈活性,某些複雜查詢不受 ORM 支援。此問題透過原始 SQL 查詢功能得到緩解,在該功能中,您將 SQL 語句字串傳遞給 ORM,併為您執行查詢。
現在已經介紹了 ORM 的成本和優點,您可以更好地理解 Prisma ORM 是什麼以及它如何適應。
Prisma ORM
Prisma ORM 是下一代 ORM,它使應用程式開發人員輕鬆使用資料庫,並具有以下工具:
- Prisma Client:自動生成且型別安全的資料庫客戶端,用於您的應用程式。
- Prisma Migrate:一個宣告式資料建模和遷移工具。
- Prisma Studio:一個現代 GUI,用於瀏覽和管理資料庫中的資料。
注意:由於 Prisma Client 是最突出的工具,我們通常簡稱為 Prisma。
這三個工具使用Prisma schema作為資料庫 schema、應用程式物件 schema 以及兩者之間對映的單一事實來源。它由您定義,是您配置 Prisma ORM 的主要方式。
Prisma ORM 透過型別安全、豐富的自動補全和用於獲取關係的自然 API 等功能,使您在構建軟體時高效且自信。
在下一節中,您將瞭解 Prisma ORM 如何實現資料對映器模式。
Prisma ORM 如何實現資料對映器模式
如本文前面所述,資料對映器模式與資料庫和應用程式由不同團隊擁有的組織非常契合。
隨著現代雲環境、託管資料庫服務和 DevOps 實踐的興起,越來越多的團隊採用跨職能方法,即團隊擁有包括資料庫和操作問題在內的完整開發週期。
Prisma ORM 實現了 DB schema 和物件 schema 的同步演進,從而從根本上減少了偏差的需求,同時仍然允許您使用 @map 屬性將應用程式和資料庫保持一定的解耦。雖然這可能看起來像一個限制,但它阻止了領域模型(透過物件 schema)的演進在事後強加給資料庫。
為了理解 Prisma ORM 對資料對映器模式的實現與傳統資料對映器 ORM 在概念上如何不同,這裡是它們的概念和構建塊的簡要比較:
| 概念 | 描述 | 傳統 ORM 中的構建塊 | Prisma ORM 中的構建塊 | Prisma ORM 中的事實來源 |
|---|---|---|---|---|
| 物件 Schema | 應用程式中的記憶體資料結構 | 模型類 | 生成的 TypeScript 型別 | Prisma schema 中的模型 |
| 資料對映器 | 在物件 Schema 和資料庫之間進行轉換的程式碼 | 對映器類 | Prisma Client 中生成的函式 | Prisma schema 中的 @map 屬性 |
| 資料庫 Schema | 資料庫中的資料結構,例如表和列 | 手動編寫或使用程式化 API 編寫的 SQL | Prisma Migrate 生成的 SQL | Prisma schema |
Prisma ORM 與資料對映器模式一致,並具有以下額外優勢:
- 透過根據 Prisma schema 生成 Prisma Client,減少定義類和對映邏輯的樣板程式碼。
- 消除了應用程式物件和資料庫 schema 之間的同步挑戰。
- 資料庫遷移作為一流公民,因為它們是從 Prisma schema 派生的。
現在我們已經討論了 Prisma ORM 資料對映器方法的概念,我們可以瞭解 Prisma schema 在實踐中如何工作。
Prisma schema
Prisma 實現資料對映器模式的核心是 Prisma schema——以下職責的單一事實來源:
- 配置 Prisma 如何連線到您的資料庫。
- 生成 Prisma Client——用於應用程式程式碼的型別安全 ORM。
- 使用 Prisma Migrate 建立和演進資料庫 schema。
- 定義應用程式物件和資料庫列之間的對映。
Prisma ORM 中的模型與 Active Record ORM 中的含義略有不同。在 Prisma ORM 中,模型在 Prisma schema 中定義為抽象實體,它們描述了表、關係以及列與 Prisma Client 中屬性之間的對映。
例如,這是一個部落格的 Prisma schema:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String? @map("post_content")
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
以下是上述示例的細分:
datasource塊定義了與資料庫的連線。generator塊告訴 Prisma ORM 為 TypeScript 和 Node.js 生成 Prisma Client。Post和User模型對映到資料庫表。- 這兩個模型具有1-n關係,其中每個
User可以有多個相關的Post。 - 模型中的每個欄位都有一個型別,例如
id的型別為Int。 - 欄位可能包含欄位屬性以定義:
- 帶
@id屬性的主鍵。 - 帶
@unique屬性的唯一鍵。 - 帶
@default屬性的預設值。 - 表列和 Prisma Client 欄位之間的對映,帶
@map屬性,例如content欄位(將在 Prisma Client 中可訪問)對映到post_content資料庫列。
- 帶
User / Post 關係可以用下圖視覺化:

在 Prisma ORM 層面,User / Post 關係由以下部分組成:
- 標量
authorId欄位,由@relation屬性引用。此欄位存在於資料庫表中——它是連線 Post 和 User 的外部索引鍵。 - 兩個關係欄位:
author和posts不存在於資料庫表中。關係欄位在 Prisma ORM 級別定義模型之間的連線,僅存在於 Prisma schema 和生成的 Prisma Client 中,用於訪問關係。
Prisma schema 的宣告性本質簡潔,允許定義資料庫 schema 和 Prisma Client 中的相應表示。
在下一節中,您將瞭解 Prisma ORM 支援的工作流程。
Prisma ORM 工作流程
Prisma ORM 的工作流程與傳統 ORM 略有不同。您可以在從頭開始構建新應用程式時使用 Prisma ORM,也可以逐步採用它:
- 新應用程式(綠地專案):尚未有資料庫 schema 的專案可以使用 Prisma Migrate 來建立資料庫 schema。
- 現有應用程式(棕地專案):已經有資料庫 schema 的專案可以透過 Prisma ORM 進行內省,以生成反映現有資料庫 schema 的 Prisma schema 和 Prisma Client。此用例適用於任何現有的遷移工具,並且對於逐步採用很有用。可以切換到 Prisma Migrate 作為遷移工具。但是,這是可選的。
在這兩種工作流程中,Prisma schema 都是主要的配置檔案。
現有資料庫專案中逐步採用的工作流程
棕地專案通常已經有一些資料庫抽象和 schema。Prisma ORM 可以透過內省現有資料庫來生成反映現有資料庫 schema 的 Prisma schema 和 Prisma Client,從而與此類專案整合。此工作流程與您可能已在使用的任何遷移工具和 ORM 相容。如果您更喜歡逐步評估和採用,此方法可以用作並行採用策略的一部分。
與此工作流程相容的非窮舉列表:
- 使用純 SQL 檔案(帶
CREATE TABLE和ALTER TABLE)建立和更改資料庫 schema 的專案。 - 使用第三方遷移庫的專案,例如 db-migrate 或 Umzug。
- 已經使用 ORM 的專案。在這種情況下,透過 ORM 進行的資料庫訪問保持不變,而生成的 Prisma Client 可以逐步採用。
實際上,內省現有資料庫並生成 Prisma Client 所需的步驟如下:
- 建立定義
datasource(在本例中為您的現有資料庫)和generator的schema.prisma:
datasource db {
provider = "postgresql"
url = "postgresql://janedoe:janedoe@localhost:5432/hello-prisma"
}
generator client {
provider = "prisma-client-js"
}
- 執行
prisma db pull以使用從資料庫 schema 派生的模型填充 Prisma schema。 - (可選)自定義 Prisma Client 和資料庫之間的欄位和模型對映。
- 執行
prisma generate。
Prisma ORM 將在 node_modules 資料夾中生成 Prisma Client,您可以從中將其匯入到您的應用程式中。有關更廣泛的使用文件,請參閱 Prisma Client API 文件。
總而言之,Prisma Client 可以作為並行採用策略的一部分整合到現有資料庫和工具的專案中。新專案將使用接下來詳細介紹的不同工作流程。
新專案的工作流程
Prisma ORM 在其支援的工作流程方面與 ORM 不同。仔細檢視建立和更改新資料庫 schema 所需的步驟有助於理解 Prisma Migrate。
Prisma Migrate 是一個用於宣告式資料建模和遷移的 CLI。與大多數作為 ORM 一部分的遷移工具不同,您只需要描述當前 schema,而不是從一個狀態移動到另一個狀態的操作。Prisma Migrate 推斷操作,生成 SQL 併為您執行遷移。
此示例演示了在新專案中使用 Prisma ORM 和新資料庫 schema(類似於上面的部落格示例)的情況:
- 建立 Prisma schema:
// schema.prisma
datasource db {
provider = "postgresql"
url = "postgresql://janedoe:janedoe@localhost:5432/hello-prisma"
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String? @map("post_content")
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
- 執行
prisma migrate以生成遷移的 SQL,將其應用於資料庫,並生成 Prisma Client。
對於資料庫 schema 的任何進一步更改:
- 將更改應用到 Prisma schema,例如,向
User模型新增registrationDate欄位: - 再次執行
prisma migrate。
最後一步演示了宣告式遷移如何透過向 Prisma schema 新增欄位並使用 Prisma Migrate 將資料庫 schema 轉換為所需狀態來工作。遷移執行後,Prisma Client 會自動重新生成,以便它反映更新的 schema。
如果您不想使用 Prisma Migrate 但仍想在新專案中使用型別安全的生成的 Prisma Client,請參閱下一節。
沒有 Prisma Migrate 的新專案的替代方案
在新專案中使用 Prisma Client 而不使用 Prisma Migrate 的情況下,可以使用第三方遷移工具。例如,一個新專案可以選擇使用 Node.js 遷移框架 db-migrate 來建立資料庫 schema 和遷移,並使用 Prisma Client 進行查詢。本質上,這已包含在現有資料庫的工作流程中。
使用 Prisma Client 訪問資料
到目前為止,本文涵蓋了 Prisma ORM 的概念、其資料對映器模式的實現以及它支援的工作流程。在本節的最後,您將瞭解如何使用 Prisma Client 訪問應用程式中的資料。
使用 Prisma Client 訪問資料庫是透過其公開的查詢方法實現的。所有查詢都返回普通 JavaScript 物件。給定上述部落格 schema,獲取使用者如下所示:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const user = await prisma.user.findUnique({
where: {
email: 'alice@prisma.io',
},
})
在此查詢中,findUnique() 方法用於從 User 表中獲取單行。預設情況下,Prisma ORM 將返回 User 表中的所有標量欄位。
注意:該示例使用 TypeScript 以充分利用 Prisma Client 提供的型別安全功能。但是,Prisma ORM 也適用於 Node.js 中的 JavaScript。
Prisma Client 透過從 Prisma schema 生成程式碼,將查詢和結果對映到結構型別。這意味著 user 在生成的 Prisma Client 中具有關聯型別:
export type User = {
id: number
email: string
name: string | null
}
這確保訪問不存在的欄位將引發型別錯誤。更廣泛地說,這意味著每個查詢的結果型別在執行查詢之前都是已知的,這有助於捕獲錯誤。例如,以下程式碼片段將引發型別錯誤:
console.log(user.lastName) // Property 'lastName' does not exist on type 'User'.
獲取關係
使用 Prisma Client 獲取關係是透過 include 選項完成的。例如,獲取使用者及其帖子將如下所示:
const user = await prisma.user.findUnique({
where: {
email: 'alice@prisma.io',
},
include: {
posts: true,
},
})
使用此查詢,user 的型別也將包括 Post,可以透過 posts 陣列欄位訪問:
console.log(user.posts[0].title)
該示例僅觸及 Prisma Client 用於CRUD 操作的 API 的皮毛,您可以在文件中瞭解更多資訊。主要思想是所有查詢和結果都由型別支援,並且您可以完全控制如何獲取關係。
結論
總而言之,Prisma ORM 是一種新型資料對映器 ORM,它與傳統 ORM 不同,並且不會遇到通常與它們相關的問題。
與傳統 ORM 不同,使用 Prisma ORM,您可以定義 Prisma schema——一個宣告性的資料庫 schema 和應用程式模型的單一事實來源。Prisma Client 中的所有查詢都返回普通的 JavaScript 物件,這使得與資料庫互動的過程更加自然且更具可預測性。
Prisma ORM 支援兩種主要工作流程:啟動新專案和在現有專案中採用。對於這兩種工作流程,您的主要配置途徑都是透過 Prisma schema。
像所有抽象一樣,Prisma ORM 和其他 ORM 都隱藏了一些資料庫的底層細節,並帶有不同的假設。
這些差異和您的用例都會影響工作流程和採用成本。希望瞭解它們的區別可以幫助您做出明智的決定。