ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

graphql-batch でバックエンドへのクエリを減らす

こんにちは、hibariya です。最近 ミートアップ が開催されるなど、GraphQL が静かに注目を集めていますね。GraphQL は Web API で使えるクエリ言語です。GraphQL 自体は特定のデータベースに依存しないため、RDBMS を使ったアプリケーションで採用することも可能です。PostgreSQL を使う Idobata でも GraphQL の Public API を公開しました。GraphQL 自体がどういうものかについては、graphql.org や以下の資料が参考になるのではないかと思います。

GraphQL でサーバ側を実装するときに起こりがちな問題として、クライアントから投げられるクエリによっては RDBMS への問合せが大量に発生してしまうというものがあります。今回は RubyRails でこのような問題に対処する方法について書きます。

チャットアプリの例を使いたいと思います。次の例は、ログインしているユーザ (viewer) が閲覧できる発言 (message) の一覧と各発言が投稿されたチャットルーム (room) の名前を取得する GraphQL のクエリです。それぞれ、一対一に対応する Active Record のモデルがあるとします。

query {
  viewer {
    # a message belongs to a room
    messages(first: 10) {
      body
      room { name } # message.room による問合せが最大10回
    } } }

特に工夫をしなければ、room { name } を取得する SQL は message の数だけ DB に投げられてしまいます。Ruby で表現すると次のようなイメージです。

Message.readable_by(current_user).limit(10).map {|message|
  # message.room が10回
  {body: message.body, room: {name: message.room.name}}
}

次の例は、あるチャットルーム (room) のメンバー (member) 一覧を取得する例です。

query {
  viewer {
    # a chat room has many members
    rooms(first: 10) {
      name
      members { name } # room.members による問合せが10回
    } } }

この場合も members { name } を取得するSQLが room の数だけ発行されてしまいます。

