跳至主要内容

prisma-binding 到 SDL-first

概述

本升级指南描述了如何迁移基于 Prisma 1 并使用 prisma-binding 实现 GraphQL 服务器的 Node.js 项目。

代码将保留 SDL-first 方法 来构建 GraphQL 架构。从 prisma-binding 迁移到 Prisma 客户端时,主要区别在于 info 对象不再能够自动解析关系,您需要实现自己的类型解析器来确保关系能够正确解析。

本指南假设您已完成 升级 Prisma ORM 层的指南。这意味着您已经

  • 安装了 Prisma ORM 2 CLI
  • 创建了 Prisma ORM 2 架构
  • 内省了您的数据库并解决了潜在的架构不兼容性
  • 安装并生成了 Prisma 客户端

本指南还假设您的文件设置类似于以下内容

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

重要部分是

  • 包含 Prisma ORM 2 架构的名为 prisma 的文件夹
  • 包含您的应用程序代码和名为 schema.graphql 的架构的 src 文件夹

如果您的项目结构与之不同,则需要调整指南中的说明以匹配您的设置。

1. 调整您的 GraphQL 架构

使用 prisma-binding,定义 GraphQL 架构(有时称为 应用程序架构)的方法基于从生成的 prisma.graphql 文件中导入 GraphQL 类型(在 Prisma 1 中,这通常称为 Prisma GraphQL 架构)。这些类型镜像您 Prisma 1 数据模型中的类型,并作为您的 GraphQL API 的基础。

使用 Prisma ORM 2,不再有 prisma.graphql 文件可以从中导入。因此,您必须在 schema.graphql 文件中直接拼写出 GraphQL 架构的所有类型。

最简单的方法是从 GraphQL Playground 下载完整的 GraphQL 架构。为此,请打开架构选项卡并点击右上角的下载按钮,然后选择SDL

Downloading the GraphQL schema with GraphQL Playground

或者,您可以使用 GraphQL CLIget-schema 命令来下载您的完整架构

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

注意:使用上述命令,您需要将 __GRAPHQL_YOGA_ENDPOINT__ 占位符替换为您的 GraphQL Yoga 服务器的实际端点。

获取 schema.graphql 文件后,将 src/schema.graphql 中的当前版本替换为新内容。请注意,这两个架构是完全等效的,除了新的架构不使用 graphql-import 从另一个文件中导入类型。相反,它在一个文件中拼写出所有类型。

以下是我们将在此指南中迁移的示例 GraphQL 架构的这两个版本的比较(您可以使用选项卡在两个版本之间切换)

# 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 架构不仅定义了直接导入的模型,还定义了之前架构中不存在的额外类型(例如 input 类型)。

2. 设置您的 PrismaClient 实例

PrismaClient 是您在 Prisma ORM 2 中用于访问数据库的新接口。它允许您调用各种方法,这些方法构建 SQL 查询并将它们发送到数据库,并将结果作为纯 JavaScript 对象返回。

PrismaClient 查询 API 的灵感来自最初的 prisma-binding API,因此您使用 Prisma 客户端发送的许多查询都会让人感觉很熟悉。

与来自 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://127.0.0.1:4466',
}),
prisma: new PrismaClient(),
}),
})

在上面的代码块中,红色行是要从当前设置中删除的行,绿色行是要添加的行。当然,您之前的设置可能与这个设置不同(例如,如果您在生产环境中运行 API,您的 Prisma ORM endpoint 不太可能是 https://127.0.0.1:4466),这只是一个示例,用于指示它可能是什么样子。

现在,当您在解析器中访问 context.prisma 时,就可以访问 Prisma 客户端查询了。

2. 编写您的 GraphQL 类型解析器

prisma-binding 能够神奇地解析 GraphQL 架构中的关系。但是,当不使用 prisma-binding 时,您需要使用所谓的类型解析器来显式解析关系。

注意您可以在本文中详细了解类型解析器的概念以及为什么需要它们:GraphQL 服务器基础知识:GraphQL 架构、类型定义和解析器解释

2.1. 为 User 类型实现类型解析器

我们示例 GraphQL 架构中的 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 的 1-n 关系
  • profile 字段表示与 Profile 的 1-1 关系

由于不再使用 prisma-binding,您现在需要在类型解析器中“手动”解析这些关系。

您可以在解析器映射中添加 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 记录。

由于这些额外的解析器,您现在可以在 GraphQL 查询/变异中嵌套关系,只要您在查询中请求有关 User 类型的任何信息,例如

{
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 架构中的 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 的 1-n 关系
  • categories 字段表示与 Category 的 m-n 关系

由于不再使用 prisma-binding,您现在需要在类型解析器中“手动”解析这些关系。

您可以在解析器映射中添加 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 记录。

由于这些额外的解析器,您现在可以在 GraphQL 查询/变异中嵌套关系,只要您在查询中请求有关 User 类型的任何信息,例如

{
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 架构中的 Profile 类型定义如下

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

此类型有一个关系:user 字段表示与 User 的 1-n 关系。

由于您不再使用prisma-binding,因此您现在需要在类型解析器中“手动”解析此关系。

您可以通过在解析器映射中添加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(其idparent对象中传递)加载User记录。

由于有了这个额外的解析器,您现在可以在 GraphQL 查询/变异中嵌套关系,无论何时您在查询中请求有关Profile类型的信息。

2.4. 实现Category类型的类型解析器

我们示例 GraphQL 架构中的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的 m-n 关系。

由于您不再使用prisma-binding,因此您现在需要在类型解析器中“手动”解析此关系。

您可以通过在解析器映射中添加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(其idparent对象中传递)加载所有Post记录。

由于有了这个额外的解析器,您现在可以在 GraphQL 查询/变异中嵌套关系,无论何时您在查询中请求有关Category类型的信息。

所有类型解析器都已到位后,您可以开始迁移实际的 GraphQL API 操作。

3. 迁移 GraphQL 操作

3.1. 迁移 GraphQL 查询

在本节中,您将把所有 GraphQL查询prisma-binding迁移到 Prisma Client。

3.1.1. 迁移users查询(使用forwardTo

在我们的示例 API 中,示例 GraphQL 架构中的users查询定义和实现如下。

使用prisma-binding的 SDL 架构定义
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

  • firstlastbeforeafter参数已删除。
  • 新的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 架构定义
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 架构定义
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: "[email protected]" }) {
id
name
}
}

3.1. 迁移 GraphQL 变异

在本节中,您将把示例架构中的 GraphQL 变异迁移出去。

3.1.2. 迁移createUser变异(使用forwardTo

在示例应用程序中,示例 GraphQL 架构中的createUser变异定义和实现如下。

使用prisma-binding的 SDL 架构定义
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: "[email protected]" }) {
id
}
}

3.1.3. 迁移createDraft(title: String!, content: String, authorId: String!): Post!查询

在示例应用程序中,createDraft变异定义和实现如下。

使用prisma-binding的 SDL 架构定义
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 架构定义
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: "[email protected]" }
bio: "I like turtles"
) {
id
name
profile {
id
bio
}
}
}

3.1.5. 迁移addPostToCategories(postId: String!, categoryIds: [String!]!): Post变异

在我们的示例应用程序中,addPostToCategories变异定义和实现如下。

使用prisma-binding的 SDL 架构定义
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 服务器。