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

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

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 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(そのidparentオブジェクトで渡されます)からのすべてのPostレコードをデータベースクエリがロードします。profileリゾルバー内では、指定されたuser(そのidparentオブジェクトで渡されます)からの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リゾルバー内では、指定されたpost(そのidparentオブジェクトで渡されます)からのすべての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(そのidparentオブジェクトで渡されます)からの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(そのidparentオブジェクトで渡されます)からのすべての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が導入されました。

  • firstlastbeforeafter引数は削除されました。
  • 新しい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サーバーの実行を停止できます。

© . All rights reserved.