WordPressサイト・テーマ・プラグイン向けGraphQLスキーマのマッピング
既存のWordPressサイトにGraphQLを導入することを決めたとします。素晴らしいですね!新しい機能にも既存の機能にも、GraphQLは基盤となるデータ層と連携する必要があります。そのため、アプリケーションのデータモデル(WordPressサイト内のカスタムPHPコード、テーマ、またはプラグイン)をGraphQLスキーマにマッピングする必要があります。
マッピングはどのように行うべきでしょうか?一度に全て行わなければならないのでしょうか?既存のデータモデルの完全なレプリカにすべきでしょうか?その過程で不適切な名前を調整することについてはどうでしょうか?技術的負債については、維持すべきか対処すべきでしょうか?
既存のWordPressアプリケーションのデータモデルをGraphQLスキーマにマッピングするための戦略をいくつか探っていきましょう。
自分のペースでスキーマをマッピングする
アプリケーションにGraphQLを追加することは、全てか無かの選択ではありません。同じアプリケーションが複数のAPIで同時に動作することもあり、その場合GraphQLは必要な期間、他のAPIと共存します。例えば、既存の機能はRESTで動かし続けながら、新しい機能にのみGraphQLを導入することができます。
GraphQLへの完全な移行を行いたい場合でも、一度に全て移行する必要はありません。既存の機能を少しずつ、着実にGraphQLへ移行していき、いつかGraphQLがアプリケーション唯一のAPIになる日が来るでしょう。
したがって、1日目に完全なGraphQLスキーマを作成することもできますが、必ずしもそうする必要はありません。どの時点でも、機能に必要なエンティティのみがスキーマ上に存在していれば十分です(型、フィールド、インターフェースを通じて)。時間の経過とともに、段階的にマッピングしていくことができます。
インターフェースに実装の負担を負わせない
GraphQLサーバーはアプリケーションのデータにアクセスするためのロジックを実装します。これはWordPressの機能を呼び出すことによって行われます。例えば、投稿データを取得するためにget_postsを呼び出します。この層では、リゾルバーを満たすためのPHPコードが存在します。
しかし、GraphQLスキーマはインターフェースです。APIでデータにアクセスするための契約を宣言します。実装の詳細には関与しません。WordPressについても、関数get_postsについても、DBテーブルwp_postsについても、SQLクエリについても何も知りません。
したがって、できる限りレイヤー間で情報が漏洩しないようにすべきです。
これが重要な理由は、データモデルが実装によって歪められることが多いからです。WordPressはこの明確な例を"attachment" CPTで示しています。これは画像などのメディアファイルを表すために使われています。
Custom Post Typeであるため、画像は投稿として扱われます。そのため、Post型を使ってメディアファイルを表したくなるかもしれません。この型には次のフィールドが含まれます:
type Post {
id: ID!
title: String
content: String
excerpt: String
}しかし、これはアプリケーションにとって適切ではないかもしれません。「content」フィールドの意味は投稿については明確ですが、画像については明確ではありません。おそらく、そこに属すべきではないでしょう。
画像がWordPressでCPTとしてモデル化されたのは、既存のロジックを再利用でき、既存のwp_postsテーブルに保存できるという利便性からでした。
しかし、便利であることは適切であることを意味せず、最終的に技術的負債につながる可能性があります(つまり、破壊的変更なしには修正できない欠陥のあるコードが、本来よりも長くアプリケーションに残り続ける状況です)。
できる限り、アプリケーションに技術的負債を抱えないようにしたいものです。機会があれば修正すべきです。データモデルをGraphQLスキーマにマッピングすることは、そのような機会を提供し、データインターフェース層で問題を修正することを可能にします。
(ただし、技術的負債はアプリケーションレベルでは依然として残るため、問題を完全には解決していませんが、できる範囲で軽減しています。)
この考えを実践してみましょう。メディアファイルを表すためにPost型を使う代わりに、画像エンティティにとって意味のあるプロパティのみを含むMedia型を持つ方が理にかなっています:
type Media {
id: ID!
src: String!
width: Int
height: Int
}内部実装レベルでは、フィールドリゾルバーは引き続きget_posts関数を実行してMedia型のエントリを解決しますが、それはGraphQLスキーマには関係のないことです。
GraphQLスキーマをDBダイアグラムから切り離す
WordPressはこのDBエンティティ関係ダイアグラムの上に実装されています:

