概要
どうも。ドリルで地面を掘削して死のうと考えていたらルシファー様と仲良くなってしまいそうになったのでジュデッカ中央の大きな穴から抜け出してきた者です。
諸般の事情でいつも使っているPCが1ヶ月ほど使えなくなったので、今回はGraphQLのメリット・デメリットについて書いてみました。
尚、GraphQLの魅力とか、そういった記事で「静的型付けが魅力💕」とかそういった基礎的な話は大前提として進めたいので省略します💕
GraphQLとは
GraphQLとは、かの悪名高い有名なFacebook社が作った、Webフロントエンドとサーバーサイドバックエンドとの問い合わせに使う、問合せ言語の一つです。
どんなふうに使うのか
例えば、/graphql
というアドレスから、「国の名前とAlpha-3コードのリスト」を取得したいとき・・・
{
countries: getCountries(
startsWith: "A",
num: 2
) { name, alpha3 }
}
という問い合わせを行うと、
{
"data": {
"countries": [
{
"name": "Afghanistan",
"alpha3": "AFG"
},
{
"name": "Aland Islands",
"alpha3": "ALA"
},
]
}
}
のような感じのレスポンスが得られます。
こんな長いクエリを打たないといけない言語がなぜ「素晴らしいのか」
そう、僕がJavaとPHPにちょっと嫌悪感を抱いているように、読者諸氏は「なぜこの無職こどおじはこのような長いクエリを打たないといけない言語が『素晴らしい』といっているのか?バカなのか死ぬのか?」などと思っているに違いないでしょう。しかし、こいつは単体では無力でも、複数組み合わせることで真価を発揮するのです・・・
メリット1: 複数の情報を1回のリクエストで取得できる
上記で紹介したクエリは実は短縮形で、実際には次のような「長いクエリ」で書くこともできます。
query Get() {
countries: getCountries(
startsWith: "A",
num: 2
) { name, alpha3 }
}
この場合、query
という作業にGet
という作業名がつけられ、その中で問い合わせの内容を指示しています。重要なのは、作業名Get()内で問い合わせる内容は、単独の問い合わせじゃなくても良いという点です。
つまり、複数の問い合わせができる。そう、こんな感じに・・・
query Get() {
countries: getCountries(
startsWith: "A",
num: 2
) { name, alpha3 }
# getCountryで単一の国情報を取得する。
JCountry: getCountry(alpha2: "JP") { name, alpha3 }
}
こうすると、こんなレスポンスになります。
{
"data": {
"countries": [
{
"name": "Afghanistan",
"alpha3": "AFG"
},
{
"name": "Aland Islands",
"alpha3": "ALA"
},
],
"JCountry": {
"name": "Japan",
"alpha3": "JPN"
}
}
}
これのどこに良いところがあるというのか。 熟練したWebエンジニアならお気づきかもしれませんが、必要なリソースを1度のアクセスのみで取得できるという、SPAを扱う上ではかなり強力な特徴を持ちます。
例えば、
- 国のリスト (REST:
/countries
, GraphQL:countries
) - 価格のリスト (REST:
/prices
, GraphQL:prices
) - スポンサーのリスト (REST:
/sponsors
, GraphQL:sponsors
) - 取引先のリスト (REST:
/partners
GraphQL:partners
)
をアプリの初期化の時点で全て取得しなければならない場合、RESTフレームワークだと、4つあまりのリソースにGETしにいくのではないかと思われます。
しかしこれだと、サーバーサイドでHTTPリクエストをページにアクセスする毎に最低でも5回 (ページロード1回、リソース取得4回)のHTTPリクエストを捌く事になります。もっとも、初期化時に必要な情報を全て提供する/init
なるリソースを用意しても良いのですが、その場合は/init
で提供する情報を適切に管理する必要があるでしょう。
さて、GraphQLの場合は、指定されたURL(e.g. /graphql
)に対して、
{
countries {name, code}
prices {planName, amount, features}
sponsors {name, desc}
partners {name, desc}
}
というクエリをGETもしくはPOSTリクエストで1回、投げるだけで必要な情報が全て取り出せるのです。しかも、必要な情報の管理もGraphQLのスキーマ定義で簡単に管理できてしまいます。そう、こんな感じに・・・
type Country {
name: String!
code: String!
alpha3: String!
}
type Price {
planName: String!
amount: Float!
features: [String!]
}
type Partner {
name: String!
desc: String!
}
type Query {
countries: [Country!]
prices: [Price!]
sponsors: [Partner!]
partners: [Partner!]
}
メリット2: テスト工数の削減が見込める
GraphQLの仕様書には、何も「エンドポイントは1つでなければならない」とは書かれていません。
何が言いたいのかというと、例えば、
-
[GET] /businesses
: ユーザーのログイン必須。ユーザーが保有している企業の情報を返す。 -
[POST] /businesses
: ユーザーのログイン必須。企業情報を登録する。 -
[GET] /transactions
: ユーザーのログイン必須。ユーザーと企業との取引を参照する。 -
[POST] /transactions
: ユーザーのログイン必須。企業取引を登録する。 -
[GET] /me
: ユーザーのログイン必須。ログインしているユーザーの情報を参照する。 -
[POST] /me
: ログインする -
[DELETE] /me
: ユーザーのログイン必須。ログアウトする
などといったRESTリソースがあるとすると、次のテストをそれぞれのリソース毎に実装するはずです~~(まさか、無職こどおじでもこういうことしてるのにQiita諸氏はしていないなんてこと、ないよね?)~~。
- ユーザーがログインしている場合、必要な処理を行う (正常処理A)
- ユーザーがログインしていて特定の条件下の場合、エラーとなるようにする(異常処理A)
- ユーザーがログインしていない場合、認証エラーとなるようにする(異常処理B)
多くの場合、異常処理Aのテストケース数は複数個になることが多いですが、今回はあえて1個しかないと仮定すると、それぞれのリソースに対して、最低でも3つのテストを書くことになります。 つまり、合計で6 x 3 + 2で計20個のテストを書くことになる (+2はログインを必要としていない[POST] /me
のテストケース数)。 たった、6つのリソースに対して20個です。 仮にこういった「ログインが必須」のリソースが20、30と、膨大な数になるとその分上記のテストを実装することになるわけです。スピード感重視のIT業界でこれだと、テストの実装やテストそのものに時間がかかりすぎる。
そこで、GraphQLの出番です。多くのGraphQLの場合、一つのエンドポイントに対してクエリやらミューテーションを定義します:
directive @authRequired on FIELD_DEFINITION
type Mutation {
regBiz(biz: Business!): Business @authRequired
setTr(bizId: UUID!): Transaction @authRequired
login(username: String!, password: String!): User
}
type Query {
getBiz(bizId: UUID!): Business @authRequired
getTr(bizId: UUID!): Transaction @authRequired
me: User @authRequired
}
上記の場合、ディレクティブとして認証が必要であることを示す、@authRequired
を定義し、ログインが必要な処理に対してこれを適用しています。が、これだと、テスト対象がリソースからクエリに変わるだけで、必要となるテスト工数は変わりません。
ところが、先程にも書いたように、GraphQLは問い合わせ言語の仕様を定義したものであって、エンドポイントは必ず一つでなければならないという事は書かれていません。
言い換えれば、認証が必要なエンドポイントと、認証が不要なエンドポイントの2つに分けることができるのです。 そう、ちょうどこんなふうに:
type Mutation {
login(username: String!, password: String!): User
}
type Mutation {
regBiz(biz: Business!): Business
setTr(bizId: UUID!): Transaction
}
type Query {
getBiz(bizId: UUID!): Business
getTr(bizId: UUID!): Transaction
me: User @authRequired
}
こうすることで、例えば、/prv
に対して、
-
/prv
がプライベート用クエリ・ミューテーションにアサインされていることを確認する - ユーザーがログインしている場合、処理を通す (or HTTP Status Code 200を返す)
- ユーザーがログインしていない場合、処理を弾く (or HTTP Status Code 401を返す)
の2つのテストケースを用意し、あとはそれぞれのクエリ、ミューテーションに対して、正常系、異常系のテストを行ってやればいいのです。ここで重要なのは、異常系のテストで上記のテストは省くことが可能です。だって、上でやっちゃったんですから。
この手法による効能
僕、クラウドベースの会計ソフトをタダで使いたい などというゲスい欲望の下、会計アプリを目下作成中だったりします。DjangoでRESTリソースを書いて、テストを書いて・・・という方法だと、テストケース数は(アプリ全体の数%の時点で)、約1,200近くあったのですが、この手法によって、現在のところ、約550近くにまで減らすことに成功しました。他にもRESTリソースになっている箇所はたくさんあるので、**今後もテストケース数は更に減ると予想されます。**すごいでしょ?
最後に
実を言うと、アプリ作ってるとはいえ、僕自身はほぼ一文無しです。だから、働かないといけません。僕をこき使って下さる方がいれば是非ともコンタクトを頂けると嬉しいです。
では。