prisma-binding から SDL-first へ
概要
このアップグレードガイドでは、Prisma 1 に基づき、GraphQL サーバーを実装するために prisma-binding
を使用している Node.js プロジェクトを移行する方法について説明します。
コードは、GraphQL スキーマを構築するために SDL-first アプローチ を維持します。prisma-binding
から Prisma Client に移行する場合、主な違いは、リレーションを自動的に解決するために info
オブジェクトを使用できなくなることです。代わりに、リレーションが適切に解決されるように、型リゾルバー を実装する必要があります。
このガイドは、すでに Prisma ORM レイヤーをアップグレードするためのガイド を完了していることを前提としています。これは、すでに以下のことを行っていることを意味します。
- Prisma ORM 2 CLI をインストール済み
- Prisma ORM 2 スキーマを作成済み
- データベースをイントロスペクションし、潜在的なスキーマの非互換性を解決済み
- Prisma Client をインストールおよび生成済み
このガイドはさらに、次のようなファイルセットアップがあることを前提としています。
.
├── README.md
├── package.json
├── prisma
│ └── schema.prisma
├── prisma1
│ ├── datamodel.prisma
│ └── prisma.yml
└── src
├── generated
│ └── prisma.graphql
├── index.js
└── schema.graphql
重要な部分は次のとおりです。
- Prisma ORM 2 スキーマを含む
prisma
という名前のフォルダ - アプリケーションコードと
schema.graphql
という名前のスキーマを含むsrc
という名前のフォルダ
プロジェクト構造がこれと異なる場合は、ガイドの手順を独自のセットアップに合わせて調整する必要があります。
1. GraphQL スキーマの調整
prisma-binding
を使用すると、GraphQL スキーマ(アプリケーションスキーマ とも呼ばれる)を定義するアプローチは、生成された prisma.graphql
ファイル(Prisma 1 では、通常 Prisma GraphQL スキーマ と呼ばれる)から GraphQL 型をインポートすることに基づいています。これらの型は、Prisma 1 データモデルの型を反映しており、GraphQL API の基礎として機能します。
Prisma ORM 2 では、インポートできる prisma.graphql
ファイルはもうありません。したがって、schema.graphql
ファイル内に GraphQL スキーマのすべての型を直接記述する必要があります。
最も簡単な方法は、GraphQL Playground から完全な GraphQL スキーマをダウンロードすることです。これを行うには、SCHEMA タブを開き、右上隅にある DOWNLOAD ボタンをクリックし、SDL を選択します。
または、GraphQL CLI の get-schema
コマンドを使用して、完全なスキーマをダウンロードすることもできます。
npx graphql get-schema --endpoint __GRAPHQL_YOGA_ENDPOINT__ --output schema.graphql --no-all
注:上記のコマンドでは、
__GRAPHQL_YOGA_ENDPOINT__
プレースホルダーを GraphQL Yoga サーバーの実際のエンドポイントに置き換える必要があります。
schema.graphql
ファイルを取得したら、src/schema.graphql
内の現在のバージョンを新しいコンテンツに置き換えます。2 つのスキーマは 100% 同等ですが、新しいスキーマは、別のファイルから型をインポートするために graphql-import
を使用していない点が異なります。代わりに、すべての型を単一のファイルに記述します。
このガイドで移行するサンプル GraphQL スキーマの 2 つのバージョンを比較してみましょう(タブを切り替えて 2 つのバージョンを切り替えることができます)。
- 以前(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 スキーマは、直接インポートされた モデル だけでなく、以前のスキーマには存在しなかった追加の型(例: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://#:4466',
}),
prisma: new PrismaClient(),
}),
})
上記のコードブロックでは、赤 の線は現在のセットアップから削除される線、緑 の線は追加する必要がある線です。もちろん、以前のセットアップがこれと異なっていた可能性はあります(たとえば、本番環境で API を実行している場合、Prisma ORM の endpoint
が https://#:4466
であった可能性は低いでしょう)。これは、どのようなものになり得るか を示すための単なるサンプルです。
リゾルバー内で context.prisma
にアクセスすると、Prisma Client クエリにアクセスできるようになります。
2. GraphQL 型リゾルバーの記述
prisma-binding
は、GraphQL スキーマ内のリレーションを 魔法のように 解決できました。ただし、prisma-binding
を使用しない場合は、いわゆる 型リゾルバー を使用してリレーションを明示的に解決する必要があります。
注 型リゾルバーの概念と、なぜそれが必要なのかについて、詳しくはこちらの記事をご覧ください:GraphQL サーバーの基礎:GraphQL スキーマ、TypeDefs、リゾルバーの説明
2.1. User
型の型リゾルバーの実装
サンプル GraphQL スキーマの User
型は、次のように定義されています。
type User implements Node {
id: ID!
email: String
name: String!
posts(
where: PostWhereInput
orderBy: Enumerable<PostOrderByInput>
skip: Int
after: String
before: String
first: Int
last: Int
): [Post!]
role: Role!
profile: Profile
jsonData: Json
}
この型には 2 つのリレーションがあります。
posts
フィールドはPost
への 1 対多のリレーションを示します。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 クエリ/ミューテーションでリレーションをネストできるようになります。例:
{
users {
id
name
posts {
# fetching this relation is enabled by the new type resolver
id
title
}
profile {
# fetching this relation is enabled by the new type resolver
id
bio
}
}
}
2.2. Post
型の型リゾルバーの実装
サンプル GraphQL スキーマの Post
型は、次のように定義されています。
type Post implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
title: String!
content: String
published: Boolean!
author: User
categories(
where: CategoryWhereInput
orderBy: Enumerable<CategoryOrderByInput>
skip: Int
after: String
before: String
first: Int
last: Int
): [Category!]
}
この型には 2 つのリレーションがあります。
author
フィールドはUser
への 1 対多のリレーションを示します。categories
フィールドはCategory
への多対多のリレーションを示します。
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 クエリ/ミューテーションでリレーションをネストできるようになります。例:
{
posts {
id
title
author {
# fetching this relation is enabled by the new type resolver
id
name
}
categories {
# fetching this relation is enabled by the new type resolver
id
name
}
}
}
2.3. Profile
型の型リゾルバーの実装
サンプル GraphQL スキーマの Profile
型は、次のように定義されています。
type Profile implements Node {
id: ID!
bio: String
user: User!
}
この型には 1 つのリレーションがあります。user
フィールドは User
への 1 対多のリレーションを示します。
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 クエリ/ミューテーションでリレーションをネストできるようになります。
2.4. Category
型の型リゾルバーの実装
サンプル GraphQL スキーマの Category
型は、次のように定義されています。
type Category implements Node {
id: ID!
name: String!
posts(
where: PostWhereInput
orderBy: Enumerable<PostOrderByInput>
skip: Int
after: String
before: String
first: Int
last: Int
): [Post!]
}
この型には 1 つのリレーションがあります。posts
フィールドは Post
への多対多のリレーションを示します。
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 クエリ/ミューテーションでリレーションをネストできるようになります。
すべての型リゾルバーを配置したら、実際の GraphQL API 操作の移行を開始できます。
3. GraphQL 操作の移行
3.1. GraphQL クエリの移行
このセクションでは、すべての GraphQL クエリ を prisma-binding
から Prisma Client に移行します。
3.1.1. users
クエリの移行(forwardTo
を使用)
サンプル API では、サンプル GraphQL スキーマの users
クエリは次のように定義および実装されています。
prisma-binding
を使用した SDL スキーマ定義
type Query {
users(where: UserWhereInput, orderBy: Enumerable<UserOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
# ... other queries
}
prisma-binding
を使用したリゾルバー実装
const resolvers = {
Query: {
users: forwardTo('prisma'),
// ... other resolvers
},
}
Prisma Client を使用した users
リゾルバーの実装
以前に forwardTo
を使用していたクエリを再実装するためのアイデアは、受信したフィルタリング、順序付け、およびページネーション引数を PrismaClient
に渡すことです。
const resolvers = {
Query: {
users: (_, args, context, info) => {
// this doesn't work yet
const { where, orderBy, skip, first, last, after, before } = args
return context.prisma.user.findMany({
where,
orderBy,
skip,
first,
last,
after,
before,
})
},
// ... other resolvers
},
}
このアプローチは、受信引数の 構造 が PrismaClient
が予期する構造と異なるため、まだ機能しません。構造の互換性を確保するには、互換性を保証する @prisma/binding-argument-transform
npm パッケージを使用できます。
npm install @prisma/binding-argument-transform
このパッケージは、次のように使用できます。
const {
makeOrderByPrisma2Compatible,
makeWherePrisma2Compatible,
} = require('@prisma/binding-argument-transform')
const resolvers = {
Query: {
users: (_, args, context, info) => {
// this still doesn't entirely work
const { where, orderBy, skip, first, last, after, before } = args
const prisma2Where = makeWherePrisma2Compatible(where)
const prisma2OrderBy = makeOrderByPrisma2Compatible(orderBy)
return context.prisma.user.findMany({
where: prisma2Where,
orderBy: prisma2OrderBy,
skip,
first,
last,
after,
before,
})
},
// ... other resolvers
},
}
これに関する最後の残りの問題は、ページネーション引数です。Prisma ORM 2 では、新しいページネーション API が導入されています。
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 スキーマ定義
type Query {
posts(searchString: String): [Post!]!
# ... other queries
}
prisma-binding
を使用したリゾルバー実装
const resolvers = {
Query: {
posts: (_, args, context, info) => {
return context.prisma.query.posts(
{
where: {
OR: [
{ title_contains: args.searchString },
{ content_contains: args.searchString },
],
},
},
info
)
},
// ... other resolvers
},
}
Prisma Client を使用した posts
リゾルバーの実装
新しい Prisma Client で同じ動作を実現するには、リゾルバーの実装を調整する必要があります。
const resolvers = {
Query: {
posts: (_, args, context) => {
return context.prisma.post.findMany({
where: {
OR: [
{ title: { contains: args.searchString } },
{ content: { contains: args.searchString } },
],
},
})
},
// ... other resolvers
},
}
GraphQL Playground でそれぞれのクエリを送信できるようになりました。
{
posts {
id
title
author {
id
name
}
}
}
3.1.3. user(uniqueInput: UserUniqueInput): User
クエリの移行
サンプルアプリでは、user
クエリは次のように定義および実装されています。
prisma-binding
を使用した SDL スキーマ定義
type Query {
user(userUniqueInput: UserUniqueInput): User
# ... other queries
}
input UserUniqueInput {
id: String
email: String
}
prisma-binding
を使用したリゾルバー実装
const resolvers = {
Query: {
user: (_, args, context, info) => {
return context.prisma.query.user(
{
where: args.userUniqueInput,
},
info
)
},
// ... other resolvers
},
}
Prisma Client を使用した user
リゾルバーの実装
新しい Prisma Client で同じ動作を実現するには、リゾルバーの実装を調整する必要があります。
const resolvers = {
Query: {
user: (_, args, context) => {
return context.prisma.user.findUnique({
where: args.userUniqueInput,
})
},
// ... other resolvers
},
}
GraphQL Playground を介してそれぞれのクエリを送信できるようになりました。
{
user(userUniqueInput: { email: "alice@prisma.io" }) {
id
name
}
}
3.1. GraphQL ミューテーションの移行
このセクションでは、サンプルスキーマから GraphQL ミューテーションを移行します。
3.1.2. createUser
ミューテーションの移行(forwardTo
を使用)
サンプルアプリでは、サンプル GraphQL スキーマの createUser
ミューテーションは次のように定義および実装されています。
prisma-binding
を使用した SDL スキーマ定義
type Mutation {
createUser(data: UserCreateInput!): User!
# ... other mutations
}
prisma-binding
を使用したリゾルバー実装
const resolvers = {
Mutation: {
createUser: forwardTo('prisma'),
// ... other resolvers
},
}
Prisma Client を使用した createUser
リゾルバーの実装
新しい Prisma Client で同じ動作を実現するには、リゾルバーの実装を調整する必要があります。
const resolvers = {
Mutation: {
createUser: (_, args, context, info) => {
return context.prisma.user.create({
data: args.data,
})
},
// ... other resolvers
},
}
新しい API に対して最初のミューテーションを記述できるようになりました。例:
mutation {
createUser(data: { name: "Alice", email: "alice@prisma.io" }) {
id
}
}
3.1.3. createDraft(title: String!, content: String, authorId: String!): Post!
クエリの移行
サンプルアプリでは、createDraft
ミューテーションは次のように定義および実装されています。
prisma-binding
を使用した SDL スキーマ定義
type Mutation {
createDraft(title: String!, content: String, authorId: String!): Post!
# ... other mutations
}
prisma-binding
を使用したリゾルバー実装
const resolvers = {
Mutation: {
createDraft: (_, args, context, info) => {
return context.prisma.mutation.createPost(
{
data: {
title: args.title,
content: args.content,
author: {
connect: {
id: args.authorId,
},
},
},
},
info
)
},
// ... other resolvers
},
}
Prisma Client を使用した createDraft
リゾルバーの実装
新しい Prisma Client で同じ動作を実現するには、リゾルバーの実装を調整する必要があります。
const resolvers = {
Mutation: {
createDraft: (_, args, context, info) => {
return context.prisma.post.create({
data: {
title: args.title,
content: args.content,
author: {
connect: {
id: args.authorId,
},
},
},
})
},
// ... other resolvers
},
}
GraphQL Playground を介してそれぞれのミューテーションを送信できるようになりました。
mutation {
createDraft(title: "Hello World", authorId: "__AUTHOR_ID__") {
id
published
author {
id
name
}
}
}
3.1.4. updateBio(bio: String, userUniqueInput: UserUniqueInput!): User
ミューテーション
サンプルアプリでは、updateBio
ミューテーションは次のように定義および実装されています。
prisma-binding
を使用した SDL スキーマ定義
type Mutation {
updateBio(bio: String!, userUniqueInput: UserUniqueInput!): User
# ... other mutations
}
prisma-binding
を使用したリゾルバー実装
const resolvers = {
Mutation: {
updateBio: (_, args, context, info) => {
return context.prisma.mutation.updateUser(
{
data: {
profile: {
update: { bio: args.bio },
},
},
where: { id: args.userId },
},
info
)
},
// ... other resolvers
},
}
Prisma Client を使用した updateBio
リゾルバーの実装
Prisma Client で同じ動作を実現するには、リゾルバーの実装を調整する必要があります。
const resolvers = {
Mutation: {
updateBio: (_, args, context, info) => {
return context.prisma.user.update({
data: {
profile: {
update: { bio: args.bio },
},
},
where: args.userUniqueInput,
})
},
// ... other resolvers
},
}
GraphQL Playground を介してそれぞれのミューテーションを送信できるようになりました。
mutation {
updateBio(
userUniqueInput: { email: "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 スキーマ定義
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 サーバーの実行を停止できます。