アプリ開発の流れを変える「GraphQL」はRESTとどう違うのか比較してみた

2017/07/12

Michael Paris

277

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

Facebookが開発しているクエリ言語「GraphQL」のAPIはどこが便利なのか。サンプルで従来のREST APIと比較しながら解説します。

GraphQLはAPIへの問い合わせ言語です。優れたパフォーマンスと開発者エクスペリエンス、強力なツールがあり、RESTの代わりになります。

3つの一般的なユースケースでRESTとGraphQLで扱う方法を紹介します。人気の映画と出演者の情報が見られるREST APIとGraphQL APIそれぞれのコード、HTMLとjQueryで作られた単純なフロントエンドアプリを用意しました。

APIを扱いながら、異なる点、強み、弱みを体験してください。まずは技術の生い立ちを手短に紹介します。

Webの黎明期

初期のWebサイトは、静的なHTMLドキュメントがインターネット越しに送られてくるだけのシンプルなものでした。やがてSQLなどでデータベースに格納されたコンテンツを動的に取得したり、JavaScriptで操作したりするようになりました。デスクトップPCのWebブラウザーで閲覧していました。

Normal Image

REST:APIの隆盛

時代は進み、2007年にはスティーブ・ジョブズがiPhoneを発表しました。スマートフォンの影響は世界、文化、通信だけにとどまらず、開発の仕事にも及びました。デスクトップだけではなく、iPhone、Android、タブレットに対応した開発が必要になったのです。

開発者たちはすべての形、サイズのアプリでデータを表示するためにRESTful APIを利用し始めました。開発モデルは以下の形です。

REST Server

GraphQL:APIの進化系

GraphQLはFacebookにより開発されたオープンソースの言語です。API作成の仕組みとしてRESTの代わりに使えます。RESTはAPIの設計と実装に使う概念上の設計モデルですが、GraphQLは標準化された言語、型付け、仕様を持ちクライアントとサーバー間を強力に結びつけます。異なるデバイス間の通信に標準化された言語があることで、大型かつクロスプラットフォームのアプリ開発がよりシンプルになります。

GraphQLの開発モデルです。

GraphQL Server

GraphQLとRESTを比較

さっそく体験してみましょう。コードはGitHubリポジトリにあります。

コードには3つのプロジェクトが含まれます。

  1. RESTful API
  2. GraphQL API
  3. jQueryとHTMLで作ったシンプルなクライアントページ

プロジェクトはあえて簡潔にし、技術を比較しやすくしています。

ターミナルウィンドウを3つ開き、cdでプロジェクトリポジトリのRESTfulGraphQLClientのフォルダーに移動します。各フォルダーから、開発用サーバーをnpm run devで起動します。

RESTによるクエリ

サンプルプロジェクトのRESTful APIにはいくつかのエンドポイントがあります。

