Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

RubyではじめるGraphQL入門――音楽アプリのサンプルで学ぶスキーマ定義と実装

GraphQLの素晴らしさを絶賛する開発者の声を耳にしたことがある方も多いでしょう。このシリーズでは、実際に技術を手を動かしながら学んでいくスタイルを大切にしており、本記事ではGraphQLを活用したサンプルアプリケーションを通じて、その基本を解説します。

GraphQLとは何か

GraphQLは、API構築に使えるクエリ言語およびランタイムです。開発スタックにおける位置づけはREST APIと似ていますが、より柔軟性が高いのが特徴です。RESTと異なり、GraphQLではレスポンスの形式や内容をクライアント側で指定できます。SQLのSELECT文で取得結果を指定できるように、GraphQLでも返却されるJSONデータ構造を指定できます。ただし、SQLとの類推で言えば、GraphQLにはWHERE句は存在せず、レスポンスのデータを供給するフィールドをアプリケーションオブジェクト上で識別する仕組みになっています。

その名の通り、GraphQLはアプリケーションを「データのグラフ」としてモデル化します。ご自身のアプリケーションをそう捉えていない方もいるかもしれませんが、これは多くのシステムで採用されているモデルです。JSONで表現できるデータは有向グラフであり、つまりグラフなのです。アプリケーションを「APIを通じてグラフモデルを提示するもの」と考えると、GraphQLは格段に理解しやすくなります。

アプリケーションでGraphQLを使う

ここまで抽象的な説明をしてきましたが、次は実際にGraphQLを使ったアプリケーションを作ってみましょう。まずはデータモデル、すなわちグラフの定義から始めます。筆者は昨年、新しい趣味としてエレキアップライトベースの演奏と音楽全般の勉強を始めました。そこでデモアプリを考える際に、音楽関連の題材が自然と思い浮かんだのです。

サンプルのオブジェクトタイプはArtist(アーティスト)Song(楽曲)です。Artistは複数のSongを持ち、Songは1人のArtistに紐付きます。各オブジェクトタイプにはnameなどの属性があります。

APIを定義する

GraphQLではSDL(Schema Definition Language)を使用します。GraphQL仕様では「型システム定義言語」と呼ばれることもあります。GraphQLの型は理論上どの言語でも定義可能ですが、最も一般的な実装非依存の記述方法はSDLなので、ここではSDLでAPIを定義します。

type Artist {
  name: String!
  songs: [Song]
  origin: [String]
}

type Song {
  name: String!
  artist: Artist
  duration: Int
  release: String
}

ArtistString型のnameを持ちます。感嘆符(!)はそのフィールドがnon-null(null不可)であることを意味します。songsSongオブジェクトの配列、originStringの配列です。Songもほぼ同様ですが、少し変わったフィールドが1つあります。releaseフィールドは本来日時型であるべきですが、GraphQLのコア型には該当する型が存在しません。異なるGraphQL実装間での完全な移植性を保つため、ここではStringを使っています。ただし、今回使用するGraphQL実装にはTime型が追加されているため、Songの定義を変更してreleaseフィールドをTime型にしましょう。返却値自体はStringですが、型をTimeとすることでAPIをより正確にドキュメント化できます。

  release: Time

最後のステップは、オブジェクトを1つ以上取得する方法の記述です。これは「ルート」、クエリの場合は「クエリルート」と呼ばれます。今回のルートにはartistというフィールド(メソッド)を1つだけ持ち、アーティストのnameを引数として要求します。

type Query {
  artist(name: String!): Artist
}

アプリケーションの作成

次に、これをアプリケーションでどう使うかを見ていきましょう。Ruby向けのGraphQLサーバー実装はいくつかあります。一部の手法では、先ほどのSDLをRuby相当のコードに変換する必要があります。筆者が開発したHTTPサーバー「Agoo」は、SDL定義をそのまま利用でき、Rubyコードもごく普通の素朴なRubyで書けるため、今回はこれを採用します。

Rubyのクラス名がGraphQLの型名と一致している点に注目してください。クラス名を型名と一致させることで、余計な複雑さを持ち込まずに済みます。

class Artist
  attr_reader :name
  attr_reader :songs
  attr_reader :origin
 
  def initialize(name, origin)
    @name = name
    @songs = []
    @origin = origin
  end
 
  # Song自身がArtistに自分を登録するためにのみ使用される。
  def add_song(song)
    @songs << song
  end
end
 
class Song
  attr_reader :name     # 文字列
  attr_reader :artist   # 参照
  attr_reader :duration # 整数
  attr_reader :release  # 時刻
 
  def initialize(name, artist, duration, release)
    @name = name
    @artist = artist
    @duration = duration
    @release = release
    artist.add_song(self)
  end
end

RubyクラスのメソッドがGraphQLのフィールドに対応します。メソッドは引数なし、またはargs={}という形式である点に注意してください。これはGraphQL APIが期待するシグネチャであり、今回の実装もそれに従っています。initializeメソッドはサンプル用のデータをセットアップするために使います。詳細は後述します。

