跳到主要内容

从 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: 'http://localhost:4466',
}),
prisma: new PrismaClient(),
}),
})

在上面的代码块中,红色 行是你当前设置中要删除的行,绿色 行是你应该添加的行。当然,你的旧设置可能与此不同(例如,如果你在生产环境中运行 API,你的 Prisma ORM endpoint 不太可能是 http://localhost: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 服务器。

© . All rights reserved.