エンドポイント 説明
/movies 映画のリンクを含んだオブジェクトの配列を返す(例:[ { href: ‘http://localhost/movie/1’ } ])
/movie/:id id(= :id)で指定された1つの映画タイトルを返す
/movie/:id/actors id(= :id)で指定された映画の出演者へのリンクを含んだオブジェクトの配列を返す
/actors 出演者のリンクを含んだオブジェクトの配列を返す
/actor/:id id(= :id)で指定された出演者1人を返す
/actor/:id/movies id(= :id)で指定された出演者が出演している映画へのリンクを含んだオブジェクトの配列を返す

注:単純なデータモデルでさえ、今後の維持や説明が必要になる6つものエンドポイントが含まれています。

あなたがクライアント側の開発者で、movies APIを使い、HTMLとjQueryで単純なWebページを作るとします。そのためには、映画と出演俳優・女優の情報が必要です。APIに必要な機能は揃っているので、データを取得します。

新しくターミナルを開いて以下を実行します。

curl localhost:3000/movies

以下の応答が返ってきます。

[
  {
    "href": "http://localhost:3000/movie/1"
  },
  {
    "href": "http://localhost:3000/movie/2"
  },
  {
    "href": "http://localhost:3000/movie/3"
  },
  {
    "href": "http://localhost:3000/movie/4"
  },
  {
    "href": "http://localhost:3000/movie/5"
  }
]

RESTfulのAPIは映画オブジェクトのリンクを配列で返します。curl http://localhost:3000/movie/1で最初の映画を取得し、curl http://localhost:3000/movie/2で2番目を取得となります。

app.jsの中身を見ると、ページの内容を埋めるのに必要な情報を取得する関数が書かれています。

const API_URL = 'http://localhost:3000/movies';
function fetchDataV1() {

  // 1 call to get the movie links
  $.get(API_URL, movieLinks => {
    movieLinks.forEach(movieLink => {

      // For each movie link, grab the movie object
      $.get(movieLink.href, movie => {
        $('#movies').append(buildMovieElement(movie))

        // One call (for each movie) to get the links to actors in this movie
        $.get(movie.actors, actorLinks => {
          actorLinks.forEach(actorLink => {

            // For each actor for each movie, grab the actor object
            $.get(actorLink.href, actor => {
              const selector = '#' + getMovieId(movie) + ' .actors';
              const actorElement = buildActorElement(actor);
              $(selector).append(actorElement);
            })
          })
        })
      })
    })
  })
}

APIに対して 1 + M + M + sum(Am)回も応答を求めるコールを発しています。理想的ではありません。Mは映画の数、sum(Am)はM本の各映画の出演者数の合計です。データ量が少ないアプリであれば良いですが、大型の本番プロジェクトには向きません。

RESTfulのアプローチは不十分です。APIを改良するには、バックエンド開発チームに頼みこんで、ページを埋めるための/moviesAndActorsエンドポイントを作ります。準備できたら、1 + M + M + sum(Am)ネットワークコールが、1つのリクエストで済みます。

curl http://localhost:3000/moviesAndActors

以下の応答が返ってきます。

[
  {
    "id": 1,
    "title": "The Shawshank Redemption",
    "release_year": 1993,
    "tags": [
      "Crime",
      "Drama"
    ],
    "rating": 9.3,
    "actors": [
      {
        "id": 1,
        "name": "Tim Robbins",
        "dob": "10/16/1958",
        "num_credits": 73,
        "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMTI1OTYxNzAxOF5BMl5BanBnXkFtZTYwNTE5ODI4._V1_.jpg",
        "href": "http://localhost:3000/actor/1",
        "movies": "http://localhost:3000/actor/1/movies"
      },
      {
        "id": 2,
        "name": "Morgan Freeman",
        "dob": "06/01/1937",
        "num_credits": 120,
        "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMTc0MDMyMzI2OF5BMl5BanBnXkFtZTcwMzM2OTk1MQ@@._V1_UX214_CR0,0,214,317_AL_.jpg",
        "href": "http://localhost:3000/actor/2",
        "movies": "http://localhost:3000/actor/2/movies"
      }
    ],
    "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BODU4MjU4NjIwNl5BMl5BanBnXkFtZTgwMDU2MjEyMDE@._V1_UX182_CR0,0,182,268_AL_.jpg",
    "href": "http://localhost:3000/movie/1"
  },
  ...
]

たった1つのリクエストで、ページを埋めるのに必要なデータが全部取得できました。Clientフォルダー内のapp.jsを見れば、改良が奏功しているのが分かります。

const MOVIES_AND_ACTORS_URL = 'http://localhost:3000/moviesAndActors';
function fetchDataV2() {
  $.get(MOVIES_AND_ACTORS_URL, movies => renderRoot(movies));
}
function renderRoot(movies) {
  movies.forEach(movie => {
    $('#movies').append(buildMovieElement(movie));
    movie.actors && movie.actors.forEach(actor => {
      const selector = '#' + getMovieId(movie) + ' .actors';
      const actorElement = buildActorElement(actor);
      $(selector).append(actorElement);
    })
  });
}

改良したアプリは以前のものよりもずっと高速ですが、まだ完璧とは言えません。http://localhost:4000を開いてください。

Demo App

このページでは映画のタイトルと画像、俳優の名前と画像を使用しています。ここでは映画のフィールドは8つのうち2つのみ、俳優は7つのうち2つのみで、リクエストした情報の4分の3は無駄になったことを示しています。回線の過剰な使用はパフォーマンスの低下だけでなく、設備コストにも跳ね返ってきます。

賢いバックエンド開発者なら一笑して、すぐ手を打つでしょう。特別なクエリパラメーター「fields」でフィールド名の配列を受け取り、リクエストにどのフィールドを返すべきかを動的に決定するのです。

たとえば、curl http://localhost:3000/moviesAndActorsの代わりに、curl http://localhost:3000/moviesAndActors?fields=title,imageとします。別の特別なクエリパラメーターとして、どの俳優のフィールドを含めるかを特定するためにactor_fieldsを設けることも考えられます。例:curl http://localhost:3000/moviesAndActors?fields=title,image&actor_fields=name,image

最適な実装に近づきました。クライアントアプリの特定ページのためにカスタムエンドポイントを作る悪い慣習が残っています。Webページとは異なる情報を表示するiOSアプリ、Androidアプリを作るときに問題になります。

明示的にデータモデルのエンティティと、エンティティ同士の間の関係性が表示できて、性能も1 + M + M + sum(Am)回で解決できる一般的なAPIを紹介します。

GraphQLによるクエリ

GraphQLはあっという間に最適化したクエリを発行します。単純で直感的なクエリで、必要な情報を全部取得できます。

query MoviesAndActors {
  movies {
    title
    image
    actors {
      image
      name
    }
  }
}

試すにはブラウザーベースの優れたGraphQL用IDEのGraphiQLでhttp://localhost:5000を開き、上記クエリを走らせます。

詳しく見ていきます。

GraphQLの考え方

GraphQLのAPIのアプローチはRESTとは異なります。動詞やURIなどのHTTP構造に頼らずに、直感的でわかりやすい問い合わせ言語と型システムの層を載せています。システムによって、クライアントとサーバーの間にデータの型という堅固な共通の約束事があり、問い合わせ言語によってクライアント側の開発者はすべてのページに必要なデータを効率よく取得できます。

GraphQLを使うには、データを仮想的な情報の「グラフ」と仮定すると分かりやすいです。情報の入ったエンティティは型と呼ばれ、型同士はフィールドを通じて関連します。クエリは、ルートから順に「グラフ」をたどりながら必要な情報を見つけます。

「グラフ」はスキーマで表現されます。スキーマとは型、インターフェイス、列挙体(enum)、共用体(union)などAPIを構成するデータモデルを表したものです。GraphQLは、便利なスキーマ言語でAPIの定義が書けます。以下は今回のmovie APIのスキーマです。

schema {
    query: Query
}

type Query {
    movies: [Movie]
    actors: [Actor]
    movie(id: Int!): Movie
    actor(id: Int!): Actor
    searchMovies(term: String): [Movie]
    searchActors(term: String): [Actor]
}

type Movie {
    id: Int
    title: String
    image: String
    release_year: Int
    tags: [String]
    rating: Float
    actors: [Actor]
}

type Actor {
    id: Int
    name: String
    image: String
    dob: String
    num_credits: Int
    movies: [Movie]
}

型システムにより、良いツールとドキュメント類、効率的なアプリなどのすばらしい扉が開かれます。伝えたいことは山ほどありますが、先に進み、いくつかの例でRESTとGraphQLの違いを紹介します。

GraphQLとRESTの違い:バージョン管理

Google検索するだけで、REST APIのバージョン管理方法に関する情報がたくさん見つかります。それほどREST APIのバージョン管理は困難なのです。通常、どのアプリ、デバイスから、どんな情報が使われているのかわからないことが難しい理由の1つです。

RESTでもGraphQLでも情報の追加は簡単です。フィールドを追加すれば、RESTのクライアントに流れますし、GraphQLはクエリを変更しない限り安全な形で無視されます。しかし、情報を削除したり編集したりするなら話は変わります。

RESTは、フィールドレベルで使われている情報を知るのは困難です。エンドポイント/moviesが使われていると分かっても、使われているのがタイトルなのか画像なのか、両方なのかは分かりません。対策は、返ってきたフィールドを特定するためのクエリパラメーター「fields」を追加することです。パラメーターはオプションなのでエンドポイントレベルの改変用に別のエンドポイント/v2/moviesを用意します。ただし、APIの入り口は肥大化し、常に最新の状態を整合性を保ちながら、ドキュメント類も整える作業は開発者には重荷です。

GraphQLのバージョン管理はまったく違います。GraphQLクエリは、リクエストしたフィールドを明示します。要求された情報を正確に把握できます。「誰が」「どの頻度」でといった情報を得ることも可能です。プリミティブ型も使えるので、スキーマにフィールドの非推奨とその理由を明示することもできます。

Versioning in GraphQL

GraphQLのバージョン管理

GraphQLとRESTの違い:データのキャッシュ

RESTのキャッシュは分かりやすく効率的です。キャッシュの活用はRESTの6つの鉄則のうちの1つとされ、RESTfulデザインに組み込まれています。エンドポイント/movies/1からの返事をキャッシュすると示されているなら、以降の/movies/1のリクエストにはすべて、キャッシュ済みのものが返されます。シンプルな仕組みです。

GraphQLのキャッシュの使い方は若干異なります。GraphQL APIをキャッシュするには各オブジェクトに一意のIDを付けます。オブジェクトに一意のIDがあれば、クライアントは正規化されたキャッシュを構築し、IDに基づいて安全にオブジェクトの保存、更新、削除をします。クライアントがそのオブジェクトを参照する下流へのクエリを発したら、キャッシュ済みのオブジェクトが返されます。GraphQLにおけるキャッシュ使用の仕組みはこちらを参照してください。

GraphQLとRESTの違い:開発者エクスペリエンス

開発者エクスペリエンスはアプリ開発の重要項目であり、エンジニアがツール環境を整えるのに膨大な時間を費やす理由です。この比較が主観的だったとしても、触れておく必要があるでしょう。

RESTは実証された存在です。豊かなエコシステムを持ち、開発者がドキュメント作成、テスト、RESTful APIのインスペクションなどをするためのツール類も充実しています。ただし、REST APIの肥大化につれて開発者がツケを払ってます。エンドポイント数は増えすぎ一貫性を保つことが困難にり、バージョン管理は難しいままです。

GraphQLは型付けの仕組みなのでGraphiQL IDEなどのすばらしいツールが使えます。スキーマにドキュメントが含まれるのもポイントです。さらに1つのエンドポイントで済み、使えるデータをドキュメントで調べる代わりに、安全な型付け言語とオートコンプリートによってAPIの概要がつかめます。ReactやReduxなどの現在主流のフロントエンドフレームワークやツールと組み合わせて使えます。Reactアプリの開発を検討しているなら、RelayないしApollo clientをチェックすることをおすすめします。

最後に

GraphQLには多少の賛否がありますが、高効率なデータ指向のアプリ開発には強力なツールです。RESTによるクライアントサイドアプリの開発には課題があります。

さらに深く学びたいなら、Scaphold.ioの、サービスとしてのGraphQL バックエンドをチェックしてください。わずか数分でAWS上にGraphQL APIをデプロイでき、自分のビジネスロジックに合わせて変更、拡張が可能です。

(原文:REST 2.0 Is Here and Its Name Is GraphQL

[翻訳:西尾健史/編集:Livit

Copyright © 2017, Michael Paris All Rights Reserved.

Michael Paris

Michael Paris

scaphold.ioの最高経営責任者かつ共同創業者で、困難な問題に取り組むのが好き。scaphold.ioは、Yコンビネーター(米国のベンチャーキャピタル)の2017年冬の選考において投資先に選出されました。

Loading...