Room.readable_by(current_user).limit(10).map {|room|
  # room.members が10回
  {name: room.name, members: room.members.map {|member| {name: member.name} }
}

このような問題の解決方法のひとつとして、graphql.org の Best Practices では複数の問合せをひとつにまとめる Batching を挙げています。これの Ruby での実装としては graphql-batch という gem があります。graphql-batch は特定の DB やライブラリに依存しておらず、問い合わせをする部分も自分で実装できるため、比較的導入しやすい gem です。さっそく使ってみましょう。

graphql-batch を使ってみる

graphql-batch を使うと「ひとつひとつの問合せをいったん Promise として保留にしておき、あとでまとめて問合せ」られるようになります。この「まとめて問合せ」を行なう処理は Loader というかたちでアプリケーション側に実装します。

class RecordLoader < GraphQL::Batch::Loader
  def initialize(model_class)
    @model_class = model_class
  end

  def perform(ids)
    @model_class.where(id: ids).each do |record|
      fulfill record.id, record
    end

    ids.each do |id|
      fulfill id, nil unless fulfilled?(id)
    end
  end
end

RecordLoader#perform(ids) が実際に問合せを行なう部分です。これを使って、message が投稿された room (message.room) を取得するための resolver を次のように書けます。

MessageType = GraphQL::ObjectType.define do
  field :body, !String
  field :room, !RoomType do
    resolve -> (message, args, ctx) {
      # message.room の代わりに:
      RecordLoader.for(Room).load(message.room_id) # => a Promise
    }
  end
end

RecordLoader.for(Room).load(…) は実際の関連 (room) ではなく Promise を返します。ここではいったん Promise を返しておき、必要になった時点でまとめて問合せて fulfill するというわけです。

RecordLoader#for は引数ごとに RecordLoader インスタンスをひとつだけ作って返します。例えば RecordLoader.for(Room)RecordLoader.for(Message) では違う Loader オブジェクトが返りますが、同じ引数で2回呼んだときは前回と同じものが返ってきます。RecordLoader#new の引数は、問合せをどういった単位でまとめたいかによって決まると言えるでしょう。

RecordLoader#load には、ロードする対象を一意に特定するためのキーを渡します。この例では Active Record のプライマリキー (id) を渡しています。ここで渡した id は各 Promise を解決するタイミングで RecordLoader#perform に ids としてまとめて渡されます。このようにして、元々は複数の message.room だったものを Room.where(id: ids) というひとつの問合せにできます。

Loader の応用例

Loader の実装としては、上記の RecordLoader の他に ActiveRecord::Associations::Preloader と組み合わせるというやり方も考えられます。これによって Active Record の eager loading の仕組みを利用できます。

class HasManyAssociationLoader < GraphQL::Batch::Loader
  def initialize(_model_class, association)
    @association = association
  end

  def perform(owners)
    ActiveRecord::Associations::Preloader.new.preload owners, @association

    owners.each do |owner|
      fulfill owner, owner.public_send(@association).to_a
    end
  end
end

この Loader を has_many な関連である room.members の読み込みに使うことで、複数の room.members をひとつの Preload に置き換えられます。

RoomType = GraphQL::ObjectType.define do
  field :name, !String
  field :members, [!UserType] do
    resolve -> (room, args, ctx) {
      # room.members の代わりに:
      HasManyAssociationLoader.for(Room, :members).load(room) # => a Promise
    }
  end
end

注意点として、この方法は関連を一気に読み込んでしまうため、数の多い has many な関連の一部だけがほしいような場合には不要なレコードを大量に読み込んでしまう可能性があります。

また、冒頭でも述べましたが graphql-batch 自体は特定の DB やライブラリに依存していないので Active Record 以外にも応用できます。例えば、Rails のキャッシュを fetch ではなく fetch_multi でまとめて取得する Loader をつくり、バックエンドとのラウンドトリップを抑えるという使い方もできるでしょう。

おわりに

主に graphql-batch を使ってバックエンドへの問合せを減らす方法について、基本的なものではありますが、いくつか例を紹介しました。Ruby で GraphQL を触りはじめた人たちの参考になれば幸いです。

今回のような N+1 Query を解決する際のよくあるやり方としては、Rails アプリであれば Active Record の includeseager_load などを用いた、いわゆる Eager Loading を用いることだと思います。同じことを GraphQL で行なう gem としては graphql-query-resolver があります。この gem は ActiveRecord::Associations::Preloader を使って、クエリに含まれる関連をまとめて問合せてくれるようです。ただ、試しに少し使ってみたところ、数の多い has_many な関連を扱う場合やクエリのカスタマイズをしたい場合にはすこし工夫が必要そうな印象でした。もしも新たな情報が得られたら、またどこかで共有できればと思います。

改訂2版 パーフェクトRubyが出版されました

こんにちは。takkanm です。

本日 2017/05/17 は、弊社メンバーが関わっている書籍パーフェクト Ruby の改訂2版の発売日です。

パーフェクト Ruby は、Ruby に関する内容を言語仕様から、メジャーなライブラリまでを網羅することを目的とされた書籍です。 弊社からは、私と hibariya が執筆に携わっています。

初版は 2013 年に発売されたので、今回は 4 年ぶりの改定となります。 改定内容としては、初版時にターゲットとした Ruby のバージョンである 2.0 から、昨年末にリリースされた 2.4 までの差分をアップデートしました。対象バージョンのアップデートにおいては追加されたメソッドが追加されているのはもちろん、初版から大きく仕様の変わった refinements についての内容を大きくリライトしました。 さらに、初版が発売後に対象を Rails に絞ったパーフェクト Rails が出版されたということもあり、Web 関連の章が削られ、代わりに test-unit を使ったテストの章が追加しています。 このように追加要素も多いので、初版を持っている方にもオススメです。

紙の本は、Amazon やお近くの書店でお求めいただけると思いますし、GihyoDigitalPublishing では PDF でもお買い求めいただけます。

みなさんが Ruby でプログラムを書く際の良きお供になれば幸いです。

併せて読みたい

AgileJapan2017とワークショップに参加して、モダンアジャイルについて考えてきました

こんにちは、平田です。 少し前の話になりますが、AgileJapan2017と、翌日に開催されたモダンアジャイルワークショップに参加してきました。

www.agilejapan.org

agilejapan.org

モダンアジャイルについて

アジャイルソフトウェア開発宣言が出されてから15年以上が経ち、その現代的な解釈を「モダンアジャイル」として Joshua Kerievsky さんがまとめました。 モダンアジャイルの4つの原則は以下の通りです。

  • Make people awesome(人々を最高に輝かせる)
  • Make safety a prerequisite(安全を必須条件にする)
  • Experiment and learn rapidly(高速に実験&学習する)
  • Deliver value continuously(継続的に価値を届ける)

それぞれの内容については、いろいろなブログ記事も出ているので、ググっていただくとして、私からは基調講演とワークショップの2日間で感じたことをいくつか紹介します。

f:id:m_pixy:20170414181136j:plain

安全

4つの原則のうちのひとつに含まれる「安全」については、基調講演の中でもかなり多くの時間を割いていました。安全は優先順位が一番高いというわけではなく、必須の前提条件であるという考え方です。Googleでの心理的安全の重要さが分かった調査結果は有名ですが、他にもメンバーが様々な挑戦をするために安全なミーティングをやるための5箇条など様々なエピソードが紹介されました。

面白かったのは、Slackが非アクティブなユーザーがいることを伝えてきて、そのユーザーの分の料金を返金してくれたというエピソードでした。Slackというサービスは顧客の予算を守ってくれるという安心感をくれたという話で、開発メンバーにかぎらず、関わるすべての人の安全のために行動するということの重要性が伝わりました。

アウトプットではなくアウトカム(成果)に着目する

良いソフトウェアを作ろう、動作するソフトウェアを作ろうと考えるのではなく、それらが生み出す「成果」に着目しようということです。 ワークショップの中で「継続的に価値を届ける」ことをチームで改善していくために、計測すべき指標を考えようというものがありました。私たちのチームでは、「継続的にチームがソフトウェアをリリースすることが重要である」と考え、たとえば日単位での本番リリース数などを数えようとしていたのですが、Joshuaさんから「それは成果なのか?」という問いかけがありました。

「包括的なドキュメントよりも動くソフトウェアを」というアジャイルマニフェストの先にある、「ソフトウェアがもたらす成果」に着目することの重要性に気づくことができました。

「要求」を「実験」として捉える

アジャイルマニフェストの「変化に対応する」というのは受動的である。高速に実験し、高速に学習するために、顧客からの要求さえも実験であると捉え、早く失敗していこうという話がありました。

まとめ

今回の基調講演や、Agile2016での基調講演(リンク)では、モダンアジャイルの4つの原則について、考えられた背景等をエピソードを交えて紹介されていました。実際のプロセスについては、チームで考えていくべきという考え方であると理解しましたが、ワークショップの中で紹介されたプロセスでは、クリックテストやセットベース設計など、UXやリーン、DevOpsなどの文脈で語られることの多いプラクティスが紹介されていました。モダンアジャイルの原則は、これらの考え方やプラクティスを束ねる概念として利用してみるのもの面白いと感じました。

最後に

さて、今回のAgileJapan2017には、2016年に引き続いて、永和システムマネジメントはサテライトスポンサーとして協力をしています。 聞きなれないスポンサーですが、これは全国で開催されるAgileJapanの地方サテライトに、永和のメンバーが講演や運営のサポートとして参加するというスポンサーです。

身近でアジャイルの話が聞ける、議論できるチャンスですので、ぜひお近くの方は参加いただければと思います。弊社のメンバーも各地のサテライトに参加します!

私も直近では、栃木サテライトにお邪魔する予定です。皆様とお会いできるのを楽しみにしています!

www.agilejapan.org

Repro様とExtreme Fish Bowl を開催しました

こんにちは @color_box です。

2017/01/27 に Repro のエンジニアの皆様と Extreme Fish Bowlを開催したので、それに関する報告をします。


Extreme Fish Bowl

Extreme Fish Bowlとは、皆に見られながらペアプログラミングをするというものです。 その場にいる全員の中でペアを組み、1台のマシンを使ってかわるがわるペアプログラミングをしていきます。 ペアプログラミングをしていない他の参加者は、観戦にまわります。

5分を1ターンとして、ターン終了時にドライバーが抜け、ナビゲータがドライバになります、ナビゲータは他の参加者の中から一人入って次のターンに進みます。 そうやってナビゲータとドライバを一人ずつ交代しながらプログラミングを進めていくのがExtreme Fish Bowlです。

プログラミングの対象となるお題は事前に与えられており、お題を解くようなコードを全員で考えていきます。 作業風景は常にスクリーンに映し出されており、会場にいる全員に共有されます。 もちろん手を動かしていない他の参加者も、ペアプログラミングをしていえる二人に温かい野次のような声援を飛ばして構いません。

f:id:color_box22:20170127204439j:plain


当日のコード

当日書かれたコードは下記リポジトリにおいてあります。 github.com

続きを読む

2017 アジャイル事業部 年始のご挨拶の会のご案内

f:id:E_Mattsan:20161213093204p:plain

2016 年も残すところ半月あまりとなりました。インフルエンザも流行の気配を見せています。みなさんもお気をつけてください。

そんな年の瀬ですが、アジャイル事業部で来年のカレンダーを作りました。1月から12月までひとりひと月ずつ、ソフトウェア開発にまつわるテーマのエピソードを載せています。

そこで年明けの1月11日(水)、年始のご挨拶として各月を担当したメンバーによるLTを行うことにいたしました。簡単な軽食とお飲み物をご用意しますのでご来場いただき楽しい時間をお過ごしいただけたらと思います。 またご来場された方にはお年賀としてこちらのカレンダーをお渡しいたします。

ご参加を希望される方は申し込みページからご登録をお願いします。

皆様のご来場をお待ちしています。

f:id:E_Mattsan:20161213090537j:plain

勤怠ボタンのつくりかた

f:id:htkymtks:20161212180250j:plain

こんにちは。永和システムマネジメントの内角低め担当、はたけやまたかし( @htkymtks )です。

Amazon Dash Button に触発されて「勤怠ボタン」をつくってみたのでご紹介します。(↓こんなのです)

勤怠ボタンとは?

私の所属する永和システムマネジメントでは在宅勤務が認められています。部署ごとに在宅勤務の運用ルールは異なりますが、私の所属するアジャイル事業部では在宅勤務時の作業開始と終了の連絡を社内チャットツール「Idobata」 *1 へ投稿するルールになっています。

勤怠ボタンの「🏠」と「🔚」を押すことで作業開始と終了を Idobata へ投稿することができます。

f:id:htkymtks:20161212160458j:plain:w300

ハードウェア

勤怠ボタンはESPr DeveloperというWiFi接続可能な小さなマイコンを利用しています。ESPr Developerに載っているチップ「ESP8266」にはArduino開発環境が用意されているため、Arduino ライクな開発を行うことができます。

回路図

回路はこんな感じ。至ってシンプル。

f:id:htkymtks:20161213100644p:plain:w300

ブレッドボード上で配線するとこんな感じ。

f:id:htkymtks:20161213102135j:plain:w300

事前準備

事前に「Arduino IDEのセットアップ」と「ESPr DeveloperのArduino化」を行う必要があります。以下のサイトを参考にESPr DeveloperのArduino化を行いました。

以下のコードで14ピンに接続したLEDがチカチカすればOKです。

void setup() {
  pinMode(14, OUTPUT);
}

void loop() {
  digitalWrite(14, HIGH);
  delay(1000);
  digitalWrite(14, LOW);
  delay(1000);
}

ソフトウェア

以下が勤怠ボタンのソースコードです。

  • 接続するWiFiSSID
  • 接続するWiFiのパスワード
  • Idobataの認証トークン(取得方法は後述)
  • 投稿先のルームID(取得方法は後述)

を設定して、Arduino IDEからESPr Developerへ書き込みます。

loopの中で12ピンと13ピンの値を監視し続け、13ピンにつながった「🏠」ボタンが押された場合は「:house:」が、12ピンにつながった「🔚」ボタンが押された場合は「:end:」が Idobata へと投稿されます。

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>

// WiFi SSID & Password
const char* ssid = "<Your WiFi SSID>";
const char* password = "<Your WiFi Password>";

// サーバ証明書のフィンガープリント
String fingerprint = "DC 0B 75 2D 66 75 42 65 AC 6D 68 C5 53 59 A4 25 E9 12 87 C2";

// 認証トークン
// curl -X POST -H "Content-type: application/json" -d '{"grant_type":"password", "username":"<Your email>", "password":"<Your password>" }' https://idobata.io/oauth/token | cut -b17-82
String authToken = "<Your auth token>";

// 投稿先ルームID
String roomID = "<Your kintai room ID>";

void setup() {
  pinMode (12, INPUT_PULLUP);
  pinMode (13, INPUT_PULLUP);
  pinMode (14, OUTPUT);
  
  Serial.begin(115200);

  // Connect to WiFi
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  piko(1);
}

void loop() {
  if (digitalRead(12) == LOW) {
    post(":end:");
    piko(3);
  }

  if (digitalRead(13) == LOW) {
    post(":house:");
    piko(2);
  }
}

// 指定した回数ピコっと光る
void piko(int n) {
  for (int i = 0; i < n; i++) {
    digitalWrite(14, HIGH);
    delay(100);
    digitalWrite(14, LOW);
    delay(100);
  }
}

void post(String message) {
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        http.begin("https://idobata.io/api/messages", fingerprint);
        http.addHeader("Content-Type", "application/json");
        http.addHeader("Authorization", "Bearer " + authToken);

        int code = http.POST("{\"message\":{\"source\":\"" + message + "\",\"room_id\":\"" + roomID + "\"}}");
        if (code > 0) {
          Serial.printf("ok: %d\n", code);
        } else {
          Serial.printf("error: %d\n", code);
        }
    }
}

