精讀《Prisma 的使用》


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,
      },
    },
  },
})

篩選條件支持 equalsnotinnotInltltegtgtecontainssearchmodestartsWithendsWithANDORNOT,一般用法如下:

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 的一大特色,因為這部分描述獨立於代碼,帶來了如下幾個好處:

  1. 定義比 Node Class 更簡潔。
  2. 不生成冗余的代碼結構。
  3. 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM