graphql-ruby + Railsで簡易なクエリとミューテーションが使えるGraphQL APIを実装する

会社でGraphQLのハンズオンがあったのをきっかけに、最近はGraphQLのサーバ側実装をちょっと触っています。

graphql-rubyを使うと、RubyでGraphQL APIを実装することができます。今回はRailsでGraphQLのクエリミューテーションを実装してみました。

graphql-ruby使用時のRailsプロジェクトにおけるファイル/ディレクトリ構成

rails generate graphql:install すると、ジェネレータが app 配下に次のようなディレクトリ構成を作ります。

app/controllers
└── graphql_controller.rb
app/graphql
├── app_schema.rb
├── mutations
└── types
    ├── mutation_type.rb
    └── query_type.rb

また、ジェネレータは config/routes に次のルーティングを追加します。GraphQL APIへのあらゆるHTTPリクエストは次のエンドポイントへPOSTメソッドで送信することとなります。

post "/graphql", to: "graphql#execute"

今回の前提条件

このエントリでは、RailsはAPIモードを使います。また、今回は次のようなデータ構成になっていることとします。

f:id:kymmt90:20170726233709p:plain

User の認証機構は作りません。実際はなんらかの認証があって、クエリ実行時に context というオブジェクトを通じて current_user を扱うことになります。

今回のゴール

今回は、次のようなクエリとミューテーションをAPIへリクエストできるようにします。

まず、クエリです。これはサーバ側のデータを取得するタイプのリクエストで、いまは引数 email で指定した user に関するデータを取得しようとしています。

{
  user(email: "foo@example.com") {
    email
    article {
      edges {
        node {
          title
          body
        }
      }
    }
  }
}

この場合、次のようなレスポンスが返ります。

{
  "data": {
    "user": {
      "email": "foo@example.com",
      "article": {
        "edges": [
          {
            "node": {
              "title": "Title 1",
              "body": "this is the body"
            }
          },
          {
            "node": {
              "title": "Title 2",
              "body": "this is the body 2"
            }
          }
        ]
      }
    }
  }
}

次にミューテーションです。これはサーバ側のデータを変更します。

{
  createArticle(article: {user_email: "foo@example.com", title: "Another test", body: "This is another test"}) {
    article {
      title
      body
    }
  }
}

この場合、article が1件増えて、次のようなレスポンスが返ります。

{
  "data": {
    "user": {
      "email": "foo@example.com",
      "article": {
        "edges": [
          {
            "node": {
              "title": "Title 1",
              "body": "this is the body"
            }
          },
          {
            "node": {
              "title": "Title 2",
              "body": "this is the body 2"
            }
          },
          {
            "node": {
              "title": "Another test",
              "body": "This is another test"
            }
          }
        ]
      }
    }
  }
}

実装手順

利用するデータの型を書く

リクエストやレスポンスに出てくるデータの型 app/graphql/types 配下の user_type.rbarticle_type.rb にGraphQL APIで利用するデータの書きます。

# app/graphql/types/user_type.rb
Types::UserType = GraphQL::ObjectType.defind do
  name 'User'

  field :email, !types.String
  connection :articles, Types::ArticleType.connection_type
end

# app/graphql/types/article_type.rb
Types::ArticleType = GraphQL::ObjectType.define do
  name 'Article'

  field :title, !types.String
  field :body, !types.String
  field :user, Types::UserType
end

field は型が持つ属性であり、名前とスカラー型(types.String, types.Int, types.ID など)を指定します。また ! で非nullであることを指定します。

Types::UserTypearticlesconnection というヘルパを使っています。これはRelay由来のconnectionというページネーションを扱うための仕組みを使えるようにしてくれるものです。connectionによるページネーションの詳細については次のページを見てください。

今回はクエリのルート階層に書くフィールドは user になりますが、これは name 'Query' を宣言している型に書きます。

