メインコンテンツにスキップ

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 を選択します。

Downloading the GraphQL schema with GraphQL Playground

または、GraphQL CLIget-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 つのバージョンを切り替えることができます)。

# import Post from './generated/prisma.graphql'
# import User from './generated/prisma.graphql'
# import Category from './generated/prisma.graphql'

type Query {
posts(searchString: String): [Post!]!
user(userUniqueInput: UserUniqueInput!): User
users(where: UserWhereInput, orderBy: Enumerable<UserOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
allCategories: [Category!]!
}

input UserUniqueInput {
id: String
email: String
}

type Mutation {
createDraft(authorId: ID!, title: String!, content: String!): Post
publish(id: ID!): Post
deletePost(id: ID!): Post
signup(name: String!, email: String!): User!
updateBio(userId: String!, bio: String!): User
addPostToCategories(postId: String!, categoryIds: [String!]!): Post
}

新しいバージョンの GraphQL スキーマは、直接インポートされた モデル だけでなく、以前のスキーマには存在しなかった追加の型(例: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 の endpointhttps://#: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 リゾルバー内では、データベースクエリは指定された authoridparent オブジェクトに含まれている)からすべての Post レコードをロードします。profile リゾルバー内では、データベースクエリは指定された useridparent オブジェクトに含まれている)から 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 リゾルバー内では、データベースクエリは Postauthor を表す User レコードをロードします。categories リゾルバー内では、データベースクエリは指定された postidparent オブジェクトに含まれている)からすべての 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 リゾルバー内では、データベースクエリは指定された profileidparent オブジェクトに含まれている)から 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 リゾルバー内では、データベースクエリは指定された categoriesidparent オブジェクトに含まれている)からすべての 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 が導入されています。

  • firstlastbefore、および after 引数は削除されました。
  • 新しい cursor 引数が beforeafter を置き換えます。
  • 新しい take 引数が firstlast を置き換えます。

新しい Prisma Client ページネーション API に準拠するように呼び出しを調整する方法を次に示します。

const {
makeOrderByPrisma2Compatible,
makeWherePrisma2Compatible,
} = require('@prisma/binding-argument-transform')

const resolvers = {
Query: {
users: (_, args, context) => {
const { where, orderBy, skip, first, last, after, before } = args
const prisma2Where = makeWherePrisma2Compatible(where)
const prisma2OrderBy = makeOrderByPrisma2Compatible(orderBy)
const skipValue = skip || 0
const prisma2Skip = Boolean(before) ? skipValue + 1 : skipValue
const prisma2Take = Boolean(last) ? -last : first
const prisma2Before = { id: before }
const prisma2After = { id: after }
const prisma2Cursor =
!Boolean(before) && !Boolean(after)
? undefined
: Boolean(before)
? prisma2Before
: prisma2After
return context.prisma.user.findMany({
where: prisma2Where,
orderBy: prisma2OrderBy,
skip: prisma2Skip,
cursor: prisma2Cursor,
take: prisma2Take,
})
},
// ... other resolvers
},
}

受信したページネーション引数が Prisma Client API の引数に適切にマッピングされるようにするために、計算が必要です。

3.1.2. posts(searchString: String): [Post!]! クエリの移行

posts クエリは、次のように定義および実装されています。

prisma-binding を使用した SDL スキーマ定義
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 サーバーの実行を停止できます。