はじめに
最近、GraphQL APIを作ることが多いです。最初の方に作ったスキーマを見ると、「なんでこんなことに...!」と自分ながら絶望することがよくあります。
今回はGraphQLの設計で「失敗したな〜」と個人的に思ったことをアンチパターンとしてまとめてみました。(随時更新)
アンチパターン
大学の履修管理のシステムを考えます。学生(students)と履修科目(courses)のERDが下記です。多対多の関係の交差テーブルstudent_courses
を作ります。
露出した交差テーブル
素朴にGraphQLスキーマを定義すると以下のようになります。
type Student {
name: String!
studentCourses: [studentCourse]
}
type Course {
title: String!
studentCourses: [studentCourse]
}
type StudentCourse {
student: Student
course: Course
}
このスキーマはRDB上の関係に忠実ですが、交差テーブルが露出しています。
クライアントが必要な情報は「学生がどの履修科目を履修しているか、またはある科目をどの学生が履修しているか」であって学生と科目がどのように関連付けられているかではありません。
利用者にとって、テーブル構造や実装の詳細は関係ありません。交差テーブルのような実装の詳細に依存すことにより変更に弱くなります。
交差テーブルのような実装の詳細は隠蔽したほうが良いでしょう。
type Student {
name: String!
courses: [Course]
}
type Course {
title: String!
students: [student]
}
そのまんまカラム
studentsテーブルに在学、卒業を表すカラムis_graduated
があるとしましょう(コレ自体はよくないテーブル設計)。
素朴なスキーマ定義は以下のようになります。
type Student {
name: String!
courses: [Course]
isGraduated: Boolean!
}
type Query {
students: [Student]
}
これも実装の詳細が顕になっています。ビジネスドメインに関する知識が必要ですが、履修科目の管理システムなので卒業生と在校生を同じStudentと考えるのは危険です。
クライアントでは永遠に卒業すみかどうかをチェックする必要が出てきてしまいます。
基本的にはAPI側で卒業生データを渡さないように制限をかけるべきでしょう。
もしクライアントで卒業生のデータ必要な場合は、queryを分ける、もつfieldが違うのであればtypeごと分けてしまいましょう。
type Query {
graduatedStudents: [Student]
students: [Student]
}
スキーマ設計時にDBに引っ張られて、カラム名をそのままfield名として使ってしまうことは良くあります。
往々にしてDBは技術的負債を抱えがちです。DBに引っ張られるのではなく、現状のドメイン知識に従ってGraphQLスキーマを定義することが大切です。
nullableリスト
type Student {
name: String!
courses: [Course]
}
StudentとCourseの関係を見てみると、生徒は0~n個までの科目を登録できます。上記の定義ではnull, [null], []のような値を取りうることになります。
基本的にリストはnull, [null]にドメイン上で意味があることは珍しいです。よってStudentは以下のようにすると良いでしょう。この場合だと、coursesは[]と値の入った配列を許容します。
type Student {
name: String!
courses: [Course!]!
}
CRUD依存症
mutationをDBのCRUDとして命名してしまうアンチパターンです。
例えば、新規に科目を登録するmutationをcreateCource(courceId: ID!)
とします。一見、自然な命名に見えます。
しかし、動詞createは明らかに実装の詳細であるDBに依存してしまっています。クライアントにとって、科目を登録することがDBにレコードを作成するという知識は不要です。命名にはできるだけドメインの言葉を使いましょう。例えばregisterCource
などです。
(もちろん言葉として妥当ならばcreateを使っても問題ありません。)
なんでもupdate mutation
更新関連の操作をすべてupdateHogeHoge
という1つのmutationで行ってしまうパターンのことです。
例えば、 科目を更新するmutation updateCource(id: ID!, title: String)
があるとしましょう。最初は更新のユースケースが少ないのでこれでも問題ありません。
ところがあるとき、授業が実施される教室も変更できるようにして欲しいと要望を受けました。mutationは安易にupdateCource(id: ID!, title: String, className: String)
と変更されます。
もちろん教室を変更できるのは、その教室が同じ時間帯に別の科目で使われてない場合のみです。
よってupdateCource
は変更する属性によって条件分岐を余儀なくされます。
以降、同様にしてupdate mutationは様々なユースケースに対応しようとして、巨大かつ複雑なロジックを抱えるようになってしまいます。
mutationはユースケースごとにできるだけ小さくわけることをおすすめします。上記の例だと教室変更はchangeClass
mutationに分割するのが良いでしょう。
おわりに
こう見るとアンチパターンは、既存のDBスキーマに引っ張られてしまうことが多い印象です。
GraphQLの設計をするときには、一度古い設計から離れて改めてドメインに向き合って設計するのが良いかもしれません。