# app/graphql/types/query_type.rb
Types::QueryType = GraphQL::ObjectType.define do
  name 'Query'

  field :user do
    type Types::UserType
    argument :email, !types.String
    resolve ->(obj, args, ctx) {
      User.find_by(email: args['email'])
    }
  end
end

ここでは、argument にクエリで指定する引数を書いています。また、resolveuser クエリが投げられたときの User レコードの取得方法をラムダ式で書いています。

ここまでで、ゴールとしていたクエリをリクエストできるようになりました。

ミューテーションと入力データの型を書く

次にミューテーションを書きます。まずは name 'Mutation' を宣言している型にフィールドとしてミューテーションを書いてきます。

# app/graphql/types/mutation_type.rb
Types::MutationType = GraphQL::ObjectType.define do
  name 'Mutation'

  field :createArticle, Types::ArticleType do
    argument :article, Types::ArticleInputType

    resolve ->(o, args, c) {
      user = User.find_by!(email: args[:article][:user_email])
      user.articles.create!(title: args[:article][:title], body: args[:article][:body])
    }
  end
end

ここで2点ほどポイントがあります。

  • argument の型が Types::ArticleInputType である
  • resolve でレコードを作成している

Types::ArticleInputType は別途定義している次のような型です。

# app/grpahql/types/article_input_type.rb
Types::ArticleInputType = GraphQL::InputObjectType.define do
  name 'ArticleInputType'

  argument :user_email, !types.String do
    description 'Email address of the user'
  end

  argument :title, !types.String do
    description 'Title of the article'
  end

  argument :body, types.String do
    description 'Body of the article'
  end
end

この型を引数とすることで、次のような Types::ArticleInputType 型の引数としてミューテーションに作成したい article のデータを渡すことができます。この方法だと型の再利用性が高まります。

createArticle(article: {user_email: "foo@example.com", title: "Another test", body: "This is another test"}) {
  # ...
}

resolve では、ミューテーションの実際の処理として、対象ユーザの関連レコード article を作成しています。

これでゴールとしていたミューテーションもリクエストできるようになりました。

サンプルコード

素振り用に実装した上記コードを次の場所に置いています。

github.com

Yokohama.rb Monthly Meetup #82に参加した

2017-07-08(土)のYokohama.rb Monthly Meetup #82参加メモです。

yokohamarb.doorkeeper.jp

Rubyレシピブック

レシピ257から260まででした*1。259, 260についてメモ。

259: リモートホストが稼働していることを確認する

リモートホストが動いているかを確認するためにTCP接続を試みるメソッドについてのレシピです。

本文中では、TCPの7番ポートで受け付けるechoサービスに対して、TCPで接続を試みることでリモートホストが動いているかを確認しています。とはいえ、実際にやってみると、TCP7番を開けているサーバはほとんどなさそうでした。

コード例では timeout ライブラリによって導入される Kernel#timeout を使っています。このメソッドはRuby2.3からdeprecatedになっており、使うと次のメッセージが表示されます。実際には Timeout#timeout へのエイリアスになっているようです*2

Object#timeout is deprecated, use Timeout.timeout instead.

メッセージのとおり、現在は明示的にモジュール関数 Timeout.#timeout を使うのが望ましいでしょう。

Timeout.timeout do
  TCPSocket.open(host, service) do
    # pass
  end
end

260: 別プロセスのRubyオブジェクトにアクセスする

dRubyを使った分散オブジェクトの操作についてのレシピです。dRubyを実際に動かしてみたのは初めてだったのでおもしろかったです。次のようなことを話していました。

  • リモートのオブジェクトに対して呼び出したメソッドはサーバ側で実行される
    • クライアント/サーバ間ではMarshalを使ってやり取りしているが、ブロック内のコード(要はProc)はMarshalでシリアライズできないので、クライアント側で実行される
  • 2,3文字ぐらいの文字列を1000万要素ぐらい入れた配列をクライアントからサーバに送ろうとすると、なぜかクライアントが死ぬ…
  • dRubyを使ってカードゲームのUNOを作ると楽しそう