クエリルートのクラスも定義が必要です。SDLのQueryルート型に対応するartistメソッドに注目してください。また、artists用のattr_readerも追加されています。SDLドキュメントのQuery型にこのフィールドを追加すれば、そのままAPIとして公開されます。

class Query
  attr_reader :artists
 
  def initialize(artists)
    @artists = artists
  end
 
  def artist(args={})
    @artists[args['name']]
  end
end

GraphQLのルート(クエリルートと混同しないよう注意)は、クエリルートのさらに上位に位置します。GraphQLでは、ルートはオプションで3つのフィールドを持つと定義されています。ここでのRubyクラスはqueryフィールドのみを実装しています。イニシャライザでは、筆者がよく聴くニュージーランドのインディーバンドのデータを読み込んでいます。

class Schema
  attr_reader :query
  attr_reader :mutation
  attr_reader :subscription
 
  def initialize()
    # テスト用のデータをセットアップ。
    artist = Artist.new('Fazerdaze', ['Morningside', 'Auckland', 'New Zealand'])
    Song.new('Jennifer', artist, 240, Time.utc(2017, 5, 5))
    Song.new('Lucky Girl', artist, 170, Time.utc(2017, 5, 5))
    Song.new('Friends', artist, 194, Time.utc(2017, 5, 5))
    Song.new('Reel', artist, 193, Time.utc(2015, 11, 2))
    @artists = {artist.name => artist}
 
    @query = Query.new(@artists)
  end
end

最後のセットアップは実装固有の部分です。ここでは、サーバーを初期化して/graphqlというHTTPリクエストパスのハンドラを登録し、起動します。

Agoo::Server.init(6464, 'root', thread_count: 1, graphql: '/graphql')
Agoo::Server.start()

続いて、GraphQL実装に先ほど定義したSDL($songs_sdl)を設定し、サーバーがリクエストを処理している間、アプリケーションはスリープ状態になります。

Agoo::GraphQL.schema(Schema.new) {
  Agoo::GraphQL.load($songs_sdl)
}
sleep

このサンプルのコードはGitHubで公開されています。

APIを使ってみる

APIをテストするには、Webブラウザ、Postman、またはcurlが使えます。

試してみるGraphQLクエリは以下のようになります。

{
  artist(name:"Fazerdaze") {
    name
    songs{
      name
      duration
    }
  }
}

このクエリはFazerdazeという名前のArtistを要求し、namesongsをJSONドキュメントとして返します。各Songについては、そのnamedurationがJSONオブジェクトとして返されます。出力は次のようになるはずです。

{
  "data": {
    "artist": {
      "name": "Fazerdaze",
      "songs": [
        {
          "name": "Jennifer",
          "duration": 240
        },
        {
          "name": "Lucky Girl",
          "duration": 170
        },
        {
          "name": "Friends",
          "duration": 194
        },
        {
          "name": "Reel",
          "duration": 193
        }
      ]
    }
  }
}

クエリから任意の空白文字を取り除いたうえで、curlでHTTP GETリクエストを送ると、同じ内容が返ってきます。

curl -w "\n" 'localhost:6464/graphql?query=\{artist(name:"Fazerdaze")\{name,songs\{name,duration\}\}\}&indent=2'

クエリ内のdurationreleaseに置き換えて試してみてください。RubyのTimeオブジェクトがJSON文字列へ変換される様子を確認できます。

曲のアウトロ

GraphQLで遊ぶのはとても楽しい経験でした。読者の皆さんも一緒についてきて、何かを学べたのであれば嬉しく思います。ありがとうございました。素晴らしい観客でした。もしRubyについてさらに語りたい方は、バーでグッズを販売していますので、お気軽にお立ち寄りください。

  1. TCMallocを使ってRubyのメモリ割り当てをプロファイリングする方法

    Rubyでは、メモリの割り当てはどのように行われているのでしょうか? Rubyは「ページ(page)」と呼ばれる塊単位でメモリを確保し、新しく生成されたオブジェクトはこのページの中に格納されていきます。 しかし、 ページがいっぱいになると、さらなるメモリが必要になります。 そのときRubyは、malloc関数を使ってオペレーティングシステム(OS)に追加のメモリを要求します。 このmalloc関数はOS標準で提供されているものですが、代替となる実装に差し替えることも可能です。 その代表的な実装のひとつが、Google製のTCMallocです。 TCMallocは「gperftools(Goog

  2. Rubyでパーサーを自作する方法!StringScannerを使った実装手順を徹底解説

    パース(構文解析)とは、文字列の集まりから意味を読み取り、プログラムが扱える形のデータへと変換する技術です。正規表現でも文字列の解析は可能ですが、すべての場面に適しているわけではありません。 たとえば、正規表現でHTMLを解析するのはあまり良い方法ではないというのは、プログラミング界隈ではよく知られた話です。 Rubyにはnokogiriという強力なライブラリがあり、HTMLの解析はこれに任せられます。しかし、自分でパーサーを一から作ってみると、文字列処理や構文解析の仕組みについて多くのことを学べます。それでは早速始めていきましょう! Rubyでのパースの基本:StringScannerクラス