跳到主要內容

從 prisma-binding 到 SDL-first

概覽

本升級指南描述瞭如何遷移一個基於 Prisma 1 並使用 prisma-binding 實現 GraphQL 伺服器的 Node.js 專案。

程式碼將保留 SDL-first 方法 來構建 GraphQL schema。從 prisma-binding 遷移到 Prisma Client 時,主要區別在於 info 物件不能再用於自動解析關係,相反,你需要實現自己的 type resolvers 以確保關係得到正確解析。

本指南假設你已經完成了 升級 Prisma ORM 層的指南。這意味著你已經

  • 安裝了 Prisma ORM 2 CLI
  • 建立了你的 Prisma ORM 2 schema
  • 內省了你的資料庫並解決了潛在的 schema 不相容性
  • 安裝並生成了 Prisma Client

本指南進一步假設你的檔案設定類似於此

.
├── README.md
├── package.json
├── prisma
│ └── schema.prisma
├── prisma1
│ ├── datamodel.prisma
│ └── prisma.yml
└── src
├── generated
│ └── prisma.graphql
├── index.js
└── schema.graphql

重要部分包括

  • 一個名為 prisma 的資料夾,其中包含你的 Prisma ORM 2 schema
  • 一個名為 src 的資料夾,其中包含你的應用程式程式碼和一個名為 schema.graphql 的 schema

如果你的專案結構不是這樣,你需要根據你自己的設定調整指南中的說明。

1. 調整你的 GraphQL schema

使用 prisma-binding,你定義 GraphQL schema(有時稱為 應用程式 schema)的方法是基於從生成的 prisma.graphql 檔案中匯入 GraphQL 型別(在 Prisma 1 中,這通常被稱為 Prisma GraphQL schema)。這些型別反映了你的 Prisma 1 資料模型中的型別,並作為你的 GraphQL API 的基礎。

使用 Prisma ORM 2,不再有你可以匯入的 prisma.graphql 檔案。因此,你必須直接在你的 schema.graphql 檔案中詳細定義你的 GraphQL schema 的所有型別。

最簡單的方法是從 GraphQL Playground 下載完整的 GraphQL schema。為此,開啟 SCHEMA 選項卡,然後點選右上角的 DOWNLOAD 按鈕,接著選擇 SDL

Downloading the GraphQL schema with GraphQL Playground

或者,你可以使用 GraphQL CLIget-schema 命令來下載你的完整 schema

npx graphql get-schema --endpoint __GRAPHQL_YOGA_ENDPOINT__ --output schema.graphql --no-all

注意:使用上述命令時,你需要將 __GRAPHQL_YOGA_ENDPOINT__ 佔位符替換為你的 GraphQL Yoga 伺服器的實際端點。

獲取 schema.graphql 檔案後,用新內容替換 src/schema.graphql 中的當前版本。請注意,這兩個 schema 是 100% 等效的,只是新版本不再使用 graphql-import 來從不同檔案匯入型別。相反,它在一個檔案中詳細定義了所有型別。

以下是本指南中我們將遷移的兩個版本的示例 GraphQL schema 的比較(你可以使用選項卡在兩個版本之間切換)

# import Post from './generated/prisma.graphql'
# import User from './generated/prisma.graphql'
# import Category from './generated/prisma.graphql'