GraphQLスキーマはDBダイアグラムを基にする必要がありますが、1対1のレプリカを作ろうとすべきではありません。GraphQLスキーマとDBダイアグラムはそれぞれ異なる前提条件や制約のもとで構築されており、もう一方には適用されないからです。
前のセクションでその例を示しました。テーブルwp_postsは画像CPTのデータを保存していますが、GraphQLではPostとMediaという2つの異なる型が存在します。
別の例として、カテゴリーを考えてみましょう。WordPressでは、投稿はカテゴリー(1つ以上)を持つことができ、任意のCPTも独自のカテゴリーを作成できます。例えば、「event」というCPTは「event_category」を持ちます。
投稿カテゴリーとイベントカテゴリーはどちらもwp_termsテーブルに保存されています。これにより、WordPressはSQLクエリを実行する際にどちらのカテゴリー型の行も取得しやすくなります。
そのため、投稿とイベントの両方から参照されるCategory型を使ってカテゴリーをマッピングしたくなるかもしれません:
type Category {
id: ID!
name: String!
}
type Post {
categories: [Category]!
}
type Event {
categories: [Category]!
}しかし、投稿は常に投稿カテゴリーを持ち、イベントは常にイベントカテゴリーを持ちます。これら2つのカテゴリー型のデータは同じDBテーブルに保存されていますが、アプリケーションレベルでは混在しません。投稿カテゴリーとイベントカテゴリーは2つの異なるエンティティです。
GraphQLは静的型システムを持っています。GraphQLを最大限に活用するには、アプリケーションレベルで異なるエンティティはGraphQLスキーマで異なる型を使ってモデル化する必要があります。
この場合、カテゴリーをGraphQLスキーマにマッピングする際は、それぞれに異なる型を作成すべきです:PostCategoryとEventCategory。そして、Post型はPostCategoryのみを参照し、Event型はEventCategoryのみを参照します:
type PostCategory {
id: ID!
name: String!
}
type Post {
categories: [PostCategory]!
}
type EventCategory {
id: ID!
name: String!
}
type Event {
categories: [EventCategory]!
}スキーマ内で全カテゴリーを包含するエンティティが必要な場合は、インターフェースCategoryで実現できます:
interface Category {
name: String!
}
type PostCategory implements Category {
id: ID!
name: String!
}
type EventCategory implements Category {
id: ID!
name: String!
}こうすることで、APIにアクセスするユーザーは、DBダイアグラムでどのようにマッピングされているか、DBにどのように保存されているかに関係なく、どのデータが取得されるかを明確に理解できます。
最終的なGraphQLスキーマが完成すると、その形状がWordPressのDBダイアグラムにある程度似ているものの、明らかに異なることがわかります:

静的型付けに従い、フィールドの命名を適応させる
フィールドは、できる限りアプリケーション内での命名を尊重すべきです。
例えば、wp_insert_post関数で投稿を作成できますが、投稿には「title」と「content」というプロパティがあります。これらの名前はGraphQLスキーマにも適しています(若干の修正が必要な場合もありますが)ので、維持すべきです:
type MutationRoot {
insertPost(title: String, content: String): Post
}
type Post {
id: ID!
title: String
content: String
}しかし、常にそうとは限りません。先ほど見たように、カスタム投稿は独自のエンティティに切り離される必要があります。そのため、get_posts関数は任意のCPTのリストを取得しますが、スキーマのルート型における同等のフィールドpostsはPost型のエンティティのみを取得し、Page(これもCPTです)は含みません:
type QueryRoot {
posts: [Post]!
}では、全ての投稿とページのリストを取得するにはどうすればよいでしょうか?別のフィールドcustomPostsを使います。これはユニオン型CustomPostUnionでマッピングされた任意のCPTのエンティティを取得します:
union CustomPostUnion = Post | Page
type QueryRoot {
customPosts: [CustomPostUnion]!
}重要な教訓はこれです:GraphQLスキーマに選ぶ命名は、取得するエンティティの型に適応させる必要があります。そして、GraphQLの強い型付けのため、その型はアプリケーション層とAPI層で異なる場合があります。
この場合、WordPressでは「post」は任意の「custom post type」を意味する場合がありますが、GraphQLでは「post」は必ずPostです。フィールドがカスタム投稿を取得する場合、GraphQLスキーマのフィールドはpostsではなくcustomPostsと命名されなければなりません。同様に、インプットがカスタム投稿のIDを受け取る場合は、postIDではなくcustomPostIDと呼ばれなければなりません。

この教訓はコメントにも適用されます。コメントは投稿だけでなく任意のCPTに追加できます。したがって、Comment型はこれを明確にするため、postではなくcustomPostフィールドを含まなければなりません:
type Comment {
id: ID!
customPost: CustomPostUnion!
}定義済み文字列値をenumに変換し、可能であれば大文字を使用する
列挙型(enum)は慣例により大文字で定義されます。例えば、graphql.orgのドキュメントでは次の例が示されています:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}新しいenum型を作成する際は、定義された定数に大文字を使用すべきです。しかし、アプリケーションからデータモデルを移行する場合、enumでマッピングできる定義済み値のセットが存在しますが、その値が小文字の文字列である場合があります。
例として、WordPressの投稿には「status」プロパティがあり、次のいずれかの値が含まれます:
"publish""pending""draft""trash"
スキーマでこのプロパティをマッピングする際、Post.statusフィールドは次のようにStringを返すことができます:
type Post {
status: String!
}しかし、ステータスは必ずそれらの定義済みの値のいずれかになるため、enumとしてマッピングする方が望ましいでしょう:
enum Status {
PUBLISH
DRAFT
PENDING
TRASH
}
type Post {
status: Status!
}ここで問題が生じるかもしれません:enum PUBLISHはアプリケーションで文字列値"PUBLISH"に変換され、"publish"にはなりません。
期待される小文字ではなく大文字の値を使用すると、アプリケーションのロジックが乱れる可能性があります。実際、WordPressで次のコードを実行しても動作しません:
// This will retrieve all posts, not only the published ones
$published_posts = get_posts([
"post_status" => "PUBLISH",
]);この場合、慣例と利便性をトレードオフとして検討し、定数をマッピングするためにenumを引き続き使用しながら、小文字で記述することを選択できます:
enum Status {
publish
draft
pending
trash
}言い換えれば、適切であることと実用的であることの中間点を見つけることができます。GraphQLスキーマを構築する際はベストプラクティスを使用すべきですが、理にかなっている場合はそこから逸脱することも許容すべきです。