SSLサーバ証明書のフィンガープリント

https 通信を行うためにサーバ証明書のフィンガープリントが必要です。ブラウザで https://idobata.io を開いて証明書の詳細を表示、「指紋 - SHA1」の値を使ってください。

f:id:htkymtks:20161212144546p:plain:w300

Idobataの認証トークン

Idobata へ投稿するためには、httpリクエストヘッダに認証トークンを付与する必要があります。ターミナル上で以下のコマンドを叩くと Idobata の認証トークンを取得できます。ソースコード中の authToken 変数へ取得した値をセットしてください。

$ curl -X POST -H "Content-type: application/json" -d '{"grant_type":"password", "username":"<Your email>", "password":"<Your password>" }' https://idobata.io/oauth/token | cut -b17-82

投稿するルームの ID

投稿するルームの ID は Idobata の「ROOM SETTINGS」から確認できます。ソースコード中の roomID 変数へ取得した値をセットしてください。

f:id:htkymtks:20161212153032p:plain:w300

ユニバーサル基板へ移植

ブレッドボード上で作成した回路をユニバーサル基板へ移植しました。

これが

f:id:htkymtks:20161213102135j:plain:w300

こうなりました。小さくてカッコイイですね。ちっちゃいは正義だ。

f:id:htkymtks:20161212180250j:plain:w500

最後に

以上、勤怠ボタンのつくりかたでした。

本当はAmazonDashButtonのガワだけでも使えないかなあと考えていたのですが、Dashのガワの再利用は思っていたよりも難しかったため基盤ムキ出しになっちゃいました。無念...。

また、今回はじめてESPr Developerを使ってみたのですが、小さくて安くてパワフルでお手軽な素晴らしいマイコンでした。ESPr Developer最高かよ。これからもESPr Developerを使ってIdobataと連携するボタンをいろいろと作りたいなあと考えております。はたけやま先生の次回作にご期待下さい!