type Query {
posts(searchString: String): [Post!]!
user(userUniqueInput: UserUniqueInput!): User
users(where: UserWhereInput, orderBy: Enumerable<UserOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
allCategories: [Category!]!
}

input UserUniqueInput {
id: String
email: String
}

type Mutation {
createDraft(authorId: ID!, title: String!, content: String!): Post
publish(id: ID!): Post
deletePost(id: ID!): Post
signup(name: String!, email: String!): User!
updateBio(userId: String!, bio: String!): User
addPostToCategories(postId: String!, categoryIds: [String!]!): Post
}

你會注意到,新版本的 GraphQL schema 不僅定義了直接匯入的 模型,還定義了之前 schema 中不存在的額外型別(例如 input 型別)。

2. 設定你的 PrismaClient 例項

PrismaClient 是 Prisma ORM 2 中你與資料庫的新介面。它允許你呼叫各種方法來構建 SQL 查詢並將它們傳送到資料庫,將結果作為普通 JavaScript 物件返回。

PrismaClient 查詢 API 的靈感來源於最初的 prisma-binding API,因此你使用 Prisma Client 傳送的許多查詢都會感到熟悉。

與 Prisma 1 中的 prisma-binding 例項類似,你也會希望將 Prisma ORM 2 中的 PrismaClient 附加到 GraphQL 的 context 上,以便在你的解析器中訪問它。

const { PrismaClient } = require('@prisma/client')

// ...

const server = new GraphQLServer({
typeDefs: 'src/schema.graphql',
resolvers,
context: (req) => ({
...req,
prisma: new Prisma({
typeDefs: 'src/generated/prisma.graphql',
endpoint: 'https://:4466',
}),
prisma: new PrismaClient(),
}),
})

在上面的程式碼塊中,紅色 行是你當前設定中要刪除的行,綠色 行是你應該新增的行。當然,你的舊設定可能與此不同(例如,如果你在生產環境中執行 API,你的 Prisma ORM endpoint 不太可能是 https://:4466),這只是一個示例,以說明它可能的樣子。

當你現在在解析器中訪問 context.prisma 時,你現在可以訪問 Prisma Client 查詢了。

2. 編寫你的 GraphQL 型別解析器

prisma-binding 能夠神奇地解析你的 GraphQL schema 中的關係。然而,當不使用 prisma-binding 時,你需要使用所謂的 型別解析器(type resolvers) 明確解析你的關係。

注意 你可以在這篇文章中瞭解更多關於型別解析器(type resolvers)的概念以及為什麼它們是必要的:GraphQL Server Basics: GraphQL Schemas, TypeDefs & Resolvers Explained

2.1. 為 User 型別實現型別解析器

我們示例 GraphQL schema 中的 User 型別定義如下

type User implements Node {
id: ID!
email: String
name: String!
posts(
where: PostWhereInput
orderBy: Enumerable<PostOrderByInput>
skip: Int
after: String
before: String
first: Int
last: Int
): [Post!]
role: Role!
profile: Profile
jsonData: Json
}

此型別有兩個關係

  • posts 欄位表示與 Post 的一對多關係
  • profile 欄位表示與 Profile 的一對一關係

由於你不再使用 prisma-binding,現在需要在型別解析器中“手動”解析這些關係。

你可以透過向你的 解析器對映(resolver map) 新增一個 User 欄位,並按如下方式實現 postsprofile 關係的解析器。

const resolvers = {
Query: {
// ... your query resolvers
},
Mutation: {
// ... your mutation resolvers
},
User: {
posts: (parent, args, context) => {
return context.prisma.user
.findUnique({
where: { id: parent.id },
})
.posts()
},
profile: (parent, args, context) => {
return context.prisma.user
.findUnique({
where: { id: parent.id },
})
.profile()
},
},
}

在這些解析器中,你正在使用新的 PrismaClient 對資料庫執行查詢。在 posts 解析器中,資料庫查詢從指定的 author(其 id 包含在 parent 物件中)載入所有 Post 記錄。在 profile 解析器中,資料庫查詢從指定的 user(其 id 包含在 parent 物件中)載入 Profile 記錄。

由於這些額外的解析器,現在當你進行查詢以請求 User 型別資訊時,你將能夠巢狀 GraphQL 查詢/突變中的關係,例如:

{
users {
id
name
posts {
# fetching this relation is enabled by the new type resolver
id
title
}
profile {
# fetching this relation is enabled by the new type resolver
id
bio
}
}
}

2.2. 為 Post 型別實現型別解析器

我們示例 GraphQL schema 中的 Post 型別定義如下

type Post implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
title: String!
content: String
published: Boolean!
author: User
categories(
where: CategoryWhereInput
orderBy: Enumerable<CategoryOrderByInput>
skip: Int
after: String
before: String
first: Int
last: Int
): [Category!]
}

此型別有兩個關係

  • author 欄位表示與 User 的一對多關係
  • categories 欄位表示與 Category 的多對多關係

由於你不再使用 prisma-binding,現在需要在型別解析器中“手動”解析這些關係。

你可以透過向你的 解析器對映(resolver map) 新增一個 Post 欄位,並按如下方式實現 authorcategories 關係的解析器。

const resolvers = {
Query: {
// ... your query resolvers
},
Mutation: {
// ... your mutation resolvers
},
User: {
// ... your type resolvers for `User` from before
},
Post: {
author: (parent, args, context) => {
return context.prisma.post
.findUnique({
where: { id: parent.id },
})
.author()
},
categories: (parent, args, context) => {
return context.prisma.post
.findUnique({
where: { id: parent.id },
})
.categories()
},
},
}

在這些解析器中,你正在使用新的 PrismaClient 對資料庫執行查詢。在 author 解析器中,資料庫查詢載入表示 PostauthorUser 記錄。在 categories 解析器中,資料庫查詢從指定的 post(其 id 包含在 parent 物件中)載入所有 Category 記錄。

由於這些額外的解析器,現在當你進行查詢以請求 User 型別資訊時,你將能夠巢狀 GraphQL 查詢/突變中的關係,例如:

{
posts {
id
title
author {
# fetching this relation is enabled by the new type resolver
id
name
}
categories {
# fetching this relation is enabled by the new type resolver
id
name
}
}
}

2.3. 為 Profile 型別實現型別解析器

我們示例 GraphQL schema 中的 Profile 型別定義如下

type Profile implements Node {
id: ID!
bio: String
user: User!
}

此型別有一個關係:user 欄位表示與 User 的一對多關係。

由於你不再使用 prisma-binding,現在需要在型別解析器中“手動”解析此關係。

你可以透過向你的 解析器對映(resolver map) 新增一個 Profile 欄位,並按如下方式實現 owner 關係的解析器。

const resolvers = {
Query: {
// ... your query resolvers
},
Mutation: {
// ... your mutation resolvers
},
User: {
// ... your type resolvers for `User` from before
},
Post: {
// ... your type resolvers for `Post` from before
},
Profile: {
user: (parent, args, context) => {
return context.prisma.profile
.findUnique({
where: { id: parent.id },
})
.owner()
},
},
}

在此解析器中,你正在使用新的 PrismaClient 對資料庫執行查詢。在 user 解析器中,資料庫查詢從指定的 profile(其 id 包含在 parent 物件中)載入 User 記錄。

由於這個額外的解析器,現在當你進行查詢以請求 Profile 型別資訊時,你將能夠巢狀 GraphQL 查詢/突變中的關係。

2.4. 為 Category 型別實現型別解析器

我們示例 GraphQL schema 中的 Category 型別定義如下

type Category implements Node {
id: ID!
name: String!
posts(
where: PostWhereInput
orderBy: Enumerable<PostOrderByInput>
skip: Int
after: String
before: String
first: Int
last: Int
): [Post!]
}

此型別有一個關係:posts 欄位表示與 Post 的多對多關係。

由於你不再使用 prisma-binding,現在需要在型別解析器中“手動”解析此關係。

你可以透過向你的 解析器對映(resolver map) 新增一個 Category 欄位,並按如下方式實現 postsprofile 關係的解析器。

const resolvers = {
Query: {
// ... your query resolvers
},
Mutation: {
// ... your mutation resolvers
},
User: {
// ... your type resolvers for `User` from before
},
Post: {
// ... your type resolvers for `Post` from before
},
Profile: {
// ... your type resolvers for `User` from before
},
Category: {
posts: (parent, args, context) => {
return context.prisma
.findUnique({
where: { id: parent.id },
})
.posts()
},
},
}

在此解析器中,你正在使用新的 PrismaClient 對資料庫執行查詢。在 posts 解析器中,資料庫查詢從指定的 categories(其 id 包含在 parent 物件中)載入所有 Post 記錄。

由於這個額外的解析器,現在當你進行查詢以請求 Category 型別資訊時,你將能夠巢狀 GraphQL 查詢/突變中的關係。

所有型別解析器就緒後,你可以開始遷移實際的 GraphQL API 操作。

3. 遷移 GraphQL 操作

3.1. 遷移 GraphQL 查詢

在本節中,你將把所有 GraphQL 查詢prisma-binding 遷移到 Prisma Client。

3.1.1. 遷移 users 查詢(使用 forwardTo

在我們的示例 API 中,示例 GraphQL schema 中的 users 查詢定義和實現如下。

使用 prisma-binding 的 SDL schema 定義
type Query {
users(where: UserWhereInput, orderBy: Enumerable<UserOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
# ... other queries
}
使用 prisma-binding 的解析器實現
const resolvers = {
Query: {
users: forwardTo('prisma'),
// ... other resolvers
},
}
使用 Prisma Client 實現 users 解析器

要重新實現以前使用 forwardTo 的查詢,思路是將傳入的過濾、排序和分頁引數傳遞給 PrismaClient

const resolvers = {
Query: {
users: (_, args, context, info) => {
// this doesn't work yet
const { where, orderBy, skip, first, last, after, before } = args
return context.prisma.user.findMany({
where,
orderBy,
skip,
first,
last,
after,
before,
})
},
// ... other resolvers
},
}

請注意,這種方法目前尚不可行,因為傳入引數的結構PrismaClient 期望的不同。為確保結構相容,你可以使用 @prisma/binding-argument-transform npm 包來確保相容性。

npm install @prisma/binding-argument-transform

你現在可以按如下方式使用此包

const {
makeOrderByPrisma2Compatible,
makeWherePrisma2Compatible,
} = require('@prisma/binding-argument-transform')

const resolvers = {
Query: {
users: (_, args, context, info) => {
// this still doesn't entirely work
const { where, orderBy, skip, first, last, after, before } = args
const prisma2Where = makeWherePrisma2Compatible(where)
const prisma2OrderBy = makeOrderByPrisma2Compatible(orderBy)
return context.prisma.user.findMany({
where: prisma2Where,
orderBy: prisma2OrderBy,
skip,
first,
last,
after,
before,
})
},
// ... other resolvers
},
}

最後一個遺留問題是分頁引數。Prisma ORM 2 引入了 新的分頁 API

  • first, last, beforeafter 引數已被移除
  • 新的 cursor 引數取代了 beforeafter
  • 新的 take 引數取代了 firstlast

你可以按如下方式調整呼叫,使其符合新的 Prisma Client 分頁 API

const {
makeOrderByPrisma2Compatible,
makeWherePrisma2Compatible,
} = require('@prisma/binding-argument-transform')

const resolvers = {
Query: {
users: (_, args, context) => {
const { where, orderBy, skip, first, last, after, before } = args
const prisma2Where = makeWherePrisma2Compatible(where)
const prisma2OrderBy = makeOrderByPrisma2Compatible(orderBy)
const skipValue = skip || 0
const prisma2Skip = Boolean(before) ? skipValue + 1 : skipValue
const prisma2Take = Boolean(last) ? -last : first
const prisma2Before = { id: before }
const prisma2After = { id: after }
const prisma2Cursor =
!Boolean(before) && !Boolean(after)
? undefined
: Boolean(before)
? prisma2Before
: prisma2After
return context.prisma.user.findMany({
where: prisma2Where,
orderBy: prisma2OrderBy,
skip: prisma2Skip,
cursor: prisma2Cursor,
take: prisma2Take,
})
},
// ... other resolvers
},
}

這些計算是為了確保傳入的分頁引數能正確對映到 Prisma Client API 中的引數。

3.1.2. 遷移 posts(searchString: String): [Post!]! 查詢

posts 查詢定義和實現如下。

使用 prisma-binding 的 SDL schema 定義
type Query {
posts(searchString: String): [Post!]!
# ... other queries
}
使用 prisma-binding 的解析器實現
const resolvers = {
Query: {
posts: (_, args, context, info) => {
return context.prisma.query.posts(
{
where: {
OR: [
{ title_contains: args.searchString },
{ content_contains: args.searchString },
],
},
},
info
)
},
// ... other resolvers
},
}
使用 Prisma Client 實現 posts 解析器

為了在新版 Prisma Client 中獲得相同的行為,你需要調整你的解析器實現。

const resolvers = {
Query: {
posts: (_, args, context) => {
return context.prisma.post.findMany({
where: {
OR: [
{ title: { contains: args.searchString } },
{ content: { contains: args.searchString } },
],
},
})
},
// ... other resolvers
},
}

你現在可以在 GraphQL Playground 中傳送相應的查詢。

{
posts {
id
title
author {
id
name
}
}
}

3.1.3. 遷移 user(uniqueInput: UserUniqueInput): User 查詢

在我們的示例應用中,user 查詢定義和實現如下。

使用 prisma-binding 的 SDL schema 定義
type Query {
user(userUniqueInput: UserUniqueInput): User
# ... other queries
}

input UserUniqueInput {
id: String
email: String
}
使用 prisma-binding 的解析器實現
const resolvers = {
Query: {
user: (_, args, context, info) => {
return context.prisma.query.user(
{
where: args.userUniqueInput,
},
info
)
},
// ... other resolvers
},
}
使用 Prisma Client 實現 user 解析器

為了在新版 Prisma Client 中獲得相同的行為,你需要調整你的解析器實現。

const resolvers = {
Query: {
user: (_, args, context) => {
return context.prisma.user.findUnique({
where: args.userUniqueInput,
})
},
// ... other resolvers
},
}

你現在可以透過 GraphQL Playground 傳送相應的查詢。

{
user(userUniqueInput: { email: "alice@prisma.io" }) {
id
name
}
}

3.1. 遷移 GraphQL 突變

在本節中,你將從示例 schema 遷移 GraphQL 突變。

3.1.2. 遷移 createUser 突變(使用 forwardTo

在示例應用中,示例 GraphQL schema 中的 createUser 突變定義和實現如下。

使用 prisma-binding 的 SDL schema 定義
type Mutation {
createUser(data: UserCreateInput!): User!
# ... other mutations
}
使用 prisma-binding 的解析器實現
const resolvers = {
Mutation: {
createUser: forwardTo('prisma'),
// ... other resolvers
},
}
使用 Prisma Client 實現 createUser 解析器

為了在新版 Prisma Client 中獲得相同的行為,你需要調整你的解析器實現。

const resolvers = {
Mutation: {
createUser: (_, args, context, info) => {
return context.prisma.user.create({
data: args.data,
})
},
// ... other resolvers
},
}

你現在可以針對新 API 編寫你的第一個突變,例如:

mutation {
createUser(data: { name: "Alice", email: "alice@prisma.io" }) {
id
}
}

3.1.3. 遷移 createDraft(title: String!, content: String, authorId: String!): Post! 查詢

在示例應用中,createDraft 突變定義和實現如下。

使用 prisma-binding 的 SDL schema 定義
type Mutation {
createDraft(title: String!, content: String, authorId: String!): Post!
# ... other mutations
}
使用 prisma-binding 的解析器實現
const resolvers = {
Mutation: {
createDraft: (_, args, context, info) => {
return context.prisma.mutation.createPost(
{
data: {
title: args.title,
content: args.content,
author: {
connect: {
id: args.authorId,
},
},
},
},
info
)
},
// ... other resolvers
},
}
使用 Prisma Client 實現 createDraft 解析器

為了在新版 Prisma Client 中獲得相同的行為,你需要調整你的解析器實現。

const resolvers = {
Mutation: {
createDraft: (_, args, context, info) => {
return context.prisma.post.create({
data: {
title: args.title,
content: args.content,
author: {
connect: {
id: args.authorId,
},
},
},
})
},
// ... other resolvers
},
}

你現在可以透過 GraphQL Playground 傳送相應的突變。

mutation {
createDraft(title: "Hello World", authorId: "__AUTHOR_ID__") {
id
published
author {
id
name
}
}
}

3.1.4. 遷移 updateBio(bio: String, userUniqueInput: UserUniqueInput!): User 突變

在示例應用中,updateBio 突變定義和實現如下。

使用 prisma-binding 的 SDL schema 定義
type Mutation {
updateBio(bio: String!, userUniqueInput: UserUniqueInput!): User
# ... other mutations
}
使用 prisma-binding 的解析器實現
const resolvers = {
Mutation: {
updateBio: (_, args, context, info) => {
return context.prisma.mutation.updateUser(
{
data: {
profile: {
update: { bio: args.bio },
},
},
where: { id: args.userId },
},
info
)
},
// ... other resolvers
},
}
使用 Prisma Client 實現 updateBio 解析器

為了在 Prisma Client 中獲得相同的行為,你需要調整你的解析器實現。

const resolvers = {
Mutation: {
updateBio: (_, args, context, info) => {
return context.prisma.user.update({
data: {
profile: {
update: { bio: args.bio },
},
},
where: args.userUniqueInput,
})
},
// ... other resolvers
},
}

你現在可以透過 GraphQL Playground 傳送相應的突變。

mutation {
updateBio(
userUniqueInput: { email: "alice@prisma.io" }
bio: "I like turtles"
) {
id
name
profile {
id
bio
}
}
}

3.1.5. 遷移 addPostToCategories(postId: String!, categoryIds: [String!]!): Post 突變

在我們的示例應用中,addPostToCategories 突變定義和實現如下。

使用 prisma-binding 的 SDL schema 定義
type Mutation {
addPostToCategories(postId: String!, categoryIds: [String!]!): Post
# ... other mutations
}
使用 prisma-binding 的解析器實現
const resolvers = {
Mutation: {
addPostToCategories: (_, args, context, info) => {
const ids = args.categoryIds.map((id) => ({ id }))
return context.prisma.mutation.updatePost(
{
data: {
categories: {
connect: ids,
},
},
where: {
id: args.postId,
},
},
info
)
},
// ... other resolvers
},
}
使用 Prisma Client 實現 addPostToCategories 解析器

為了在 Prisma Client 中獲得相同的行為,你需要調整你的解析器實現。

const resolvers = {
Mutation: {
addPostToCategories: (_, args, context, info) => {
const ids = args.categoryIds.map((id) => ({ id }))
return context.prisma.post.update({
where: {
id: args.postId,
},
data: {
categories: { connect: ids },
},
})
},
// ... other resolvers
},
}

你現在可以透過 GraphQL Playground 傳送相應的查詢。

mutation {
addPostToCategories(
postId: "__AUTHOR_ID__"
categoryIds: ["__CATEGORY_ID_1__", "__CATEGORY_ID_2__"]
) {
id
title
categories {
id
name
}
}
}

4. 清理

由於整個應用現在已升級到 Prisma ORM 2,你可以刪除所有不必要的檔案並移除不再需要的依賴項。

4.1. 清理 npm 依賴項

你可以從移除與 Prisma 1 設定相關的 npm 依賴項開始。

npm uninstall graphql-cli prisma-binding prisma1

4.2. 刪除未使用的檔案

接下來,刪除你的 Prisma 1 設定檔案。

rm prisma1/datamodel.prisma prisma1/prisma.yml

4.3. 停止 Prisma ORM 伺服器

最後,你可以停止執行你的 Prisma ORM 伺服器。

© . This site is unofficial and not affiliated with Prisma Data, Inc.