前言
本篇文章將會介紹一個NodeJS社區中的ORM:Prisma。我接觸它的時間不算長,但已經對它的未來發展充滿信心。這篇文章其實三個月以前就寫了一部分,所以文中會出現“如果你覺得它不錯,不如考慮基於Prisma來完成你的畢設”這樣的話。
在剛開始寫的時候, bven爺的畢設一行都還沒動,而到了我今天發的時候,他已經是優秀畢業生了...
同時,原本准備一篇搞定所有內容,但是覺得這種教程類的文章如果寫的這么長,很難讓人有讀完的興致。所以就拆成了兩部分:
- 第一部分主要是鋪墊,介紹目前NodeJS社區比較主流的ORM與Query Builder,以及Prisma的簡單使用,這一部分主要是為接觸ORM較少的同學做一個基礎知識的鋪墊。
- 第二部分包括Prisma的花式進階使用,包括多表級聯、多數據庫協作以及與GraphQL的實戰,最后會展開來聊一聊Prisma的未來。
文章的大致順序如下:
- NodeJS社區中的老牌、傳統ORM
- 傳統ORM的Data Mapper 與 Active Record模式
- Query Builder
- Prisma的基礎環境配置
- Hello Prisma
- 從單表CRUD開始
- 多表、多數據庫實戰
- Prisma與GraphQL:全鏈路類型安全
- Prisma與一體化框架
NodeJS社區中的ORM
經常寫Node應用的同學通常免不了要和ORM打交道,畢竟寫原生SQL對於大部分前端同學來說真的是一種折磨。ORM的便利性使得很多情況下我們能直觀而方便的和數據庫打交道(雖然的確有些情況下ORM搞不定),用我們熟悉的JavaScript來花式操作數據庫。 NodeJS社區中主流的ORM主要有這么幾個,它們都有各自的一些特色:
- Sequelize,比較老牌的一款ORM,缺點是TS支持不太好,但是社區有Sequelize-TypeScript。
Sequelize定義表結構的方式是這樣的: - ``
typescript
const { Sequelize, Model, DataTypes } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:');
class User extends Model {}
User.init({
username: DataTypes.STRING,
birthday: DataTypes.DATE
}, { sequelize, modelName: 'user' });
(async () => {
await sequelize.sync();
const jane = await User.create({
username: 'janedoe',
birthday: new Date(1980, 6, 20)
});
console.log(jane.toJSON());
})();
```
(我是覺得不那么符合直覺,所以我只在入門時期簡單使用過) - TypeORM,NodeJS社區star最多的一個ORM。也確實很好用,在我周圍的同學里備受好評,同時也是我自己用的最多的一個ORM。亮點在基於裝飾器語法聲明表結構、事務、級聯等,以及很棒的TS支持。
TypeORM聲明表結構是這樣的: - `
typescript
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
age: number;
}
比起Sequelize來要直觀的多,而且由於通過類屬性的方式來定義數據庫字段,可以很好的兼容Mixin以及其他基於類屬性的工具庫,如TypeGraphQL。 - MikroORM,比較新的一個ORM,同樣大量基於裝飾器語法,亮點在於自動處理所有事務以及表實體會在全局保持單例模式,暫時還沒深入使用過過。
MikroORM定義表結構方式是這樣的: - `
typescript
@Entity()
export class Book extends BaseEntity {
@Property()
title!: string;
@ManyToOne()
author!: Author;
@ManyToOne()
publisher?: IdentifiedReference<Publisher>;
@ManyToMany({ fixedOrder: true })
tags = new Collection<BookTag>(this);
}
- Mongoose、Typegoose,MongoDB專用的ORM,這里簡單放一下TypeGoose的使用示例:
- `
typescript
import { prop, getModelForClass } from '@typegoose/typegoose';
import * as mongoose from 'mongoose';
class User {
@prop()
public name?: string;
@prop({ type: () => [String] })
public jobs?: string[];
}
const UserModel = getModelForClass(User);
(async () => {
await mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true, dbName: 'test' });
const { _id: id } = await UserModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User);
const user = await UserModel.findById(id).exec();
console.log(user);
})();
- Bookshelf,一個相對簡單一些但也五臟俱全的ORM,基於Knex(Strapi底層的Query Builder,后面會簡單介紹)。它的使用方式大概是這樣的:
- `
typescript
const knex = require('knex')({
client: 'mysql',
connection: process.env.MYSQL_DATABASE_CONNECTION
})
// bookshelf 基於 knex,所以需要實例化knex然后傳入
const bookshelf = require('bookshelf')(knex)
const User = bookshelf.model('User', {
tableName: 'users',
posts() {
return this.hasMany(Posts)
}
})
const Post = bookshelf.model('Post', {
tableName: 'posts',
tags() {
return this.belongsToMany(Tag)
}
})
const Tag = bookshelf.model('Tag', {
tableName: 'tags'
})
new User({id: 1}).fetch({withRelated: ['posts.tags']}).then((user) => {
console.log(user.related('posts').toJSON())
}).catch((error) => {
console.error(error)
})
另外,一個比較獨特的地方是bookshelf支持了插件機制,其他ORM通常通過hook或者subscriber的方式實現類似的功能,如密碼存入時進行一次加密、TPS計算、等。
ORM的Data Mapper與Actice Record模式
如果你去看了上面列舉的ORM文檔,你會發現MikroORM的簡介中包含這么一句話:TypeScript ORM for Node.js based on Data Mapper
,而TypeORM的簡介中則是TypeORM supports both Active Record and Data Mapper patterns
。
先來一個問題,使用ORM的過程中,你是否了解過 Data Mapper 與 Active Record 這兩種模式的區別?
先來看看TypeORM中分別是如何使用這兩種模式的:
Active Record:
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
isActive: boolean;
}
const user = new User();
user.name = "不渡";
user.isActive = true;
await user.save();
const newUsers = await User.find({ isActive: true });
TypeORM中,Active Record模式下需要讓實體類繼承BaseEntity
類,這樣實體類上就具有了各種方法,如save
remove
find
方法等。Active Record模式最早由 Martin Fowler 在 企業級應用架構模式 一書中命名,這一模式使得對象上擁有了相關的CRUD方法。在RoR中就使用了這一模式來作為MVC中的M,即數據驅動層。如果你對RoR中的Active Record有興趣,可以閱讀 全面理解Active Record(我不會Ruby,因此就不做介紹了)。
Data Mapper:
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
isActive: boolean;
}
const userRepository = connection.getRepository(User);
const user = new User();
user.name = "不渡";
user.isActive = true;
await userRepository.save(user);
await userRepository.remove(user);
const newUsers = await userRepository.find({ isActive: true });
可以看到在Data Mapper模式中,實體類不再能夠自己進行數據庫操作,而是需要先獲取到一個對應到表的“倉庫”,然后再調用這個“倉庫”上的方法。
這一模式同樣由Martin Fowler最初命名,Data Mapper更像是一層攔在操作者與實際數據之間的訪問層,就如上面例子中先獲取具有訪問權限(即相應方法)的對象,再進行數據的操作。
對這兩個模式進行比較,很容易發現Active Record模式要簡單的多,而Data Mapper模式則更加嚴謹。那么何時使用這兩種模式就很清楚了,如果你在開發比較簡單的應用,直接使用Active Record模式就好了,因為這確實會減少很多代碼。但是如果你在開發規模較大的應用,使用Data Mapper模式則能夠幫助你更好的維護代碼(實體類不再具有訪問數據庫權限了,只能通過統一的接口(getRepository
getManager
等)),一個例子是在Nest、Midway這兩個IoC風格的Node框架中,均使用Data Mapper模式注入Repository實例,然后再進行操作。
最后,NodeJS中使用Data Mapper的ORM主要包括Bookshelf、MikroORM、objection.js以及本文主角Prisma等。
Query Builder
實際上除了ORM與原生SQL以外,還有一種常用的數據庫交互方式:Query Builder(以下簡稱QB)。
QB和ORM其實我個人覺得既有相同之處又有不同之處,但是挺容易搞混,比如 MQuery (MongoDB的一個Query Builder)的方法是這樣的:
mquery().find(match, function (err, docs) {
assert(Array.isArray(docs));
})
mquery().findOne(match, function (err, doc) {
if (doc) {
// the document may not be found
console.log(doc);
}
})
mquery().update(match, updateDocument, options, function (err, result){})
是不是看起來和ORM很像?但我們再看看其他的場景:
mquery({ name: /^match/ })
.collection(coll)
.setOptions({ multi: true })
.update({ $addToSet: { arr: 4 }}, callback)
在ORM中,通常不會存在這樣的多個方法鏈式調用,而是通過單個方法+多個參數的方式來操作,這也是Query Builder和ORM的一個重要差異。再來看看TypeORM的Query Builder模式:
import { getConnection } from "typeorm";
const user = await getConnection()
.createQueryBuilder()
.select("user")
.from(User, "user")
.where("user.id = :id", { id: 1 })
.getOne();
以上的操作其實就相當於userRepo.find({ id: 1 })
,你可能會覺得QB的寫法過於繁瑣,但實際上這種模式要靈活的多,和SQL語句的距離也要近的多(你可以理解為每一個鏈式方法調用都會對最終生成的SQL語句進行一次操作)。
同時在部分情境(如多級級聯下)中,Query Builder反而是代碼更簡潔的那一方,如:
const selectQueryBuilder = this.executorRepository
.createQueryBuilder("executor")
.leftJoinAndSelect("executor.tasks", "tasks")
.leftJoinAndSelect("executor.relatedRecord", "records")
.leftJoinAndSelect("records.recordTask", "recordTask")
.leftJoinAndSelect("records.recordAccount", "recordAccount")
.leftJoinAndSelect("records.recordSubstance", "recordSubstance")
.leftJoinAndSelect("tasks.taskSubstance", "substance");
以上代碼構建了一個包含多張表的級聯關系的Query Builder。
級聯關系如下:
- Executor
- tasks -> Task
- relatedRecord -> Record
- Task
- substances -> Substance
- Record
- recordTask -> Task
- recordAccount -> Account
- recordSubstance -> Substance
再看一個比較主流的Query Builder knex,我是在嘗鮮strapi的過程中發現的,strapi底層依賴於knex去進行數據庫交互以及連接池相關的功能,knex的使用大概是這樣的:
const knex = require('knex')({
client: 'sqlite3',
connection: {
filename: './data.db',
},
});
try {
await knex.schema
.createTable('users', table => {
table.increments('id');
table.string('user_name');
})
.createTable('accounts', table => {
table.increments('id');
table.string('account_name');
table
.integer('user_id')
.unsigned()
.references('users.id');
})
const insertedRows = await knex('users').insert({ user_name: 'Tim' })
await knex('accounts').insert({ account_name: 'knex', user_id: insertedRows[0] })
const selectedRows = await knex('users')
.join('accounts', 'users.id', 'accounts.user_id')
.select('users.user_name as user', 'accounts.account_name as account')
const enrichedRows = selectedRows.map(row => ({ ...row, active: true }))
} catch(e) {
console.error(e);
};
可以看到knex的鏈式操作更進了一步,甚至可以鏈式創建多張數據庫表。
Prisma
接下來就到了我們本篇文章的主角:Prisma 。Prisma對自己的定義仍然是NodeJS的ORM,但個人感覺它比普通意義上的ORM要強大得多。這里放一張官方的圖,來大致了解下Prisma和ORM、SQL、Query Builder的能力比較:
你也可以閱讀方方老師翻譯的這篇Why Prisma?來了解更多。
獨特的Schema定義方式、比TypeORM更加嚴謹全面的TS類型定義(尤其是在級聯關系中)、更容易上手和更貼近原生SQL的各種操作符等,很容易讓初次接觸的人欲罷不能(別說了,就是我)。
簡單的介紹下這些特點:
- Schema定義,我們前面看到的ORM都是使用JS/TS文件來定義數據庫表結構的,而Prisma不同,它使用
.prisma
后綴的文件來書寫獨特的Prisma Schema,然后基於schema生成表結構,VS Code有prisma官方提供的高亮、語法檢查插件,所以不用擔心使用負擔。
同時,這也就意味着圍繞Prisma Schema會產生一批generator功能的生態,如typegraphql-prisma就能夠基於Prisma Schema生成TypeGraphQL的Class定義,甚至還有CRUD的基本Resolver,類似的還有palJS提供的基於Prisma Schema生成Nexus的類型定義與CRUD方法(所以說GraphQL和Prisma這種都是SDL-First的工具真的是天作之合)。
TypeGraphQL、Resolver屬於GraphQL相關的工具/概念,如果未曾了解過也不要緊。
一個簡單的schema.prisma
可能是這樣的:
datasource db { provider = "sqlite" url = env("SINGLE_MODEL_DATABASE_URL") } generator client { provider = "prisma-client-js" output = "./client" } model Todo { id Int @id @default(autoincrement()) title String content String? finished Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
是不是感覺即使你沒用過,但還是挺好看懂。 - TS類型定義,可以說Prisma的類型定義是全覆蓋的,查詢參數、操作符參數、級聯參數、返回結果等等,比TypeORM的都更加完善。
- 更全面的操作符,如對字符串的查詢,Prisma中甚至提供了contains、startsWith、endsWith這種細粒度的操作符供過濾使用(而TypeORM中只能使用ILike這種方法來全量匹配)。(這些操作符的具體作用我們會在后面講到)
在這一部分的最后,我們來簡單的介紹下Prisma的使用流程,在正文中,我們會一步步詳細介紹Prisma的使用,包括單表、多表級聯以及Prisma與GraphQL的奇妙化學反應。
環境配置在下一節,這里我們只是先感受一下使用方式
- 首先,創建一個名為
prisma
的文件夾,在內部創建一個schema.prisma
文件
如果你使用的是VS Code,可以安裝Prisma擴展來獲得.prisma
的語法高亮 - 在schema中定義你的數據庫類型、路徑以及你的數據庫表結構,示例如下:
model Todo { id Int @id @default(autoincrement()) title String } - 運行
prisma generate
命令,prisma將為你生成Prisma Client
,內部結構是這樣的:
- 在你的文件中導入
Prisma Client
即可使用:
import { PrismaClient } from "./prisma/client";
const prisma = new PrismaClient();
async function createTodo(title: string, content?: string) {
const res = await prisma.todo.create({
data: {
title,
content,
},
});
return res;
}
每張表都會被存放在prisma.__YOUR_MODEL__
的命名空間下。
如果看完簡短的介紹你已經感覺這玩意有點好玩了,那么在跟着本文完成實踐后,你可能也會默默把手上的項目遷移到Prisma(畢設也可以安排上)~
上手Prisma
你可以在 Prisma-Article-Example 找到完整的示例,以下的例子我們會從一個空文件夾開始。
項目初始化
- 創建一個空文件夾,執行
npm init -y
yarn、pnpm同理 - 全局安裝
@prisma/cli
:npm install prisma -g
@prisma/cli
包已被更名為prisma
全局安裝@prisma/cli
是為了后面執行相關命令時方便些~ - 安裝必要的依賴:
npm install @prisma/client sqlite3 prisma -S
npm install typescript @types/node nodemon ts-node -D
安裝prisma
到文件夾時會根據你的操作系統下載對應的Query Engine:
- 執行
prisma version
,確定安裝成功。
- 執行
prisma init
,初始化一個Prisma項目(這個命令的侵入性非常低,只會生成prisma
文件夾和.env
文件,如果.env
文件已經存在,則會將需要的環境變量追加到已存在的文件)。
- 查看
.env
文件
# Environment variables declared in this file are automatically made available to Prisma. # See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables # Prisma supports the native connection string format for PostgreSQL, MySQL and SQLite. # See the documentation for all the connection string options: https://pris.ly/d/connection-strings DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
你會發現這里的數據庫默認使用的是postgresql,在本文中為了降低學習成本,我們全部使用SQLite作為數據庫,因此需要將變量值修改為file:../demo.sqlite
如果你此前沒有接觸過SQLite,可以理解為這是一個能被當作數據庫讀寫的文件(.sqlite
后綴),因此使用起來非常容易,也正是因為它是文件,所以需要將DATABASE_URL
這一變量改為file://
協議。
同樣的,在Prisma Schema中我們也需要修改數據庫類型為sqlite
:
// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema datasource db { provider = "sqlite" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" }
創建數據庫
在上面的Prisma Schema中,我們只定義了datasource和generator,它們分別負責定義使用的數據庫配置和客戶端生成的配置,舉例來說,默認情況下prisma生成的client會被放置在node_modules下,導入時的路徑也是import { PrismaClient } from "@prisma/client"
,但你可以通過client.output
命令更改生成的client位置。
generator client {
provider = "prisma-client-js"
output = "./client"
}
這一命令會使得client被生成到prisma
文件夾下,如:
將client生成到對應的prisma文件夾下這一方式使得在monorepo(或者只是多個文件夾的情況)下,每個項目可以方便的使用不同配置的schema生成的client。
我們在Prisma Schema中新增數據庫表結構的定義:
datasource db {
provider = "sqlite"
url = env("SINGLE_MODEL_DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
output = "./client"
}
model Todo {
id Int @id @default(autoincrement())
title String
content String?
finished Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
簡單解釋下相關語法:
- Int、String等這一類標量會被自動基於數據庫類型映射到對應的數據類型。標量類型后的
?
意味着這一字段是可選的。 @id
意為標識此字段為主鍵,@default()
意為默認值,autoincrement
與now
為prisma內置的函數,分別代表自增主鍵與字段寫入時的時間戳,類似的內置函數還有uuid、cuid等。
客戶端生成與使用
現在你可以生成客戶端了,執行prisma generate
:
還沒完,我們的數據庫文件(即sqlite文件)還沒創建出來,執行prisma db push
這個命令也會執行一次prisma generate
,你可以使用--skip-generate
跳過這里的client生成。
現在根目錄下就出現了demo.sqlite
文件。
在根目錄下創建index.ts:
// index.ts
import { PrismaClient } from "./prisma/client";
const prisma = new PrismaClient();
async function main() {
console.log("Prisma!");
}
main();
從使用方式你也可以看出來
PrismaClient
實際上是一個類,所以你可以繼承這個類來進行很多擴展操作,在后面我們會提到。
在開始使用前,為了后續學習的簡潔,我們使用nodemon
+ ts-node
,來幫助我們在index.ts發生變化時自動重新執行。
{
"name": "Prisma2-Explore",
"restartable": "r",
"delay": "500",
"ignore": [
".git",
"node_modules/**",
"/prisma/*",
],
"verbose": true,
"execMap": {
"": "node",
"js": "node --harmony",
"ts": "ts-node "
},
"watch": ["./**/*.ts"],
}
並將啟動腳本添加到package.json:
{
"scripts": {
"start": "nodemon index.ts"
}
}
執行npm start
:
Prisma單表初體驗
環境配置
接下來就到了正式使用環節,上面的代碼只是一個簡單的開發工作流示范,本文接下來的部分不會使用到(但是你可以基於這個工作流自己進一步的探索Prisma)。
在接下來,你所需要的相關環境我已經准備完畢,見Prisma-Article-Example,clone倉庫到本地,運行配置完畢的npm scripts即可。在這里簡單的介紹下項目中的npm scripts,如果在閱讀完畢本部分內容后覺得意猶未盡,可以使用這些scripts直接運行其他部分如多表、GraphQL相關的示例。簡單介紹部分scripts:
yarn flow
:從零開始完整的執行 生成客戶端 - 構建項目 - 執行構建產物 的流程。yarn dev:**
:在開發模式下運行項目,文件變化后重啟進程。yarn generate:**
:為項目生成Prisma Client。- 使用
yarn gen:client
來為所有項目生成Prisma Client。
yarn setup:**
:為構建完畢的項目生成SQLite文件。yarn invoke:**
:執行構建后的JS文件。- 使用
yarn setup
執行所有構建后的JS文件。
本部分(Prisma單表示例)的代碼見 single-model,相關的命令包括:
$ yarn dev:single
$ yarn generate:single
$ yarn setup:single
$ yarn invoke:single
在開始下文的CRUD代碼講解時,最好首先運行起來項目。首先執行yarn generate:single
,生成Prisma Client,然后再yarn dev:single
,進入開發模式,如下:
我直接一頓CRUD
根據前面已經提到的使用方式,首先引入Prisma Client並實例化:
import { PrismaClient } from "./prisma/client";
const prisma = new PrismaClient();
Prisma將你的表類(Table Class)掛載在prisma.MODEL
下,MODEL
值直接來自於schema.prisma
中的model名稱,如本例是Todo
,那么就可以在prisma.todo
下獲取到相關的操作方法:
因此,簡單的CRUD完全可以直接照着API來,
創建:
async function createTodo(title: string, content?: string) {
const res = await prisma.todo.create({
data: {
title,
content: content ?? null,
},
});
return res;
}
create方法接受兩個參數:
- data,即你要用來創建新數據的屬性,類型定義由你的schema決定,如這里content在schema中是可選的字符串(
String?
),其類型就為string|null
,所以需要使用??
語法來照顧參數未傳入的情況。 - select,決定create方法返回的對象中的字段,如果你指定
select.id
為false,那么create方法的返回值對象中就不會包含id這一屬性。這一參數在大部分prisma方法中都包含。
讀取:
async function getTodoById(id: number) {
const res = await prisma.todo.findUnique({
where: { id },
});
return res;
}
findUnique方法類似於TypeORM中的findOne方法,都是基於主鍵查詢,在這里將查詢條件傳入給where參數。
讀取所有:
async function getTodos(status?: boolean) {
const res = await prisma.todo.findMany({
orderBy: [{ id: "desc" }],
where: status
? {
finished: status,
}
: {},
select: {
id: true,
title: true,
content: true,
createdAt: true,
},
});
return res;
}
在這里我們額外傳入了orderBy方法來對返回的查詢結果進行排序,既然有了排序,當然也少不了分頁。你還可以傳入cursor
、skip
、take
等參數來完成分頁操作。
cursor-based 與 offset-based 實際上是兩種不同的分頁方式。
類似的,更新操作:
async function updateTodo(
id: number,
title?: string,
content?: string,
finished?: boolean
) {
const origin = await prisma.todo.findUnique({
where: { id },
});
if (!origin) {
throw new Error("Item Inexist!");
}
const res = await prisma.todo.update({
where: {
id,
},
data: {
title: title ?? origin.title,
content: content ?? origin.content,
finished: finished ?? origin.finished,
},
});
return res;
}
這里執行的是在未查詢到主鍵對應的數據實體時拋出錯誤,你也可以使用upsert方法來在數據實體不存在時執行創建。
批量更新:
async function convertStatus(status: boolean) {
const res = await prisma.todo.updateMany({
where: {
finished: !status,
},
data: {
finished: {
set: status,
},
},
});
return res;
}
注意,這里我們使用set屬性,來直接設置finished的值。這一方式和直接設置其為false是效果一致的,如果這里是個number類型,那么除了set以外,還可以使用increment、decrement、multiply以及divide方法。
最后是刪除操作:
async function deleteTodo(id: number) {
const res = await prisma.todo.delete({
where: { id },
});
return res;
}
async function clear() {
const res = await prisma.todo.deleteMany();
return res;
}
你可以自由的在以上這些例子以外,借助良好的TS類型提示花式探索Prisma的API,也可以提前看看其它部分的例子來早一步感受Prisma的強大能力。
尾聲 & 下篇預告
以上使用到的Prisma方法(如create)與操作符(如set)只是一小部分,目的只是為了讓你大致感受下Prisma與其他傳統ORM相比新奇的使用方式。在下篇中,我們將會介紹:
- Prisma多張數據表的級聯關系處理
- 多個Prisma Client協作
- Prisma與其他ORM的協作
- 和上一項一樣都屬於
- Prisma + GraphQL 全流程實戰
- Prisma的展望:工作原理、一體化框架
敬請期待~