メインコンテンツへ移動

GraphQL

無料2017-06-10#Solution#GraphQL与RESTful#GraphQL与REST#GraphQL指南#GraphQL vs RESTful

かねてから噂には聞いていたGraphQLですが、なかなか詳しく調べる機会がありませんでした。手頃な記事を見つけたので、この機会に入門してみます。

はじめに

本文の第一部は、REST 2.0 Is Here and Its Name Is GraphQL を翻訳したものです。タイトルが非常にインパクトがあったので、つい惹きつけられてしまいました。

残りの部分はGraphQLに関する考察です。それでは、翻訳文を読みながら疑問点を整理していきましょう。

一. 翻訳文

GraphQLはAPIのためのクエリ言語です。RESTとは本質的に異なりますが、GraphQLはRESTの代替案として利用でき、高いパフォーマンス、優れた開発体験、そして強力なツール群を提供します。

本記事では、RESTとGraphQLを使用して一般的なシナリオをどのように処理するかを見ていきます。この記事には3つのプロジェクトが付属しており、人気の映画や俳優情報のAPIを提供しています。また、HTMLとjQueryを使用したシンプルなフロントエンドアプリも用意されており、対応するRESTとGraphQLのソースコードを確認できます。

これらのAPIを通じて、これら2つの技術の違いを確認し、それぞれのメリット・デメリットを理解していきます。始める前に、まずは舞台設定として、これらの技術がどのようにして生まれたのかを簡単に振り返ってみましょう。

Webの初期

Webの初期は非常にシンプルでした。当時のWebアプリは静的なHTMLドキュメントでした。その後、データベース(SQLなど)に保存された動的なコンテンツを含み、JavaScriptでインタラクティブな機能を追加するWebサイトへと進化しました。ほとんどのWebコンテンツはデスクトップPCのブラウザからアクセスされており、すべてが順調に見えました。

Web Server fetches data & outputs HTML

REST:APIの登場

2007年、スティーブ・ジョブズがiPhoneを発表した時代まで進みましょう。スマートフォンは世界、文化、コミュニケーションに多大な影響を与えただけでなく、開発者の仕事も非常に複雑にしました。スマートフォンは開発の現状を破壊し、わずか数年のうちに、デスクトップ、iPhone、Android、タブレットが混在するようになりました。

これに対応するため、開発者はRESTful APIを使用して、さまざまな形状やサイズのアプリにデータを提供し始めました。新しい開発モデルは概ね以下のようになりました。

REST maps URIs to Resource

GraphQL:APIの進化

GraphQLはFacebookによって設計され、オープンソース化されたAPIのためのクエリ言語です。APIを構築する際のRESTの代替案として考えることができますが、RESTがAPIの設計・実装のための概念モデルであるのに対し、GraphQLは標準化された言語、型システム、そしてクライアントとサーバー間の強力な契約を確立する仕様定義です。すべてのデバイス間の通信に標準的な言語を提供し、大規模なクロスプラットフォームアプリの作成プロセスを簡素化します。

GraphQLを使用すると、図はこのようになります:

GraphQL is a query language for APIs

GraphQL vs REST

以下のセクションでは、ソースコードに沿って進めることをお勧めします。本記事に関連するソースコードは accompanying GitHub repo で確認できます。

ソースコードには3つのプロジェクトが含まれています:

  • RESTful APIの実装

  • GraphQL APIの実装

  • jQueryとHTMLで構築されたシンプルなクライアントWebページ

これら2つの技術をできるだけシンプルに比較するため、プロジェクトは意図的にシンプルに設計されています。

一緒に進めたい場合は、まず3つのターミナルウィンドウを開き、リポジトリの RESTfulGraphQLClient ディレクトリに cd して、npm run dev でサービスを起動してください。準備ができたら先を読み進めてください :)

RESTによるクエリ

私たちのRESTful APIには、いくつかのエンドポイント(endpoints)があります:

EndpointDescription
/movies映画へのリンクを含むオブジェクトの配列を返す (e.g. [ { href: ‘http://localhost/movie/1’ } ]
/movie/:idid = :id の単一の映画を返す
/movie/:id/actorsid = :id の映画に出演している俳優へのリンクを含む配列を返す
/actors俳優へのリンクを含むオブジェクトの配列を返す
/actor/:idid = :id の単一の俳優を返す
/actor/:id/moviesid = :id の俳優が出演している映画へのリンクを含む配列を返す

注意:このようなシンプルなデータモデルであっても、すでにメンテナンスとドキュメント作成が必要なエンドポイントが6つも存在します。

私たちがクライアント開発者で、これらの映画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 を実行して最初の映画を取得し、2番目は curl http://localhost:3000/movie/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);
            })
          })
        })
      })
    })
  })
}