その他

以前書いた「DockerでRailsの開発環境を作る」ブログ記事を@miyohideさんが読んでいたということで、あらためて何をやったか紹介しました。

blog.kymmt.com

次のようなコメントをもらいました。

  • たとえばDBへの接続のためにIPアドレスは必要?
    • docker-compose.yml でつけたサービス名をそのまま使える
  • 本番環境はコンテナではない?
    • 今は開発環境だけ
      • エンジニアとデザイナの環境を統一して、うまく動かないときの原因切り分けを楽にしたかったのが最初のモチベーションのため
  • ホストからコンテナへのファイルコピーが遅くなってくるのでrsyncを使っている
    • 現状そこをネックに感じるレベルには達してなかった
  • コンテナが溜まってくるのはどうしてる?
    • コマンド実行は --rm をつけてやってもらう
    • こんな方法もあるそうです

また、@takeshyさんからDDD(ドメイン駆動設計)に基づいてRailsを使ったAPIサーバを作り直した事例について共有していただきました。

qiita.com

次のようなお話が聞けました。

  • ドメイン層は純粋なドメインロジックのコードだけになるのでわかりやすい
  • リポジトリ層はActive Recordを使わずに生SQLを書くことが多い
  • いかにimmutableな値オブジェクトに寄せるかがキモ
  • ルートエンティティ経由でドメインロジックを呼び出す
  • コントローラ層は params の処理ぐらいだけやってアプリケーション層に委譲
  • アプリケーション層はコントローラ層のアクションと一対一対応している
  • ドメインの処理を終わった時などにイベントを飛ばして、それを監視しているサブシステムが通知処理などを実行する
    • メインのドメインに通知処理などが入らない
    • ユーザ作成など、実行した処理がすべてイベントとして記録されるので、ログ活用やデバッグが楽になる

懇親会

次の結論に達した気がします。



次回はまだ未定のようですが、普段どおりであれば8月の第2土曜日に開催されると思います。

*1:258「LDAPで情報を得る」は飛ばしました

*2:ref: https://github.com/nahi/httpclient/issues/289

Yokohama.rb Monthly Meetup #81に参加した

2017-06-10(土)のYokohama.rb Monthly Meetup #81参加メモです。

yokohamarb.doorkeeper.jp

3月の#78も参加していたのにメモを書いていなかったので、久しぶり感あります。

Rubyレシピブック

レシピ254から256まで。HTTP, SMTP, FTPというプロトコル3連発でした。

254: HTTPクライアントをつくる

HTTPクライアントを一からつくるというわけではなく、open-uri, net/http, httpclientを使ってみるというレシピでした。rest-clientというクライアントライブラリがあるという話も聞けました。

github.com

あとはlibcurlラッパのtyphoeusとかがありますね。

github.com

255: メールを送信する

net/stmpを使って、素のRubyスクリプトでメールを送るというレシピでした。ふだんはAction Mailerばかりなので新鮮。

今回はメールの受信テストにMailCatcherを使っていましたが、Railsのときはletter_openerを使ったり、Action Mailerにもプレビューがあるという話をしていました。

256: FTPでファイルを送受信する

Ruby, net/ftpというライブラリも標準添付されています。@igrepさんにPure-FTPdというFTPサーバを立ててデモしてもらいました。

バイナリファイル、テキストファイルの送受信がプロトコルとして存在していて、そのまま Net::FTP#getbinaryfile のように対応するメソッドが存在しています。get という名前ですが、返り値はなくローカルファイルを保存するという仕様。

その他

Rubocopを導入するとき、とっかかりのルールはどうしたらよさそうかという話をしたところ、onkcopの設定を参考にしてみるとよさそうというお話を聞いたので、それを眺めたりしていました。

github.com

あとはこのエントリをもくもく書いたり。

blog.kymmt.com



次回は2017-07-08(土)です。

yokohamarb.doorkeeper.jp