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
或者,您可以使用 GraphQL CLI 的 get-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 的两个版本的比较(您可以使用选项卡在两个版本之间切换)
- 之前(使用 graphql-import)
- 之后(使用 Prisma 2)
# 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
}
type Query {
posts(searchString: String): [Post!]!
user(id: ID!): User
users(where: UserWhereInput, orderBy: Enumerable<UserOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
allCategories: [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!]
}
input CategoryCreateManyWithoutPostsInput {
create: [CategoryCreateWithoutPostsInput!]
connect: [CategoryWhereUniqueInput!]
}
input CategoryCreateWithoutPostsInput {
id: ID
name: String!
}
enum CategoryOrderByInput {
id_ASC
id_DESC
name_ASC
name_DESC
}
input CategoryWhereInput {
"""Logical AND on all given filters."""
AND: [CategoryWhereInput!]
"""Logical OR on all given filters."""
OR: [CategoryWhereInput!]
"""Logical NOT on all given filters combined by AND."""
NOT: [CategoryWhereInput!]
id: ID
"""All values that are not equal to given value."""
id_not: ID
"""All values that are contained in given list."""
id_in: [ID!]
"""All values that are not contained in given list."""
id_not_in: [ID!]
"""All values less than the given value."""
id_lt: ID
"""All values less than or equal the given value."""
id_lte: ID
"""All values greater than the given value."""
id_gt: ID
"""All values greater than or equal the given value."""
id_gte: ID
"""All values containing the given string."""
id_contains: ID
"""All values not containing the given string."""
id_not_contains: ID
"""All values starting with the given string."""
id_starts_with: ID
"""All values not starting with the given string."""
id_not_starts_with: ID
"""All values ending with the given string."""
id_ends_with: ID
"""All values not ending with the given string."""
id_not_ends_with: ID
name: String
"""All values that are not equal to given value."""
name_not: String
"""All values that are contained in given list."""
name_in: [String!]
"""All values that are not contained in given list."""
name_not_in: [String!]
"""All values less than the given value."""
name_lt: String
"""All values less than or equal the given value."""
name_lte: String
"""All values greater than the given value."""
name_gt: String
"""All values greater than or equal the given value."""
name_gte: String
"""All values containing the given string."""
name_contains: String
"""All values not containing the given string."""
name_not_contains: String
"""All values starting with the given string."""
name_starts_with: String
"""All values not starting with the given string."""
name_not_starts_with: String
"""All values ending with the given string."""
name_ends_with: String
"""All values not ending with the given string."""
name_not_ends_with: String
posts_every: PostWhereInput
posts_some: PostWhereInput
posts_none: PostWhereInput
}
input CategoryWhereUniqueInput {
id: ID
}
scalar DateTime
"""Raw JSON value"""
scalar Json
"""An object with an ID"""
interface Node {
"""The id of the object."""
id: ID!
}
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!]
}
input PostCreateManyWithoutAuthorInput {
create: [PostCreateWithoutAuthorInput!]
connect: [PostWhereUniqueInput!]
}
input PostCreateWithoutAuthorInput {
id: ID
title: String!
content: String
published: Boolean
categories: CategoryCreateManyWithoutPostsInput
}
enum PostOrderByInput {
id_ASC
id_DESC
createdAt_ASC
createdAt_DESC
updatedAt_ASC
updatedAt_DESC
title_ASC
title_DESC
content_ASC
content_DESC
published_ASC
published_DESC
}
input PostWhereInput {
"""Logical AND on all given filters."""
AND: [PostWhereInput!]
"""Logical OR on all given filters."""
OR: [PostWhereInput!]
"""Logical NOT on all given filters combined by AND."""
NOT: [PostWhereInput!]
id: ID
"""All values that are not equal to given value."""
id_not: ID
"""All values that are contained in given list."""
id_in: [ID!]
"""All values that are not contained in given list."""
id_not_in: [ID!]
"""All values less than the given value."""
id_lt: ID
"""All values less than or equal the given value."""
id_lte: ID
"""All values greater than the given value."""
id_gt: ID
"""All values greater than or equal the given value."""
id_gte: ID
"""All values containing the given string."""
id_contains: ID
"""All values not containing the given string."""
id_not_contains: ID
"""All values starting with the given string."""
id_starts_with: ID
"""All values not starting with the given string."""
id_not_starts_with: ID
"""All values ending with the given string."""
id_ends_with: ID
"""All values not ending with the given string."""
id_not_ends_with: ID
createdAt: DateTime
"""All values that are not equal to given value."""
createdAt_not: DateTime
"""All values that are contained in given list."""
createdAt_in: [DateTime!]
"""All values that are not contained in given list."""
createdAt_not_in: [DateTime!]
"""All values less than the given value."""
createdAt_lt: DateTime
"""All values less than or equal the given value."""
createdAt_lte: DateTime
"""All values greater than the given value."""
createdAt_gt: DateTime
"""All values greater than or equal the given value."""
createdAt_gte: DateTime
updatedAt: DateTime
"""All values that are not equal to given value."""
updatedAt_not: DateTime
"""All values that are contained in given list."""
updatedAt_in: [DateTime!]
"""All values that are not contained in given list."""
updatedAt_not_in: [DateTime!]
"""All values less than the given value."""
updatedAt_lt: DateTime
"""All values less than or equal the given value."""
updatedAt_lte: DateTime
"""All values greater than the given value."""
updatedAt_gt: DateTime
"""All values greater than or equal the given value."""
updatedAt_gte: DateTime
title: String
"""All values that are not equal to given value."""
title_not: String
"""All values that are contained in given list."""
title_in: [String!]
"""All values that are not contained in given list."""
title_not_in: [String!]
"""All values less than the given value."""
title_lt: String
"""All values less than or equal the given value."""
title_lte: String
"""All values greater than the given value."""
title_gt: String
"""All values greater than or equal the given value."""
title_gte: String
"""All values containing the given string."""
title_contains: String
"""All values not containing the given string."""
title_not_contains: String
"""All values starting with the given string."""
title_starts_with: String
"""All values not starting with the given string."""
title_not_starts_with: String
"""All values ending with the given string."""
title_ends_with: String
"""All values not ending with the given string."""
title_not_ends_with: String
content: String
"""All values that are not equal to given value."""
content_not: String
"""All values that are contained in given list."""
content_in: [String!]
"""All values that are not contained in given list."""
content_not_in: [String!]
"""All values less than the given value."""
content_lt: String
"""All values less than or equal the given value."""
content_lte: String
"""All values greater than the given value."""
content_gt: String
"""All values greater than or equal the given value."""
content_gte: String
"""All values containing the given string."""
content_contains: String
"""All values not containing the given string."""
content_not_contains: String
"""All values starting with the given string."""
content_starts_with: String
"""All values not starting with the given string."""
content_not_starts_with: String
"""All values ending with the given string."""
content_ends_with: String
"""All values not ending with the given string."""
content_not_ends_with: String
published: Boolean
"""All values that are not equal to given value."""
published_not: Boolean
author: UserWhereInput
categories_every: CategoryWhereInput
categories_some: CategoryWhereInput
categories_none: CategoryWhereInput
}
input PostWhereUniqueInput {
id: ID
}
type Profile implements Node {
id: ID!
bio: String
user: User!
}
input ProfileCreateOneWithoutUserInput {
create: ProfileCreateWithoutUserInput
connect: ProfileWhereUniqueInput
}
input ProfileCreateWithoutUserInput {
id: ID
bio: String
}
input ProfileWhereInput {
"""Logical AND on all given filters."""
AND: [ProfileWhereInput!]
"""Logical OR on all given filters."""
OR: [ProfileWhereInput!]
"""Logical NOT on all given filters combined by AND."""
NOT: [ProfileWhereInput!]
id: ID
"""All values that are not equal to given value."""
id_not: ID
"""All values that are contained in given list."""
id_in: [ID!]
"""All values that are not contained in given list."""
id_not_in: [ID!]
"""All values less than the given value."""
id_lt: ID
"""All values less than or equal the given value."""
id_lte: ID
"""All values greater than the given value."""
id_gt: ID
"""All values greater than or equal the given value."""
id_gte: ID
"""All values containing the given string."""
id_contains: ID
"""All values not containing the given string."""
id_not_contains: ID
"""All values starting with the given string."""
id_starts_with: ID
"""All values not starting with the given string."""
id_not_starts_with: ID
"""All values ending with the given string."""
id_ends_with: ID
"""All values not ending with the given string."""
id_not_ends_with: ID
bio: String
"""All values that are not equal to given value."""
bio_not: String
"""All values that are contained in given list."""
bio_in: [String!]
"""All values that are not contained in given list."""
bio_not_in: [String!]
"""All values less than the given value."""
bio_lt: String
"""All values less than or equal the given value."""
bio_lte: String
"""All values greater than the given value."""
bio_gt: String
"""All values greater than or equal the given value."""
bio_gte: String
"""All values containing the given string."""
bio_contains: String
"""All values not containing the given string."""
bio_not_contains: String
"""All values starting with the given string."""
bio_starts_with: String
"""All values not starting with the given string."""
bio_not_starts_with: String
"""All values ending with the given string."""
bio_ends_with: String
"""All values not ending with the given string."""
bio_not_ends_with: String
user: UserWhereInput
}
input ProfileWhereUniqueInput {
id: ID
}
enum Role {
ADMIN
CUSTOMER
}
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
}
input UserCreateInput {
id: ID
email: String
name: String!
role: Role
jsonData: Json
posts: PostCreateManyWithoutAuthorInput
profile: ProfileCreateOneWithoutUserInput
}
enum UserOrderByInput {
id_ASC
id_DESC
email_ASC
email_DESC
name_ASC
name_DESC
role_ASC
role_DESC
jsonData_ASC
jsonData_DESC
}
input UserWhereInput {
"""Logical AND on all given filters."""
AND: [UserWhereInput!]
"""Logical OR on all given filters."""
OR: [UserWhereInput!]
"""Logical NOT on all given filters combined by AND."""
NOT: [UserWhereInput!]
id: ID
"""All values that are not equal to given value."""
id_not: ID
"""All values that are contained in given list."""
id_in: [ID!]
"""All values that are not contained in given list."""
id_not_in: [ID!]
"""All values less than the given value."""
id_lt: ID
"""All values less than or equal the given value."""
id_lte: ID
"""All values greater than the given value."""
id_gt: ID
"""All values greater than or equal the given value."""
id_gte: ID
"""All values containing the given string."""
id_contains: ID
"""All values not containing the given string."""
id_not_contains: ID
"""All values starting with the given string."""
id_starts_with: ID
"""All values not starting with the given string."""
id_not_starts_with: ID
"""All values ending with the given string."""
id_ends_with: ID
"""All values not ending with the given string."""
id_not_ends_with: ID
email: String
"""All values that are not equal to given value."""
email_not: String
"""All values that are contained in given list."""
email_in: [String!]
"""All values that are not contained in given list."""
email_not_in: [String!]
"""All values less than the given value."""
email_lt: String
"""All values less than or equal the given value."""
email_lte: String
"""All values greater than the given value."""
email_gt: String
"""All values greater than or equal the given value."""
email_gte: String
"""All values containing the given string."""
email_contains: String
"""All values not containing the given string."""
email_not_contains: String
"""All values starting with the given string."""
email_starts_with: String
"""All values not starting with the given string."""
email_not_starts_with: String
"""All values ending with the given string."""
email_ends_with: String
"""All values not ending with the given string."""
email_not_ends_with: String
name: String
"""All values that are not equal to given value."""
name_not: String
"""All values that are contained in given list."""
name_in: [String!]
"""All values that are not contained in given list."""
name_not_in: [String!]
"""All values less than the given value."""
name_lt: String
"""All values less than or equal the given value."""
name_lte: String
"""All values greater than the given value."""
name_gt: String
"""All values greater than or equal the given value."""
name_gte: String
"""All values containing the given string."""
name_contains: String
"""All values not containing the given string."""
name_not_contains: String
"""All values starting with the given string."""
name_starts_with: String
"""All values not starting with the given string."""
name_not_starts_with: String
"""All values ending with the given string."""
name_ends_with: String
"""All values not ending with the given string."""
name_not_ends_with: String
role: Role
"""All values that are not equal to given value."""
role_not: Role
"""All values that are contained in given list."""
role_in: [Role!]
"""All values that are not contained in given list."""
role_not_in: [Role!]
posts_every: PostWhereInput
posts_some: PostWhereInput
posts_none: PostWhereInput
profile: ProfileWhereInput
}
您会注意到,新版本的 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
字段并按如下方式实现 posts
和 profile
关联关系的解析器来做到这一点
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
字段并按如下方式实现 author
和 categories
关联关系的解析器来做到这一点
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
解析器内部,数据库查询加载表示 Post
的 author
的 User
记录。在 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
字段并按如下方式实现 posts
和 profile
关联关系的解析器来做到这一点
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
first
、last
、before
和after
参数已删除- 新的
cursor
参数替换了before
和after
- 新的
take
参数替换了first
和last
以下是如何调整调用以使其符合新的 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 服务器。