お気づきの通り、これはあまり理想的ではありません。これらすべてを完了するために、私たちは 1 + M + M + sum(Am) 回のAPI呼び出しを行いました。ここで 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 でページを確認すると、以下のようになります:

Move demo page

よく見ると、私たちのページで使用しているのは映画の titleimage、そして各俳優の nameimage だけであることに気づくでしょう(実際には、映画オブジェクトの8つのフィールドのうち2つ、俳優オブジェクトの7つのフィールドのうち2つしか使っていません)。つまり、ネットワークリクエストで取得した情報の3/4を無駄にしていることになります!このように帯域幅を過剰に使用することはパフォーマンスに大きく影響し、インフラコストの増加も招きます。

機転の利くバックエンド開発者は、鼻で笑いながら 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ページ、Androidアプリ、iOSアプリで表示する情報が異なる場合、この問題はより顕著になります。

もし、データモデル内のエンティティとそれらの関係を明示的に説明しつつ、 1 + M + M + sum(Am) のようなパフォーマンスの問題を引き起こさない汎用的なAPIを構築できたら、どんなに素晴らしいでしょうか?朗報です!それは可能です!

GraphQLによるクエリ

GraphQLを使えば、即座に最適なクエリへと移行できます。シンプルで直感的なクエリを1つ送るだけで、必要なすべての情報を冗長性なく取得できます:

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

強くお勧めします!手動で試してみてください。http://localhost:5000 の GraphiQL(素晴らしいブラウザ用GraphQL IDE)を開き、上記のクエリを実行してみてください。

それでは、もう少し踏み込んでみましょう。

GraphQLについて考える

GraphQLはRESTとは全く異なるAPIアプローチを採用しています。HTTPのメソッド(動詞)やURIといった構造に依存するのではなく、データの上に直感的なクエリ言語と強力な型システム層を置き、クライアントとサーバ��間の強力な契約を提供します。クエリ言語は、クライアント開発者が任意のページで必要な任意のデータを永続的に取得できるメカニズムを提供します。

GraphQLは、データを「仮想的な情報グラフ」として見ることを推奨します。情報を構成する実体(エンティティ)を type と呼び、これらの typefields を通じて互いに関連付けられます。クエリはルートから始まり、この仮想グラフを辿って必要な情報を取得します。

この「仮想グラフ」は schema と呼ばれます。schema は、APIのデータモデルを構成する typeinterfaceenumunion の集合です。GraphQLには、APIを定義するための便利なスキーマ言語も含まれています。例えば、これが映画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 vs Rest:バージョニング

Googleで検索すれば、REST APIのバージョニングに関する(あるいはそれに関連する)多くの意見が見つかります。ここでは深く追求しませんが、これが重要な問題であることは強調しておきたいです。バージョニングが難しい要因の一つは、どの情報がどのアプリやデバイスで使用されているかを把握するのが通常困難であることです。

情報の追加は通常簡単です。RESTでもGraphQLでも、フィールドを追加した場合、RESTクライアントにはデータが流れ込みますが、GraphQLではクエリを変更しない限り安全に無視されます。しかし、情報の削除や編集となると話は別です。

REST方式では、フィールド単位でどの情報が使われているかを知ることは困難です。 /movies というエンドポイントが使われていることは分かっても、クライアントが titl(タイトル)を使っているのか、 image を使っているのか、あるいはその両方なのかは分かりません。解決策として、返すフィールドを指定するクエリパラメータを追加することもできますが、これらのパラメータは通常オプションです。そのため、 /v2/movies のようなエンドポイントレベルの変更がよく見られます。これは機能しますが、APIの表面積を増やし、常に更新し詳細なドキュメントを提供する負担を開発者に強いることになります。

GraphQLにおけるバージョニングは異なります。すべてのGraphQLクエリは、どのフィールドがリクエストされたかを正確に記述する必要があります。この強制的な要求により、私たちはどの情報がリクエストされたかを正確に把握でき、さらにリクエストの頻度やリクエスト元を特定することも可能になります。また、GraphQLには、スキーマの要素に非推奨(deprecated)フラグと非推奨の理由を付与するためのプリミティブ(primitives)も備わっています。

GraphQLにおけるバージョニング:

Evolve your API without versions

GraphQL vs REST:キャッシュ

RESTにおけるキャッシュは直接的で効率的です。実際、キャッシュは RESTの6つの原則(制約)の一つ であり、RESTfulな設計に組み込まれています。 /movies/1 エンドポイントからのレスポンスがキャッシュ可能であれば、将来の /movies/1 へのリクエストは単純にキャッシュに置き換えることができ、非常にシンプルです。

