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 是在最大程度貼近數據庫結構描述的基礎上,對關聯關系進行了進一步抽象,並且背后維護了與數據模型的對應關系,下圖很好的說明了這一點:
可以看到,幾乎與數據庫的定義一模一樣,唯一多出來的 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 查詢。
轉自https://mp.weixin.qq.com/s/DJ_yzSsSkLf0r-lfP6ZSmQ