prisma-bindingからSDL-firstへ
概要
このアップグレードガイドでは、Prisma 1に基づき、prisma-binding
を使用してGraphQLサーバーを実装している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
ファイルはもうありません。そのため、GraphQLスキーマのすべての型をschema.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 Server Basics: GraphQL Schemas, TypeDefs & Resolvers Explained
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サーバーの実行を停止できます。