prisma 绑定到 SDL 优先
概述
¥Overview
本升级指南介绍如何迁移基于 Prisma 1 并使用 prisma-binding
实现 GraphQL 服务器的 Node.js 项目。
¥This upgrade guide describes how to migrate a Node.js project that's based on Prisma 1 and uses prisma-binding
to implement a GraphQL server.
该代码将保留 SDL 优先方法 用于构建 GraphQL 模式。从 prisma-binding
迁移到 Prisma Client 时,主要区别在于 info
对象不能再用于自动解析关系,而是需要实现类型解析器以确保关系得到正确解析。
¥The code will keep the SDL-first approach for constructing the GraphQL schema. When migrating from prisma-binding
to Prisma Client, the main difference is that the info
object can't be used to resolve relations automatically any more, instead you'll need to implement your type resolvers to ensure that relations get resolved properly.
本指南假定你已经完成了 Prisma ORM 层升级指南。这意味着你已经:
¥The guide assumes that you already went through the guide for upgrading the Prisma ORM layer. This means you already:
-
安装 Prisma ORM 2 CLI
¥installed the Prisma ORM 2 CLI
-
创建了你的 Prisma ORM 2 架构
¥created your Prisma ORM 2 schema
-
内省你的数据库并解决潜在的架构不兼容性
¥introspected your database and resolved potential schema incompatibilities
-
安装并生成 Prisma 客户端
¥installed and generated Prisma Client
该指南进一步假设你有一个与此类似的文件设置:
¥The guide further assumes that you have a file setup that looks similar to this:
.
├── README.md
├── package.json
├── prisma
│ └── schema.prisma
├── prisma1
│ ├── datamodel.prisma
│ └── prisma.yml
└── src
├── generated
│ └── prisma.graphql
├── index.js
└── schema.graphql
重要的部分是:
¥The important parts are:
-
使用 Prisma ORM 2 架构以
prisma
调用的文件夹¥A folder called with
prisma
with your Prisma ORM 2 schema -
名为
src
的文件夹,其中包含你的应用代码和名为schema.graphql
的架构¥A folder called
src
with your application code and a schema calledschema.graphql
如果这不是你的项目结构,你需要调整指南中的说明以匹配你自己的设置。
¥If this is not what your project structure looks like, you'll need to adjust the instructions in the guide to match your own setup.
1. 调整你的 GraphQL 架构
¥ Adjusting your GraphQL schema
使用 prisma-binding
,定义 GraphQL 架构(有时称为 应用模式)的方法基于从生成的 prisma.graphql
文件导入 GraphQL 类型(在 Prisma 1 中,这通常称为 Prisma GraphQL 架构)。这些类型反映了 Prisma 1 数据模型中的类型,并作为 GraphQL API 的基础。
¥With prisma-binding
, your approach for defining your GraphQL schema (sometimes called application schema) is based on importing GraphQL types from the generated prisma.graphql
file (in Prisma 1, this is typically called Prisma GraphQL schema). These types mirror the types from your Prisma 1 datamodel and serve as foundation for your GraphQL API.
使用 Prisma ORM 2,你不再可以导入 prisma.graphql
文件。因此,你必须直接在 schema.graphql
文件中拼出 GraphQL 模式的所有类型。
¥With Prisma ORM 2, there's no prisma.graphql
file any more that you could import from. Therefore, you have to spell out all the types of your GraphQL schema directly inside your schema.graphql
file.
最简单的方法是从 GraphQL Playground 下载完整的 GraphQL 架构。为此,请打开 SCHEMA 选项卡并单击右上角的下载按钮,然后选择 SDL:
¥The easiest way to do so is by downloading the full GraphQL schema from the GraphQL Playground. To do so, open the SCHEMA tab and click the DOWNLOAD button in the top-right corner, then select SDL:
或者,你可以使用 GraphQL CLI 的 get-schema
命令下载完整架构:
¥Alternatively, you can use the get-schema
command of the GraphQL CLI to download your full schema:
npx graphql get-schema --endpoint __GRAPHQL_YOGA_ENDPOINT__ --output schema.graphql --no-all
注意:使用上述命令,你需要将
__GRAPHQL_YOGA_ENDPOINT__
占位符替换为 GraphQL Yoga 服务器的实际端点。¥Note: With the above command, you need to replace the
__GRAPHQL_YOGA_ENDPOINT__
placeholder with the actual endpoint of your GraphQL Yoga server.
获得 schema.graphql
文件后,用新内容替换 src/schema.graphql
中的当前版本。请注意,这两个架构 100% 等效,只是新架构不使用 graphql-import
从不同文件导入类型。相反,它在一个文件中详细说明了所有类型。
¥Once you obtained the schema.graphql
file, replace your current version in src/schema.graphql
with the new contents. Note that the two schemas are 100% equivalent, except that the new one doesn't use graphql-import
for importing types from a different file. Instead, it spells out all types in a single file.
以下是我们将在本指南中迁移的示例 GraphQL 模式的这两个版本的比较(你可以使用选项卡在两个版本之间切换):
¥Here's a comparison of these two versions of the sample GraphQL schema that we'll migrate in this guide (you can use the tabs to switch between the two versions):
- Before (with graphql-import)
- After (with 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 架构不仅定义了直接导入的模型,还定义了之前架构中不存在的其他类型(例如 input
类型)。
¥You'll notice that the new version of your GraphQL schema not only defines the models that were imported directly, but also additional types (e.g. input
types) that were not present in the schema before.
2. 设置你的 PrismaClient
实例
¥ Set up your PrismaClient
instance
PrismaClient
是 Prisma ORM 2 中数据库的新接口。它允许你调用各种方法来构建 SQL 查询并将其发送到数据库,以纯 JavaScript 对象的形式返回结果。
¥PrismaClient
is your new interface to the database in Prisma ORM 2. It lets you invoke various methods which build SQL queries and send them to the database, returning the results as plain JavaScript objects.
PrismaClient
查询 API 的灵感来自最初的 prisma-binding
API,因此你使用 Prisma Client 发送的许多查询都会感到熟悉。
¥The PrismaClient
query API is inspired by the initial prisma-binding
API, so a lot of the queries you send with Prisma Client will feel familiar.
与 Prisma 1 中的 prisma-binding
实例类似,你还希望将 Prisma ORM 2 中的 PrismaClient
连接到 GraphQL 的 context
,以便可以在解析器内部访问:
¥Similar to the prisma-binding
instance from Prisma 1, you also want to attach your PrismaClient
from Prisma ORM 2 to GraphQL's context
so that in can be accessed inside your resolvers:
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
),这只是一个示例,用于指示它的外观。
¥In the code block above, the red lines are the lines to be removed from your current setup, the green lines are the ones that you should add. Of course, it's possible that your previous setup differed from this one (e.g. it's unlikely that your Prisma ORM endpoint
was http://localhost:4466
if you're running your API in production), this is just a sample to indicate what it could look like.
当你现在访问解析器内部的 context.prisma
时,你现在可以访问 Prisma 客户端查询。
¥When you're now accessing context.prisma
inside of a resolver, you now have access to Prisma Client queries.
2. 编写 GraphQL 类型解析器
¥ Write your GraphQL type resolvers
prisma-binding
能够神奇地解析 GraphQL 模式中的关系。但是,当不使用 prisma-binding
时,你需要使用所谓的类型解析器显式解析你的关系。
¥prisma-binding
was able to magically resolve relations in your GraphQL schema. When not using prisma-binding
though, you need to explicitly resolve your relations using so-called type resolvers.
注意 你可以在本文中了解有关类型解析器的概念以及为什么需要它们的更多信息:GraphQL 服务器基础知识:GraphQL 模式、TypeDef 和解析器解释
¥You can learn more about the concept of type resolvers and why they're necessary in this article: GraphQL Server Basics: GraphQL Schemas, TypeDefs & Resolvers Explained
2.1.实现 User
类型的类型解析器
¥2.1. Implementing the type resolver for the User
type
我们的示例 GraphQL 架构中的 User
类型定义如下:
¥The User
type in our sample GraphQL schema is defined as follows:
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
}
这个类型有两个关系:
¥This type has two relations:
-
posts
字段表示与Post
的 1-n 关系¥The
posts
field denotes a 1-n relation toPost
-
profile
字段表示与Profile
的 1-1 关系¥The
profile
field denotes a 1-1 relation toProfile
由于你不再使用 prisma-binding
,因此你现在需要在类型解析器中解析这些关系 "manually"。
¥Since you're not using prisma-binding
any more, you now need to resolve these relations "manually" in type resolvers.
你可以通过向解析器映射添加 User
字段并实现 posts
和 profile
关系的解析器来实现此目的,如下所示:
¥You can do so by adding a User
field to your resolver map and implement the resolvers for the posts
and profile
relations as follows:
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
中的所有 Post
记录(其 id
包含在 parent
对象中)。在 profile
解析器内部,数据库查询从指定的 user
(其 id
在 parent
对象中携带)加载 Profile
记录。
¥Inside of these resolvers, you're using your new PrismaClient
to perform a query against the database. Inside the posts
resolver, the database query loads all Post
records from the specified author
(whose id
is carried in the parent
object). Inside the profile
resolver, the database query loads the Profile
record from the specified user
(whose id
is carried in the parent
object).
由于这些额外的解析器,现在每当你在查询中请求有关 User
类型的信息时,你现在都可以在 GraphQL 查询/突变中嵌套关系,例如:
¥Thanks to these extra resolvers, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about the User
type in a query, e.g.:
{
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
类型的类型解析器
¥2.2. Implementing the type resolver for the Post
type
我们的示例 GraphQL 架构中的 Post
类型定义如下:
¥The Post
type in our sample GraphQL schema is defined as follows:
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!]
}
这个类型有两个关系:
¥This type has two relations:
-
author
字段表示与User
的 1-n 关系¥The
author
field denotes a 1-n relation toUser
-
categories
字段表示与Category
的 m-n 关系¥The
categories
field denotes a m-n relation toCategory
由于你不再使用 prisma-binding
,因此你现在需要在类型解析器中解析这些关系 "manually"。
¥Since you're not using prisma-binding
any more, you now need to resolve these relations "manually" in type resolvers.
你可以通过向解析器映射添加 Post
字段并实现 author
和 categories
关系的解析器来实现此目的,如下所示:
¥You can do so by adding a Post
field to your resolver map and implement the resolvers for the author
and categories
relations as follows:
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
中的所有 Category
记录(其 id
包含在 parent
对象中)。
¥Inside of these resolvers, you're using your new PrismaClient
to perform a query against the database. Inside the author
resolver, the database query loads the User
record that represents the author
of the Post
. Inside the categories
resolver, the database query loads all Category
records from the specified post
(whose id
is carried in the parent
object).
由于这些额外的解析器,现在每当你在查询中请求有关 User
类型的信息时,你现在都可以在 GraphQL 查询/突变中嵌套关系,例如:
¥Thanks to these extra resolvers, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about the User
type in a query, e.g.:
{
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
类型的类型解析器
¥2.3. Implementing the type resolver for the Profile
type
我们的示例 GraphQL 架构中的 Profile
类型定义如下:
¥The Profile
type in our sample GraphQL schema is defined as follows:
type Profile implements Node {
id: ID!
bio: String
user: User!
}
这种类型有一个关系:user
字段表示与 User
的 1-n 关系。
¥This type has one relation: The user
field denotes a 1-n relation to User
.
由于你不再使用 prisma-binding
,因此你现在需要在类型解析器中解析此关系 "manually"。
¥Since you're not using prisma-binding
any more, you now need to resolve this relation "manually" in type resolvers.
你可以通过将 Profile
字段添加到解析器映射并实现 owner
关系的解析器来实现此目的,如下所示:
¥You can do so by adding a Profile
field to your resolver map and implement the resolvers for the owner
relation as follows:
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
记录。
¥Inside of this resolver, you're using your new PrismaClient
to perform a query against the database. Inside the user
resolver, the database query loads the User
records from the specified profile
(whose id
is carried in the parent
object).
借助这个额外的解析器,无论何时你在查询中请求有关 Profile
类型的信息,你现在都可以在 GraphQL 查询/突变中嵌套关系。
¥Thanks to this extra resolver, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about the Profile
type in a query.
2.4.实现 Category
类型的类型解析器
¥2.4. Implementing the type resolver for the Category
type
我们的示例 GraphQL 架构中的 Category
类型定义如下:
¥The Category
type in our sample GraphQL schema is defined as follows:
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 关系。
¥This type has one relation: The posts
field denotes a m-n relation to Post
.
由于你不再使用 prisma-binding
,因此你现在需要在类型解析器中解析此关系 "manually"。
¥Since you're not using prisma-binding
any more, you now need to resolve this relation "manually" in type resolvers.
你可以通过向解析器映射添加 Category
字段并实现 posts
和 profile
关系的解析器来实现此目的,如下所示:
¥You can do so by adding a Category
field to your resolver map and implement the resolvers for the posts
and profile
relations as follows:
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
中的所有 Post
记录(其 id
包含在 parent
对象中)。
¥Inside of this resolver, you're using your new PrismaClient
to perform a query against the database. Inside the posts
resolver, the database query loads all Post
records from the specified categories
(whose id
is carried in the parent
object).
借助这个额外的解析器,无论何时你在查询中请求有关 Category
类型的信息,你现在都可以在 GraphQL 查询/突变中嵌套关系。
¥Thanks to this extra resolver, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about a Category
type in a query.
所有类型解析器就位后,你就可以开始迁移实际的 GraphQL API 操作。
¥With all your type resolvers in place, you can start migrating the actual GraphQL API operations.
3. 迁移 GraphQL 操作
¥ Migrate GraphQL operations
3.1.迁移 GraphQL 查询
¥3.1. Migrate GraphQL queries
在本部分中,你将把所有 GraphQL 查询从 prisma-binding
迁移到 Prisma 客户端。
¥In this section, you'll migrate all GraphQL queries from prisma-binding
to Prisma Client.
3.1.1.迁移 users
查询(使用 forwardTo
)
¥3.1.1. Migrate the users
query (which uses forwardTo
)
在我们的示例 API 中,示例 GraphQL 架构中的 users
查询的定义和实现如下。
¥In our sample API, the users
query from the sample GraphQL schema is defined and implemented as follows.
使用 prisma-binding
的 SDL 架构定义
¥SDL schema definition with prisma-binding
type Query {
users(where: UserWhereInput, orderBy: Enumerable<UserOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
# ... other queries
}
使用 prisma-binding
实现解析器
¥Resolver implementation with prisma-binding
const resolvers = {
Query: {
users: forwardTo('prisma'),
// ... other resolvers
},
}
使用 Prisma 客户端实现 users
解析器
¥Implementing the users
resolver with Prisma Client
要重新实现之前使用 forwardTo
的查询,想法是将传入的过滤、排序和分页参数传递给 PrismaClient
:
¥To re-implement queries that were previously using forwardTo
, the idea is to pass the incoming filtering, ordering and pagination arguments to 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 包来确保兼容性:
¥Note that this approach does not work yet because the structures of the incoming arguments is different from the ones expected by PrismaClient
. To ensure the structures are compatible, you can use the @prisma/binding-argument-transform
npm package which ensures compatibility:
npm install @prisma/binding-argument-transform
你现在可以按如下方式使用该包:
¥You can now use this package as follows:
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:
¥The last remaining issue with this are the pagination arguments. Prisma ORM 2 introduces a new pagination API:
-
删除了
first
、last
、before
和after
参数¥The
first
,last
,before
andafter
arguments are removed -
新的
cursor
参数取代了before
和after
¥The new
cursor
argument replacesbefore
andafter
-
新的
take
参数取代了first
和last
¥The new
take
argument replacesfirst
andlast
以下是你可以调整调用以使其符合新的 Prisma 客户端分页 API 的方法:
¥Here is how you can adjust the call to make it compliant with the new Prisma Client pagination 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 客户端 API 中的分页参数。
¥The calculations are needed to ensure the incoming pagination arguments map properly to the ones from the Prisma Client API.
3.1.2.迁移 posts(searchString: String): [Post!]!
查询
¥3.1.2. Migrate the posts(searchString: String): [Post!]!
query
posts
查询的定义和实现如下。
¥The posts
query is defined and implemented as follows.
使用 prisma-binding
的 SDL 架构定义
¥SDL schema definition with prisma-binding
type Query {
posts(searchString: String): [Post!]!
# ... other queries
}
使用 prisma-binding
实现解析器
¥Resolver implementation with 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 客户端实现 posts
解析器
¥Implementing the posts
resolver with Prisma Client
要在新的 Prisma 客户端中获得相同的行为,你需要调整解析器实现:
¥To get the same behavior with the new Prisma Client, you'll need to adjust your resolver implementation:
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 中发送相应的查询:
¥You can now send the respective query in the GraphQL Playground:
{
posts {
id
title
author {
id
name
}
}
}
3.1.3.迁移 user(uniqueInput: UserUniqueInput): User
查询
¥3.1.3. Migrate the user(uniqueInput: UserUniqueInput): User
query
在我们的示例应用中,user
查询的定义和实现如下。
¥In our sample app, the user
query is defined and implemented as follows.
使用 prisma-binding
的 SDL 架构定义
¥SDL schema definition with prisma-binding
type Query {
user(userUniqueInput: UserUniqueInput): User
# ... other queries
}
input UserUniqueInput {
id: String
email: String
}
使用 prisma-binding
实现解析器
¥Resolver implementation with prisma-binding
const resolvers = {
Query: {
user: (_, args, context, info) => {
return context.prisma.query.user(
{
where: args.userUniqueInput,
},
info
)
},
// ... other resolvers
},
}
使用 Prisma 客户端实现 user
解析器
¥Implementing the user
resolver with Prisma Client
要在新的 Prisma 客户端中获得相同的行为,你需要调整解析器实现:
¥To get the same behavior with the new Prisma Client, you'll need to adjust your resolver implementation:
const resolvers = {
Query: {
user: (_, args, context) => {
return context.prisma.user.findUnique({
where: args.userUniqueInput,
})
},
// ... other resolvers
},
}
你现在可以通过 GraphQL Playground 发送相应的查询:
¥You can now send the respective query via the GraphQL Playground:
{
user(userUniqueInput: { email: "alice@prisma.io" }) {
id
name
}
}
3.1.迁移 GraphQL 突变
¥3.1. Migrate GraphQL mutations
在本部分中,你将从示例架构中迁移 GraphQL 突变。
¥In this section, you'll migrate the GraphQL mutations from the sample schema.
3.1.2.迁移 createUser
突变(使用 forwardTo
)
¥3.1.2. Migrate the createUser
mutation (which uses forwardTo
)
在示例应用中,示例 GraphQL 架构中的 createUser
突变的定义和实现如下。
¥In the sample app, the createUser
mutation from the sample GraphQL schema is defined and implemented as follows.
使用 prisma-binding
的 SDL 架构定义
¥SDL schema definition with prisma-binding
type Mutation {
createUser(data: UserCreateInput!): User!
# ... other mutations
}
使用 prisma-binding
实现解析器
¥Resolver implementation with prisma-binding
const resolvers = {
Mutation: {
createUser: forwardTo('prisma'),
// ... other resolvers
},
}
使用 Prisma 客户端实现 createUser
解析器
¥Implementing the createUser
resolver with Prisma Client
要在新的 Prisma 客户端中获得相同的行为,你需要调整解析器实现:
¥To get the same behavior with the new Prisma Client, you'll need to adjust your resolver implementation:
const resolvers = {
Mutation: {
createUser: (_, args, context, info) => {
return context.prisma.user.create({
data: args.data,
})
},
// ... other resolvers
},
}
你现在可以针对新 API 编写第一个突变,例如:
¥You can now write your first mutation against the new API, e.g.:
mutation {
createUser(data: { name: "Alice", email: "alice@prisma.io" }) {
id
}
}
3.1.3.迁移 createDraft(title: String!, content: String, authorId: String!): Post!
查询
¥3.1.3. Migrate the createDraft(title: String!, content: String, authorId: String!): Post!
query
在示例应用中,createDraft
突变的定义和实现如下。
¥In the sample app, the createDraft
mutation is defined and implemented as follows.
使用 prisma-binding
的 SDL 架构定义
¥SDL schema definition with prisma-binding
type Mutation {
createDraft(title: String!, content: String, authorId: String!): Post!
# ... other mutations
}
使用 prisma-binding
实现解析器
¥Resolver implementation with 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 客户端实现 createDraft
解析器
¥Implementing the createDraft
resolver with Prisma Client
要在新的 Prisma 客户端中获得相同的行为,你需要调整解析器实现:
¥To get the same behavior with the new Prisma Client, you'll need to adjust your resolver implementation:
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 发送相应的突变:
¥You can now send the respective mutation via the GraphQL Playground:
mutation {
createDraft(title: "Hello World", authorId: "__AUTHOR_ID__") {
id
published
author {
id
name
}
}
}
3.1.4.迁移 updateBio(bio: String, userUniqueInput: UserUniqueInput!): User
突变
¥3.1.4. Migrate the updateBio(bio: String, userUniqueInput: UserUniqueInput!): User
mutation
在示例应用中,updateBio
突变的定义和实现如下。
¥In the sample app, the updateBio
mutation is defined and implemented as follows.
使用 prisma-binding
的 SDL 架构定义
¥SDL schema definition with prisma-binding
type Mutation {
updateBio(bio: String!, userUniqueInput: UserUniqueInput!): User
# ... other mutations
}
使用 prisma-binding
实现解析器
¥Resolver implementation with 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 客户端实现 updateBio
解析器
¥Implementing the updateBio
resolver with Prisma Client
要使用 Prisma Client 获得相同的行为,你需要调整解析器实现:
¥To get the same behavior with Prisma Client, you'll need to adjust your resolver implementation:
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 发送相应的突变:
¥You can now send the respective mutation via the 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
突变
¥3.1.5. Migrate the addPostToCategories(postId: String!, categoryIds: [String!]!): Post
mutation
在我们的示例应用中,addPostToCategories
突变的定义和实现如下。
¥In our sample app, the addPostToCategories
mutation is defined and implemented as follows.
使用 prisma-binding
的 SDL 架构定义
¥SDL schema definition with prisma-binding
type Mutation {
addPostToCategories(postId: String!, categoryIds: [String!]!): Post
# ... other mutations
}
使用 prisma-binding
实现解析器
¥Resolver implementation with 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 客户端实现 addPostToCategories
解析器
¥Implementing the addPostToCategories
resolver with Prisma Client
要使用 Prisma Client 获得相同的行为,你需要调整解析器实现:
¥To get the same behavior with Prisma Client, you'll need to adjust your resolver implementation:
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 发送相应的查询:
¥You can now send the respective query via the GraphQL Playground:
mutation {
addPostToCategories(
postId: "__AUTHOR_ID__"
categoryIds: ["__CATEGORY_ID_1__", "__CATEGORY_ID_2__"]
) {
id
title
categories {
id
name
}
}
}
4. 打扫干净
¥ Cleaning up
由于整个应用现已升级到 Prisma ORM 2,你可以删除所有不必要的文件并删除不再需要的依赖。
¥Since the entire app has now been upgraded to Prisma ORM 2, you can delete all unnecessary files and remove the no longer needed dependencies.
4.1.清理 npm 依赖
¥4.1. Clean up npm dependencies
你可以首先删除与 Prisma 1 设置相关的 npm 依赖:
¥You can start by removing npm dependencies that were related to the Prisma 1 setup:
npm uninstall graphql-cli prisma-binding prisma1
4.2.删除未使用的文件
¥4.2. Delete unused files
接下来,删除 Prisma 1 设置的文件:
¥Next, delete the files of your Prisma 1 setup:
rm prisma1/datamodel.prisma prisma1/prisma.yml
4.3.停止 Prisma ORM 服务器
¥4.3. Stop the Prisma ORM server
最后,你可以停止运行 Prisma ORM 服务器。
¥Finally, you can stop running your Prisma ORM server.