Prisma:下一代ORM,不僅僅是ORM(上篇)
前言
本篇文章將會介紹一個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 Builderknex,我是在嚐鮮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-stringsDATABASE_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-schemadatasource 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的展望:工作原理、一體化框架
敬請期待~
漫思