GraphQLにおけるキャッシュ処理は少し異なります。GraphQL APIをキャッシュするには通常、APIの各オブジェクトに何らかの一意の識別子を導入する必要があります。各オブジェクトに一意の識別子があれば、クライアントは標準化されたキャッシュを構築し、その識別子を使用して確実なキャッシュの作成、更新、有効期限の管理を行うことができます。クライアントがそのオブジェクトを参照する後続のクエリを発行する際は、そのオブジェクトのキャッシュバージョンが使用されます。GraphQLのキャッシュの仕組みについて詳しく知りたい場合は、 このトピックを深く掘り下げた優れた記事 があります。

GraphQL vs REST:開発体験

開発体験はアプリ開発において非常に重要な側面であり、エンジニアが優れたツールを構築するために多くの時間を費やす理由でもあります。ここでの比較は主観的ではありますが、言及する価値はあると考えています。

RESTを試してみてください。RESTには、ドキュメント作成、テスト、RESTful APIの検査を支援する豊かなツールエコシステムがあります。しかし、開発者はREST APIの拡張に多大な代償を払っています。エンドポイントの数は瞬く間に膨れ上がり、不整合が顕著になり、バージョニングはますます困難になります。

GraphQLは開発体験において確かに秀でた点があります。型システムにより、GraphiQL IDEやスキーマ内蔵ドキュメントなど、素晴らしいツールの数々が実現されています。GraphQLにはエンドポイントが1つしかなく、どのデータが利用可能かを探すためにドキュメントに頼る必要もありません。型安全な言語があり、利用可能なものをオートコンプリートで確認しながら、APIを素早く構築できます。また、GraphQLはReactやReduxといった人気のフロントエンドフレームワークやツールとも連携できます。Reactでアプリを構築することを考えているなら、 RelayApollo client をチェックすることを強くお勧めします。

まとめ

GraphQLは、効率的なデータ駆動型アプリを構築するための強力なツールセットを提供します。RESTがすぐに消えることはありませんが、特にクライアントアプリを構築する際には、GraphQLには求められている多くの要素が備わっています。

さらに詳しく知りたい場合は、 Scaphold.io’s GraphQL Backend as a Service や、 AWSで本番レベルのGraphQL APIを数分でデプロイする方法 を確認し、自身のビジネスロジックをカスタマイズして拡張してみてください。

この記事を気に入っていただければ幸いです。ご意見やコメントがあれば、ぜひお聞かせください。最後までお読みいただきありがとうございました!

二. 考察

インターフェースの上の抽象レイヤーを一段増やすことで、確かに柔軟性は向上します。例えば、アトミックなインターフェースを実装するだけで、レスポンス内容を自由に組み合わせることが可能になります。

注意:上記の翻訳文ではGraphQLを「データ上の抽象」としていますが、実際には「インターフェース上の抽象」であるべきです(単にインターフェースの概念が弱まり、外部に公開されなくなっただけで、弱まったインターフェースはSQL文のようなものに近くなります)。もし各フィールドがそれぞれクエリインターフェースに対応していれば、 汎用的なインターフェース管理層 を実装することで、GraphQLの全機能を簡単に実現できます。実際、GraphQLはそのような汎用的な定義を提供しているに過ぎません。

そうなると、最大の問題は 冗長なクエリ の存在でしょう。なぜなら、 field を自由に組み合わせて返せるようにするには、まず field レベルまで細分化する必要があるからです。つまり、本来一つの強力なインターフェースが一括で返していたフィールド群に対して、各フィールドごとに弱いインターフェースを提供しなければ、カスタムクエリに基づいてレスポンス内容を正確に構築することはできません。

もちろん、クエリの最適化(フィールドの依存関係に基づいたパッチクエリなど)によって冗長なクエリを一部軽減することは可能です。しかし、複雑なシナリオでは、このような最適化を実装するのは容易ではないかもしれません。

もし、このような最適化を内蔵し、パフォーマンス問題を解決するデータベース(または抽象クエリ層)が存在すれば、GraphQLは圧倒的な優位性を獲得するでしょう。まず、際限なくエンドポイントを追加し続ける必要がなくなります。また、標準化された一つの仕組みを維持することと、無数のエンドポイント(しかも同一エンドポイントの複数バージョン混在)を維持することを比較すれば、どちらを選ぶべきかは一目瞭然です。

フロントエンドのエコシステムとの連携(Reduxはそれほど汎用的ではありませんが)については、明らかに大きな問題ではありません。

コメント

コメントはまだありません

コメントを書く