ORM(Object relational mappers) 的含義是,將資料模型與 Object 建立強力的對映關係,這樣我們對資料的增刪改查可以轉換為操作 Object(物件)。
Prisma 是一個現代 Nodejs ORM 庫,根據 Prisma 官方文件 可以瞭解這個庫是如何設計與使用的。
概述
Prisma 提供了大量工具,包括 Prisma Schema、Prisma Client、Prisma Migrate、Prisma CLI、Prisma Studio 等,其中最核心的兩個是 Prisma Schema 與 Prisma Client,分別是描述應用資料模型與 Node 操作 API。
與一般 ORM 完全由 Class 描述資料模型不同,Primsa 採用了一個全新語法 Primsa Schema 描述資料模型,再執行 prisma generate
產生一個配置檔案儲存在 node_modules/.prisma/client
中,Node 程式碼裡就可以使用 Prisma Client 對資料增刪改查了。
Prisma Schema
Primsa Schema 是在最大程度貼近資料庫結構描述的基礎上,對關聯關係進行了進一步抽象,並且背後維護了與資料模型的對應關係,下圖很好的說明了這一點:
<img width=400 src="https://z3.ax1x.com/2021/10/17/5YwZoF.png">
可以看到,幾乎與資料庫的定義一模一樣,唯一多出來的 posts
與 author
其實是彌補了資料庫表關聯外來鍵中不直觀的部分,將這些外來鍵轉化為實體物件,讓操作時感受不到外來鍵或者多表的存在,在具體操作時再轉化為 join 操作。下面是對應的 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 db
申明瞭連結資料庫資訊;generator client
申明瞭使用 Prisma Client 進行客戶端操作,也就是說 Prisma Client 其實是可以替換實現的;model
是最核心的模型定義。
在模型定義中,可以通過 @map
修改欄位名對映、@@map
修改表名對映,預設情況下,欄位名與 key 名相同:
model Comment {
title @map("comment_title")
@@map("comments")
}
欄位由下面四種描述組成:
- 欄位名。
- 欄位型別。
- 可選的型別修飾。
- 可選的屬性描述。
model Tag {
name String? @id
}
在這個描述裡,包含欄位名 name
、欄位型別 String
、型別修飾 ?
、屬性描述 @id
。
欄位型別
欄位型別可以是 model,比如關聯型別欄位場景:
model Post {
id Int @id @default(autoincrement())
// Other fields
comments Comment[] // A post can have many comments
}
model Comment {
id Int
// Other fields
Post Post? @relation(fields: [postId], references: [id]) // A comment can have one post
postId Int?
}
關聯場景有 1v1, nv1, 1vn, nvn 四種情況,欄位型別可以為定義的 model 名稱,並使用屬性描述 @relation
定義關聯關係,比如上面的例子,描述了 Commenct
與 Post
存在 nv1 關係,並且 Comment.postId
與 Post.id
關聯。
欄位型別還可以是底層資料型別,通過 @db.
描述,比如:
model Post {
id @db.TinyInt(1)
}
對於 Prisma 不支援的型別,還可以使用 Unsupported
修飾:
model Post {
someField Unsupported("polygon")?
}
這種型別的欄位無法通過 ORM API 查詢,但可以通過 queryRaw
方式查詢。queryRaw
是一種 ORM 對原始 SQL 模式的支援,在 Prisma Client 會提到。
型別修飾
型別修飾有 ?
[]
兩種語法,比如:
model User {
name String?
posts Post[]
}
分別表示可選與陣列。
屬性描述
屬性描述有如下幾種語法:
model User {
id Int @id @default(autoincrement())
isAdmin Boolean @default(false)
email String @unique
@@unique([firstName, lastName])
}
@id
對應資料庫的 PRIMARY KEY。
@default
設定欄位預設值,可以聯合函式使用,比如 @default(autoincrement())
,可用函式包括 autoincrement()
、dbgenerated()
、cuid()
、uuid()
、now()
,還可以通過 dbgenerated
直接呼叫資料庫底層的函式,比如 dbgenerated("gen_random_uuid()")
。
@unique
設定欄位值唯一。
@relation
設定關聯,上面已經提到過了。
@map
設定對映,上面也提到過了。
@updatedAt
修飾欄位用來儲存上次更新時間,一般是資料庫自帶的能力。
@ignore
對 Prisma 標記無效的欄位。
所有屬性描述都可以組合使用,並且還存在需對 model 級別的描述,一般用兩個 @
描述,包括 @@id
、@@unique
、@@index
、@@map
、@@ignore
。
ManyToMany
Prisma 在多對多關聯關係的描述上也下了功夫,支援隱式關聯描述:
model Post {
id Int @id @default(autoincrement())
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
posts Post[]
}
看上去很自然,但其實背後隱藏了不少實現。資料庫多對多關係一般通過第三張表實現,第三張表會儲存兩張表之間外來鍵對應關係,所以如果要顯式定義其實是這樣的:
model Post {
id Int @id @default(autoincrement())
categories CategoriesOnPosts[]
}
model Category {
id Int @id @default(autoincrement())
posts CategoriesOnPosts[]
}
model CategoriesOnPosts {
post Post @relation(fields: [postId], references: [id])
postId Int // relation scalar field (used in the `@relation` attribute above)
category Category @relation(fields: [categoryId], references: [id])
categoryId Int // relation scalar field (used in the `@relation` attribute above)
assignedAt DateTime @default(now())
assignedBy String
@@id([postId, categoryId])
}
背後生成如下 SQL:
CREATE TABLE "Category" (
id SERIAL PRIMARY KEY
);
CREATE TABLE "Post" (
id SERIAL PRIMARY KEY
);
-- Relation table + indexes -------------------------------------------------------
CREATE TABLE "CategoryToPost" (
"categoryId" integer NOT NULL,
"postId" integer NOT NULL,
"assignedBy" text NOT NULL
"assignedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY ("categoryId") REFERENCES "Category"(id),
FOREIGN KEY ("postId") REFERENCES "Post"(id)
);
CREATE UNIQUE INDEX "CategoryToPost_category_post_unique" ON "CategoryToPost"("categoryId" int4_ops,"postId" int4_ops);
Prisma Client
描述好 Prisma Model 後,執行 prisma generate
,再利用 npm install @prisma/client
安裝好 Node 包後,就可以在程式碼裡操作 ORM 了:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
CRUD
使用 create
建立一條記錄:
const user = await prisma.user.create({
data: {
email: 'elsa@prisma.io',
name: 'Elsa Prisma',
},
})
使用 createMany
建立多條記錄:
const createMany = await prisma.user.createMany({
data: [
{ name: 'Bob', email: 'bob@prisma.io' },
{ name: 'Bobo', email: 'bob@prisma.io' }, // Duplicate unique key!
{ name: 'Yewande', email: 'yewande@prisma.io' },
{ name: 'Angelique', email: 'angelique@prisma.io' },
],
skipDuplicates: true, // Skip 'Bobo'
})
使用 findUnique
查詢單條記錄:
const user = await prisma.user.findUnique({
where: {
email: 'elsa@prisma.io',
},
})
對於聯合索引的情況:
model TimePeriod {
year Int
quarter Int
total Decimal
@@id([year, quarter])
}
需要再巢狀一層由 _
拼接的 key:
const timePeriod = await prisma.timePeriod.findUnique({
where: {
year_quarter: {
quarter: 4,
year: 2020,
},
},
})
使用 findMany
查詢多條記錄:
const users = await prisma.user.findMany()
可以使用 SQL 中各種條件語句,語法如下:
const users = await prisma.user.findMany({
where: {
role: 'ADMIN',
},
include: {
posts: true,
},
})
使用 update
更新記錄:
const updateUser = await prisma.user.update({
where: {
email: 'viola@prisma.io',
},
data: {
name: 'Viola the Magnificent',
},
})
使用 updateMany
更新多條記錄:
const updateUsers = await prisma.user.updateMany({
where: {
email: {
contains: 'prisma.io',
},
},
data: {
role: 'ADMIN',
},
})
使用 delete
刪除記錄:
const deleteUser = await prisma.user.delete({
where: {
email: 'bert@prisma.io',
},
})
使用 deleteMany
刪除多條記錄:
const deleteUsers = await prisma.user.deleteMany({
where: {
email: {
contains: 'prisma.io',
},
},
})
使用 include
表示關聯查詢是否生效,比如:
const getUser = await prisma.user.findUnique({
where: {
id: 19,
},
include: {
posts: true,
},
})
這樣就會在查詢 user
表時,順帶查詢所有關聯的 post
表。關聯查詢也支援巢狀:
const user = await prisma.user.findMany({
include: {
posts: {
include: {
categories: true,
},
},
},
})
篩選條件支援 equals
、not
、in
、notIn
、lt
、lte
、gt
、gte
、contains
、search
、mode
、startsWith
、endsWith
、AND
、OR
、NOT
,一般用法如下:
const result = await prisma.user.findMany({
where: {
name: {
equals: 'Eleanor',
},
},
})
這個語句代替 sql 的 where name="Eleanor"
,即通過物件巢狀的方式表達語義。
Prisma 也可以直接寫原生 SQL:
const email = 'emelie@prisma.io'
const result = await prisma.$queryRaw(
Prisma.sql`SELECT * FROM User WHERE email = ${email}`
)
中介軟體
Prisma 支援中介軟體的方式在執行過程中進行擴充,看下面的例子:
const prisma = new PrismaClient()
// Middleware 1
prisma.$use(async (params, next) => {
console.log(params.args.data.title)
console.log('1')
const result = await next(params)
console.log('6')
return result
})
// Middleware 2
prisma.$use(async (params, next) => {
console.log('2')
const result = await next(params)
console.log('5')
return result
})
// Middleware 3
prisma.$use(async (params, next) => {
console.log('3')
const result = await next(params)
console.log('4')
return result
})
const create = await prisma.post.create({
data: {
title: 'Welcome to Prisma Day 2020',
},
})
const create2 = await prisma.post.create({
data: {
title: 'How to Prisma!',
},
})
輸出如下:
Welcome to Prisma Day 2020
1
2
3
4
5
6
How to Prisma!
1
2
3
4
5
6
可以看到,中介軟體執行順序是洋蔥模型,並且每個操作都會觸發。我們可以利用中介軟體擴充業務邏輯或者進行操作時間的打點記錄。
精讀
ORM 的兩種設計模式
ORM 有 Active Record 與 Data Mapper 兩種設計模式,其中 Active Record 使物件背後完全對應 sql 查詢,現在已經不怎麼流行了,而 Data Mapper 模式中的物件並不知道資料庫的存在,即中間多了一層對映,甚至背後不需要對應資料庫,所以可以做一些很輕量的除錯功能。
Prisma 採用了 Data Mapper 模式。
ORM 容易引發效能問題
當資料量大,或者效能、資源敏感的情況下,我們需要對 SQL 進行優化,甚至我們需要對特定的 Mysql 的特定版本的某些核心錯誤,對 SQL 進行某些看似無意義的申明調優(比如在 where 之前再進行相同條件的 IN 範圍限定),有的時候能取得驚人的效能提升。
而 ORM 是建立在一個較為理想化理論基礎上的,即資料模型可以很好的轉化為物件操作,然而物件操作由於遮蔽了細節,我們無法對 SQL 進行鍼對性調優。
另外,得益於物件操作的便利性,我們很容易通過 obj.obj. 的方式訪問某些屬性,但這背後生成的卻是一系列未經優化(或者部分自動優化)的複雜 join sql,我們在寫這些 sql 時會提前考慮效能因素,但通過物件呼叫時卻因為成本低,或覺得 ORM 有 magic 優化等想法,寫出很多實際上不合理的 sql。
Prisma Schema 的好處
其實從語法上,Prisma Schema 與 Typeorm 基於 Class + 裝飾器的擴充幾乎可以等價轉換,但 Prisma Schema 在實際使用中有一個很不錯的優勢,即減少樣板程式碼以及穩定資料庫模型。
減少樣板程式碼比較好理解,因為 Prisma Schema 並不會出現在程式碼中,而穩定模型是指,只要不執行 prisma generate
,資料模型就不會變化,而且 Prisma Schema 也獨立於 Node 存在,甚至可以不放在專案原始碼中,相比之下,修改起來會更加慎重,而完全用 Node 定義的模型因為本身是程式碼的一部分,可能會突然被修改,而且也沒有執行資料庫結構同步的操作。
如果專案採用 Prisma,則模型變更後,可以執行 prisma db pull
更新資料庫結構,再執行 prisma generate
更新客戶端 API,這個流程比較清晰。
總結
Prisma Schema 是 Prisma 的一大特色,因為這部分描述獨立於程式碼,帶來了如下幾個好處:
- 定義比 Node Class 更簡潔。
- 不生成冗餘的程式碼結構。
- Prisma Client 更加輕量,且查詢返回的都是 Pure Object。
至於 Prisma Client 的 API 設計其實並沒有特別突出之處,無論與 sequelize 還是 typeorm 的 API 設計相比,都沒有太大的優化,只是風格不同。
不過對於記錄的建立,我更喜歡 Prisma 的 API:
// typeorm - save API
const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)
// typeorm - insert API
const userRepository = getManager().getRepository(User)
userRepository.insert({
name: 'Alice',
})
// sequelize
const user = User.build({
name: 'Alice',
})
await user.save()
// Mongoose
const user = await User.create({
name: 'Alice',
email: 'alice@prisma.io',
})
// prisma
const newUser = await prisma.user.create({
data: {
name: 'Alice',
},
})
首先存在 prisma
這個頂層變數,使用起來會非常方便,另外從 API 擴充上來說,雖然 Mongoose 設計得更簡潔,但新增一些條件時擴充性會不足,導致結構不太穩定,不利於統一記憶。
Prisma Client 的 API 統一採用下面這種結構:
await prisma.modelName.operateName({
// 資料,比如 create、update 時會用到
data: /** ... */,
// 條件,大部分情況都可以用到
where: /** ... */,
// 其它特殊引數,或者 operater 特有的引數
})
所以總的來說,Prisma 雖然沒有對 ORM 做出革命性改變,但在微創新與 API 優化上都做得足夠棒,github 更新也比較活躍,如果你決定使用 ORM 開發專案,還是比較推薦 Prisma 的。
在實際使用中,為了規避 ORM 產生笨拙 sql 導致的效能問題,可以利用 Prisma Middleware 監控查詢效能,並對效能較差的地方採用 prisma.$queryRaw
原生 sql 查詢。
討論地址是:精讀《Prisma 的使用》· Issue #362 · dt-fe/weekly
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)