跳到主要内容

prisma-binding 到 SDL-first

概述

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

代码将保留用于构建 GraphQL schema 的 SDL-first 方法。当从 prisma-binding 迁移到 Prisma Client 时,主要的区别在于 info 对象不能再用于自动解析关联关系,相反,您需要实现您的类型解析器以确保关联关系得到正确解析。

本指南假设您已经完成了升级 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 文件(在 Prisma 1 中,这通常称为 Prisma GraphQL schema导入 GraphQL 类型。这些类型镜像了您 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% 等价的,除了新的 schema 不使用 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://127.0.0.1:4466',
}),
prisma: new PrismaClient(),
}),
})

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

当您现在在解析器内部访问 context.prisma 时,您现在可以访问 Prisma Client 查询。

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

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

注意 您可以在本文中了解有关类型解析器的概念以及为什么它们是必要的更多信息:GraphQL 服务器基础知识:GraphQL Schema、TypeDefs 和解析器详解

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 的 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 记录。

感谢这些额外的解析器,当您在查询中请求有关 User 类型的信息时,您现在可以在 GraphQL 查询/mutation 中嵌套关联关系,例如

{
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 的 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 记录。

感谢这些额外的解析器,当您在查询中请求有关 User 类型的信息时,您现在可以在 GraphQL 查询/mutation 中嵌套关联关系,例如

{
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 的 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(其 id 包含在 parent 对象中)加载 User 记录。

感谢这个额外的解析器,当您在查询中请求有关 Profile 类型的信息时,您现在可以在 GraphQL 查询/mutation 中嵌套关联关系。

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 的 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(其 id 包含在 parent 对象中)加载所有 Post 记录。

感谢这个额外的解析器,当您在查询中请求有关 Category 类型的信息时,您现在可以在 GraphQL 查询/mutation 中嵌套关联关系。

完成所有类型解析器后,您可以开始迁移实际的 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

  • 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 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: "[email protected]" }) {
id
name
}
}

3.1. 迁移 GraphQL mutation

在本节中,您将迁移示例 schema 中的 GraphQL mutation。

3.1.2. 迁移 createUser mutation(使用 forwardTo

在示例应用程序中,示例 GraphQL schema 中的 createUser mutation 定义和实现如下。

使用 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,例如

mutation {
createUser(data: { name: "Alice", email: "[email protected]" }) {
id
}
}

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

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

使用 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

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

3.1.4. 迁移 updateBio(bio: String, userUniqueInput: UserUniqueInput!): User mutation

在示例应用程序中,updateBio mutation 定义和实现如下。

使用 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

mutation {
updateBio(
userUniqueInput: { email: "[email protected]" }
bio: "I like turtles"
) {
id
name
profile {
id
bio
}
}
}

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

在示例应用程序中,addPostToCategories mutation 定义和实现如下。

使用 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 服务器。