ボクココ

個人開発に関するテックブログ

外部サービスの API で取ってきたデータを一括でDB登録する

ども、@kimihom です。

f:id:cevid_cpp:20191024173445j:plain

最近はいろいろな企業が API を提供してくれるようになってきた。外部サービスの API で、例えば顧客情報などを一括で取り込みたいというケースはよくあることだろう。私はこうして実装したということで記事として起こしてみる。

バックグラウンド処理

こうした外部サービスとのやりとりは、外部サービス側のレスポンス速度などで実行時間が大きく変動するため、バックグラウンドで呼び出す流れが基本となるだろう。私はバックグラウンド処理は基本的に AWS Lambda 側に渡して実行させるようにしている。AWS Lambda 側で処理をさせると開発やテストなどが個人的には簡単でやりやすくて気に入っている。

サーバー => AWS Lambda => 外部サービスAPI 呼び出し => サーバー => データベース

AWS Lambda で外部API を呼んで一気にデータを取ってきた後、データを整えて JSON 形式にする。その JSON を AWS Lambda から自前のサーバーへ HTTP リクエストを送って、そのリクエストを判定してデータベースに保存していくという流れだ。

実装の中で、AWS Lambda 側の処理とサーバーでデータを受け取る部分の実装に関して記す。

データ取得における注意点

まずは AWS Lambda 側でデータ取得する際の注意点を記しておく。

一括取得の API はデータ数と API 側の制約に注意しよう

一括取得の API は、想像の通りそれだけでそれなりにサーバーに負荷がかかる。そのため、一部の API では秒間のリクエスト制限を設けているところもある。何も考えずに全データ一気に取ってくるぜ!ってことやっちゃうと、途中でデータ取得に失敗してうまくいかないことがよくある。ほとんどの API ドキュメントには、このような制限が記載されているので確認しよう。API からデータをリスト取得する際の ページごとの取得件数や上限なども確認しておこう。

テスト段階では、外部サービスの API で取ってくるデータ量ってのはテストデータ程度なので問題になることはない。ただ実際に使っているユーザーの中には想像を超えるような大量のデータを外部サービス側で保存していることがある。なので仮にデータが何万件あっても API で全部とってこれちゃうような場合でも、実装の中に最大の取得上限は設けておいたほうがいい。外部サービス側で万・億並のデータがあったら、それだけで自分たちのデータベースがパンクしてしまう恐れがある。全データ取ってこなきゃ意味がない!っていう API 連携を予定している場合には、それでも期間などで絞り込みをして全データ取得って手段は控えるべきだと思う。ここまで書いてそれでも全部取るってなら私は止めはしない。

用件に満たないデータは予め整備しよう

外部サービス側の API では データがない(NULL)って場合も多くある。そうしたデータのないものも とにかく API で全部とってきてしまうと、それだけで無駄なリクエストとなってしまう。例えば自分たちのサービスではメールアドレスが必須だけど外部サービス API 側ではメールアドレスが必須ではない場合、メールアドレスの存在するデータだけを API のパラメータで指定できるなら指定しよう。メールアドレスありだけっていう API パラメータが指定できない場合は、自分たちのサーバーに送る前のデータ整備の段階で、メールアドレスのない情報は予め削除しておこう。これだけで多くのリクエストの無駄を解消することができる。全部自前のサーバーでやればいいやって判断は後々痛い目を見ることになる。

一括でデータを送ることは控えよう

例えば外部サービス API で 4,000件のデータを取ってこれたとしよう。そのデータを一括で 自前のサーバーにリクエストを送ることは決してしないように。それだけで自前サーバーがパンクする恐れがある。

100件ごとに データを区切って、AWS Lambda => 自前サーバー のレスポンスがちゃんと返ってきたら、次のデータを送るって流れを取ろう。最近の AWS Lambda は10分以上も実行させ続けられるので、数千件程度であれば問題になることはないはずだ。

データ保存における注意点

では次に AWS Lambda から自前のサーバーに渡ってきたときのサーバー側の注意点を上げよう。

リクエストの認証をしよう

AWS Lambda から送られてきた HTTP リクエストのみをデータ保存するように実装しよう。仮にもし 一括登録の URL が外部に漏れて、データを一括で送信された場合、意図しないデータ登録が発生してしまうリスクがある。どこまで厳密にするかは実装次第ではあるけど、最低限リクエストのパラメータを確認することはちゃんとしよう。

同一データの扱いに気をつけよう

例えば自分たちのデータでは メールアドレスはユニークでなければならないケースなどだ。外部サービス側では当然そんな制約がないことも多くあるので、その場合のデータ登録について実装を考慮する必要がある。私の場合、メールアドレスが登録されていない場合にはデータ挿入、メールアドレスが既に存在する場合にはデータ更新 というところで実装を分けている。サーバーに送られてきた JSON を一つ一つチェックし、データ一括挿入枠と一件ずつ更新するデータ更新枠の2つに分ける。

データ一括挿入枠に入ったデータは、activerecord-import を使って一括登録する。データ更新枠に入ったデータは、一件ずつ Update をしていく。アップデートだけだったとしても、1リクエストにつき 100件なので、そこまで負荷がかかる処理でもない。

終わりに

今後、SaaS のビジネスがますます賑わっていくにつれ、こうした API による連携ってのはどんどん増えていくことだろう。

利用ユーザーさんがより便利に活用できるように、Cool な実装で問題を解決していこう。

Rails コールバックによる S3 事故と対策

ども、@kimihom です。

f:id:cevid_cpp:20190601165719j:plain

Rails でコードを書いていたら、きっと一度は書いたことがあるだろう before_destroy で起こりうるリスクと対策について記す。

S3 のドメインが変わるお知らせ

先日、S3 の URL が変更されるとのお知らせが届いた。

dev.classmethod.jp

これを読んで、「そのうち対応が必要なら早いうちにやっちゃおう。"s3-ap-northeast-1.amazonaws.com" ってリージョンも長ったるしいし。」ってことでさっと対応してみよう。

User の Image URL の更新

User モデルがあったとして、その User に image_url で画像URLを保存するデータがある。そこで、画像を保存している S3 URL をアップデートする処理を書く。

そもそも、ユーザー画像に S3 のドメインから登録させるってのは、あまりすべきことではないだろう。ただ、Twitter ログインなどから画像を登録したり、CRM の画像を参照する可能性もあったことから、画像URL をドメインから DB に保存するようにするケースもあるだろう。

てことで、以下のようなコードを実行することになる。

prev_s3 = "s3-ap-northeast-1.amazonaws.com/myapp"
after_s3 = "myapp.s3.amazonaws.com"
User.where("image_url is not null").each do |c|
  c.update(image_url: c.image_url.sub(prev_s3, after_s3))
end; ""

画像の自動削除

ユーザーが画像を再度アップロードした時は、前に使っていた画像はもう使わないので削除するような処理を実装することがあるだろう。これをしないと使わない画像がどんどん溜まっていってしまうからである。具体的には以下のような形である。

class User < ApplicationRecord

  before_save :delete_image_if_image_changed

  def delete_image_if_image_changed
    return if self.attribute_was("image_url").blank? || !self.changed.include?("image_url")
    remove_s3_image(self.attribute_was("image_url"))
  end

end

これでユーザーが画像を繰り返しアップロードするたびに、前の画像は無事削除されるようになり、無駄に S3 の画像が残らず、費用の節約にもなる。

さて、このコールバックが定義された状態で、先ほどのコードを実行したらどうなるか、もうお分かりだろう。そう、画像URL だけが変わり、画像の実態が削除されてしまうのである。

S3 のバージョニングを有効にしておこう

画像が一括削除されてしまうような悲劇が起きたとしても、なんとか対応できるようにするために、S3 のバージョニングを有効にすることをお勧めする。S3 の対象のバケットに行って "プロパティ" -> "バージョニング" で 有効にすれば OK である。

「バージョニングを有効にするくらいならそもそも消さなければよくないか」という考え方もあるだろうが、使われている画像/使われていない画像が全てバケット内にあるようなカオスな状態にはしたくないケースでバージョニングが便利に使える。

これで、削除された画像もバージョン表示をすれば、取得が可能だ。S3 費用の節約よりも、安全に運用できる道を選ぶべきだろう。

そもそも、update はコールバックが呼ばれてしまうので、今回の場合は update_column を呼び出すのが正解ということになる。

終わりに

今回の根本である「Rails のコールバックがいけない」とは私は思わない。使い方次第ではコールバックは便利に使えるし、コードを短くできる。

本記事がより安全にサービス運用をするために考える きっかけになれば幸いである。

Ruby on Rails で自前トーストを作るサンプル

ども、@kimihom です。

f:id:cevid_cpp:20190407182905p:plain

Rails で開発をしていると、何かしらのメッセージをユーザーに掲示したい時がよく出てくる。

  • コンタクトを作成しました。
  • メンバーの招待に失敗しました。
  • アイテムを更新しました。

こういったときに flash っていう便利なメッセージ保存場所があるのはご存知かと思う。

flash - リファレンス - - Railsドキュメント

ただ、この Flash を View で普通に使うだけだと、Ajax リクエストした場合には Flash 表示させることができない。この問題を解決すべく、JavaScript 上でもトーストを同じように出せるような実装をしてみようというのが今回のテーマである。

トースト HTML の用意

まずはデフォルトで非表示のトーストの HTML をレイアウトに埋め込もう。CSS もよしなに用意しておく(padding とか細かいのは省略)。

<div id="common-messages">
  <p class="notice corner <%= "active" if notice %>"><span class="msg"><%= notice %></span></p>
  <p class="alert corner <%= "active" if alert %>"><span class="msg"><%= alert %></span></p>
</div>
#common-messages {
  position: relative;
  p {
    position: fixed;
    top: 12px;
    color: #ffffff;
    display: none;
    z-index: 99;
    &.notice {
      background-color: #009DB0;
    }
    &.alert {
      background-color: #de4437;
    }
    &.active {
      display: block;
    }
  }
}

もし noticealert の flash があれば、クラスを active にしておくのがポイントだ。まずはこれだけで、controller 側で flash が設定されていた場合の表示に備えられる。

では、JavaScript 側で任意のタイミングでトーストを表示させるようにしてみよう。

Toast = (function() {

  function show(target, text) {
    var $target = $("#common-messages ." + target);
    $("#common-messages ." + target + " .msg").text(text);
    $target.addClass("active");
    willHide();
  }

  var timeout = null;
  function willHide() {
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(function() {
      $("#common-messages .active").removeClass("active");
      timeout = null;
    }, 5000);
  }

  return {
    show: show
  };
}());

$(function() {
  // default toast hide
  Toast.willHide();
})

Toast.show("alert", "アラートメッセージだよ"); を呼ぶと、見事 JavaScript 側で任意のタイミングで同じトーストを呼び出すことができる。とりわけ、Ajax で送信したレスポンスでトースト表示させたい時などには便利に使えるだろう。 5秒後に勝手に非表示になるような実装にしているが、これは場合によっては x クリックした時だけ非表示にするなどの実装に変えることもあるかもしれない。

基本的な利用方針

普通にページ遷移するケースでは、JavaScript 側で呼ぶよりかは flash[:notice] などにセットしておいたほうが見通しが良くなる。とりわけ I18n で言語の切り替えをしている場合には、なおさら flash に詰めた方が楽だ。

もし JavaScript 側の結果メッセージなど、どうしても flash が使えない場合にだけ JavaScript 側で Toast.show を呼ぶようにしよう。この場合の I18n は別途、以下のような 言語対応の View を用意する必要が出てくるだろう。

<div style="display: none;">
  <span id="locale-created"><%= t('msg.created') %></span>
  <span id="locale-updated"><%= t('msg.updated') %></span>
  <span id="locale-deleted"><%= t('msg.deleted') %></span>
</div>

そんで、指定した要素の中身を Toast で表示するようにする。

Toast.show("notice", $("#locale-created").text());

I18n を気にしないなら、そのまま Toast に日本語を放り投げれば OK である。

終わりに

強化された自前トーストは、ページ遷移時のアラートだけでなく、JavaScript 側で起きたイベント時にもアラートを出すことを可能にした。今宵、JavaScript で色々なイベントが発生するようになってきているので、こうした対応は Rails で開発をしていると必要になる事項となるだろう。

適切なメッセージを表示させて、ユーザーに快適に Web アプリケーションを使ってもらえるようにしよう。

現場で使える Ruby on Rails 5速習実践ガイド

現場で使える Ruby on Rails 5速習実践ガイド

  • 作者: 大場寧子,松本拓也,櫻井達生,小田井優,大塚隆弘,依光奏江,銭神裕宜,小芝美由紀
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2018/10/19
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

Ruby on Rails 5.1 から 5.2 へアップデートログ

ども、@kimihom です。

f:id:cevid_cpp:20190301214729j:plain

今回は Ruby と Ruby on Rails をそれぞれバージョンアップさせたので、そのログを記そうと思う。

アップデート概要

Ruby と Ruby on Rails をそれぞれアップデート。そろそろ Rails 6.0 も出そうなタイミングってこともあって、定期的にアップデートしないとすぐに取り残されちゃうね。

  • Ruby 2.5.3 -> 2.6.1
  • Ruby on Rails 5.1.6 -> 5.2.2

Ruby on Rails 5.2 リリースノート - Rails ガイド

関連 Gem のアップデート

Ruby 自体のアップデートは特に問題なく終わった。Rails を 5.2.2 にあげようとした時 Devise が依存関係で引っかかったので先にアップデート。

  • Devise 4.4.1 -> 4.6.1

これで Rails 5.2 へアップグレードできた。いつも通りアップグレードのコマンドを実行。

bundle exec rails app:update

このコマンドで config 等で変わる差分を見ながらやっていく訳だけども、自分のプロジェクトでカスタマイズした config もあったのでひとまず 差分を見てエディタにコピっておいて、あとで一つずつマージする作業をしていった。そのほうが安全だね。

ソースコードの修正

bootsnap っていうアプリ起動を効率化させる Gem を追記。

【アプリ起動時間を50%削減】Rails5.2からデフォルトGemに採用されたBootsnapを検証した · カウル Tech Blog

belongs_to がデフォルトで required: true になったようだ。has_many な関係のモデルがあった時、親要素を指定する必要があるのがデフォルトになった様子。一部、モデルの親子関係で 親_id に nil を許容するものがあったので、そこの部分を修正。

# belongs_to :user
belongs_to :user, optional: true

SQLite を使っていると起動する度に警告が出てきたので、Arel を追記

eager_load(:user).order(Arel.sql("動的な Order 指定")) 

Arel.sqlを付けるだけじゃダメ!? Railsで&quot;Dangerous query method …”の警告が出たときの対応方法 - Qiita

安易に Arel つけて解決するんじゃねーぞという記事。自分の場合は完全にシステム側で文字の出し分けするだけだったので大丈夫だった。ユーザーの投稿内容を Arel 使ったりしたらそりゃあやばいよね。

ActiveStorage は使わない。

# config/application.rb 
+#require "active_storage/engine"
# config/environments/production.rb 
+  #config.active_storage.service = :local

Uglifier がなんかバグったので、ググって解決。

-  config.assets.js_compressor = :uglifier
+  config.assets.js_compressor = Uglifier.new(harmony: true)

form_with がデフォルトで remote: true にならなくなった。ソースコードを読むと、どうやら form_with_generates_remote_forms 設定を読み込んでいるようだ。config/initializers/new_framework_defaults.rb に追記。

+# Make `form_with` generate non-remote forms.
+Rails.application.config.action_view.form_with_generates_remote_forms = true

factory_bot もアップデートしたら、値の指定方法が {} でくくらないといけなくなっていたので対応。

 FactoryBot.define do
   factory :contact do
-    friendly_number "080-5352-3523"
-    name "Test Taro"
+    friendly_number {"080-5352-3523"}
+    name {"Test Taro"}
   end
 end

途中で気づいたこと

先ほど紹介したリリースノートを見ると、Rails 5.2 からRedis Cache Storeが組み込まれるようになったようだ。うちではすでに Redis をキャッシュストアと利用していたから、それがデフォルトで使えるようになった様子。

bin/yarn ってコマンドが出てきて、Heroku のデプロイが簡単になったかなと思ったらそういうわけではなかった。引き続き buildpack に nodejs を追加する必要があった。その方法に関しては既に下記に記してある。

www.bokukoko.info

終わりに

Rails 4 -> 5 のような大きいアップデートとは違うので、ちょっとした修正で無事アップグレードを終えることができた。ちゃんとテストコードを書いて、気になるところは手動でテストもしっかりやったから安心である。

新しいプロジェクトだったら Action Cable とか Active Storage とか使うだろうけど、やっぱ前からあるプロジェクトだとわざわざ同じ機能を作り直したくはないよね。これらの技術に関しても追従できるように、定期的にちょっとしたサンプル Rails プロジェクトを作って遊んでみたいと思う。

今回のアップデートで Ruby の高速化・省メモリや ActiveRecord の高速化などに期待したいところだ。

Ruby on Rails の魅力と思想

ども、@kimihom です。

私は Web フレームワークは Ruby on Rails を利用している。かれこれバージョン2.2 の頃から使い続けているので 7年以上になる。そこまでして私が Ruby on Rails を使い続ける魅力について個人的な想いを記していく。

Rails の作者 DHH と彼の環境

Rails の作者として有名な DHH(David Heinemeier Hansson) という名前は、 Ruby on Rails を触ったことがあるなら必ずや聞いたことがあるだろう。しかし、彼のいる会社 Basecamp がどんな想いでどんなことをしているかを知っている人は案外少ない。

Basecamp はプロジェクト管理の SaaS である。今や世界中に顧客を抱える超有名サービスであり、Basecamp は Ruby on Rails の最新版をプロダクトに反映され続けている。そしてこの Basecamp という会社は、資金調達をせずに自己資本でサービスを成長させた Bootstrap の事例として最も有名なサービスの一つである。資金調達をせずに、大成功とまで言える領域にたどり着けた背景には Ruby on Rails が深く関わっていることに間違いはない。そして、DHH の想いが Basecamp の成功と Ruby on Rails の思想に深く関わっているはずだ。

Rails は誰のためのフレームワークか?

私が7年以上 Ruby on Rails を触り続けている立場として、以下の考察を記していく。

Ruby on Rails は、極少人数(理想は一人) がサービス開発するのに理想的なフレームワークであり、それを目指し続けていると考えている。Ruby on Rails の敷かれたレールに乗ることで、一気に目的地に到達することができる。

特に大事なのが極少人数で構成されたチームのためのフレームワークであるという点だ。 3人よりは2人がいいし、2人よりは1人で Ruby on Rails を扱うのがいい。関連する人が多ければ多いほど、逆に DB と View が密になっていることで開発効率を悪くさせてしまう。大人数での開発だと Ruby の柔軟な書き方であったり、Ruby on Rails のレールに乗ったプログラムの書き方が、逆にマイナスの側面を与えてしまうだろう。

DHH の会社や思想を理解せずに、単に "Ruby on Rails はみんな使ってるから自社で使う" という判断をすると手痛い思いをする。そして、そんな人が Ruby on Rails 自体を批判するようになる。「Ruby そのものが自由すぎる」だとか、「他人の書いた Rails コードを読みたくない」だとかいう批判をよく聞くけど、そういった声は基本的に5人以上で Rails を触ってしまった場合に起きる問題である。これをしている時点で、私からすれば Rails の思想に乗っ取った開発をしていないのではないかと考える。

Rails は、少人数であり続けられるチームが常に理想の速度で Web アプリケーションを開発・維持するために理想なフレームワークであり続けるだろう。 例えばスタートアップのように人数が急激に増えるような環境においては、必ずや Rails を使っていること自体に問題が発生していくことだろう。この機能を実現するのに Rails のレールから外れなきゃいけないだとか、View と DB が密になり過ぎて担当を分けられないから ActionView を使わないといった選択をするってわけだ。その意味で、5人以上の開発チームで Ruby on Rails を使うのはナンセンスだと考えている。それだけの規模で開発するのであれば、Rails ではなくフロントエンドとバックエンドが最初から疎になっているものを選ぶべきではないか。

ここで一つ大切な事実をお伝えしたいのが、DHH 自体が人を急激に増やしていくスタートアップそのものを否定することが多いということである。これは日本でなぜかあまり知られていない大切な事実である。

Ruby on RailsのDavid Heinemeier Hanssonが語るお金と幸福の関係 - ログミーBiz

にも関わらず、急激に人数を増やしているスタートアップな方が Ruby on Rails を使って、「Rails 辛い」といっているのを見ると、私からすれば滑稽な姿にしか見えないってわけだ。

Rails のホスティングの選択

そんな Rails をホスティングするには、やはり Heroku がおすすめだ。Heroku は Hero + Haiku の造語であることはよく知られている。まさに、「5・7・5 として少ない文字(機能)を磨き続ける人を Hero にする」という考えは、Ruby on Rails の少人数でサービス運営する人の考えと素晴らしくマッチするのである。

今では色々な言語やフレームワークがサポートされているけど、Heroku 初期は Rails のホスティングサービスであったことからも、Rails の思想から生まれたホスティングサービスであることがわかるかと思う。

終わりに

巷では、Rails を使った開発プロジェクトのお話がよく出てくるけど、案外 Rails の作者や Rails の思想について語られることは少ない。

Ruby on Rails の What(何ができるか) よりも、Why (なぜ Rails が存在するか) について深く知る機会があればいいなと思う。Why を知った上で Rails を使えば、本当の意味での Rails のファンになることができよう。

今更ながら Rails 5.1 にアップデートした話

ども、@kimihom です。

f:id:cevid_cpp:20180121202451p:plain

先日、ようやく Rails 5.1 にアップデートしたので、それについて簡単にまとめを書いていこうと思う。

アップデートの経緯

前までは Rails 4.2.x の最新をアップデートし続けている形で運用していた。 Rails 5 以降の新機能はチェックしてきたんだけど、どうしても使いたい機能とかがなかったため、 Rails 4.2 のままでいいやと そのままにしてきた。最近になって Rails 5.2 の話も出てきたので、いよいよアップデートしないとレールに乗り遅れるってことで Rails4.2 からRails5.1 に一気に上げることにした。

Rails のアップデートに関しては Gem の依存関係をクリアして本元 Rails をアップデートした後、ひたすら失敗したテストコードを直しまくるという形で対応した。アップデートの詳細な方法に関しては、他のブログなどでたくさん取り上げられている話題であるので、本記事では省略させてもらう。ここら辺の作業はコードの整備も兼ねてできたので、前より綺麗なソースコードになって良かった。

Rails5.1 からの Yarn への移設はかなり気に入っている。これのおかげで今まで 外部 JavaScript は app/assets/javascripts/vendor みたいなところに突っ込んでいたのから解放された。今後は定期的に各 JavaScript ライブラリのバージョンアップを行って、JavaScript ライブラリのレールにもうまく乗っかっていきたいと考えている。また、CDN で公開されていた JavaScript ライブラリでも、それが npm で公開されている場合は全てそちらに移した。これによって application.js として一つにまとめられた状態として読み込まれるので、余計な通信を削減することになる。

form_forform_with に一括して変更した。おそらく今後は form_with が標準になるだろうし、既存コードのほとんどが remote: true なアクションだったため、form_with を使う方が合っていた。form_with の使い方の記事は以前書いたので興味がある場合は参照してほしい。

Rails 5.1 を Heroku にデプロイする方法も特殊だったので必要に応じて参照いただきたい。

そして最後は機能の全てを手動テストした。テストコードだけでは全ての動作を保証できないので、手動テストで実行して見つけたエラーをひたすら直すことで、晴れてリリースを迎えることができた。メソッドが動くけど結果が地味に挙動が変わったりすることもあったので注意したいところだ。最終的には以下のような diff となった。外部ライブラリを Yarn で入れるようにした分でだいぶ Rails プロジェクトがスッキリした。

$ git diff master --stat
367 files changed, 13907 insertions(+), 32253 deletions(-)

アップデート後のパフォーマンスなど

Rails 5.1 にあげたことで、運用面で問題にならないかが心配だった。メモリが急上昇してしまったり、特定の処理のレスポンスが極端に遅くなるなど、これらの項目は実際に本番環境に上げてみないとわかりづらい項目である。

今の所、Rails 4.2 のころとあまり変わらない感じで安定している。これに関して今後、Puma のスレッド数やワーカー数をチューニングしてどの値が最適化を調整していく予定だ。ワーカー数は2にした瞬間にメモリが 100% を超えてしまったので、 Rails x Puma x Heroku の場合は ワーカー数は1にするしかない気がする。現状、スレッド数8, ワーカー数1で運用している。

終わりに

なかなか思い切ったソースの修正をしたけど、ユーザーには全く変わっていないように見える。そうした改善は個人的には結構好きで、バージョンアップ以外にもちょくちょく続けている。ひとまず Rails5.1 の最新版にあげられたので、一安心だ。このための自動&手動テストはかなり慎重にやってきたので、リリースできたときは嬉しかった。

こうしてアップデートをすることで、また自分のソースコードに愛着が湧いて、今後もメンテナンスを続けたいと思えるようになる。近々 Rails 5.2 にもアップデートして、ActiveStorage を使ってみたいと思う。

もし Rails4.2 を使っている方がいて、この記事が少しでも参考になったなら幸いだ。

Rails アプリの国際化の実装と考察

ども、@kimihom です。2018年もどうぞよろしくお願いします。

さて、今回は Rails アプリを世界へ向けて展開したい人向けの記事を書いていこう。

基本的な参考資料

まずは Rails の I18Nドキュメント を読むことから始めよう。ここに基本的なことは全て書かれている。

Rails国際化 (I18n) API | Rails ガイド

I18N で立ちはだかるのは大きく2つの事項がある。言語と時差だ。以下にそれぞれ記す。

多言語化対応

まずは多言語化から見ていこう。基本的に Rails では config/locales の中に en.yml ja.yml などを定義していって、 View の中に t('home.title') みたいな感じで指定すれば勝手にそれぞれのロケールの言語で翻訳してくれる。てな訳で開発中の多言語化の際には上記 yml ファイルの中に記載していく習慣を持っておけば基本的には問題ないだろう。

ただし、JavaScript 側でアラートメッセージなどを出したいって場合には Rails 側で管理できないので工夫が必要だ。自前でやる上で一番簡単な方法は、View のなかに display: none な Key-Value のセットを保存しておくやり方がある。ただこの方法だと量が多くなると管理が相当めんどいので、量が多い場合は i18n-js などを検討しよう。

<div style="display: none;">
  <span id="error1"><%= t('error.msg1') %></span>
  <span id="error2"><%= t('error.msg2') %></span>
  <span id="error3"><%= t('error.msg3') %></span>
  ...
</div>

<script>
console.log($("#error1").text());
</script>

そして重要なのが、ユーザーごとに言語を切り替える設定の部分だ。デフォルトのロケールは config/application.rb で書く。以下の例では 何も指定がなければ、日本語になる。

    config.i18n.available_locales = ["en", "ja"]
    config.i18n.default_locale = :ja

ではどうやってユーザーごとに最適な言語を表示させるのか。それは先ほどの Rails 国際化のリンクに方法が書いてあるので、検討してみてほしい。地理やブラウザ言語などで分ける方法もあるけど、私としては URL で完全に分ける方法が最も良いと考えている。なぜなら 基本的に Google や SNS でシェアした URL に言語設定が入っていれば、それがアクセスする人にとっての最適な言語に間違いないからだ。

てな訳で、ルーティングの設定のなかに locale が入るようにセットする。

Rails.application.routes.draw do
  get '/:locale' => "home#index", as: :root
  get '/' => redirect("/ja")

  scope ":locale", locale: /ja|en/ do
    resources :contacts
    # ...
  end
end

URL でロケールを明示する方法の場合、Root URL だけ ja or en のどっちかがわからないのでリダイレクトさせる必要がある点に注意しよう。https://www.my-worldapp.com/en/contacts/1 のような URL 構成になる。

さて、この URL に params[:locale] が入るようになるので、あとはコントローラ側で表示の出し分けをすれば OK だ。

class ApplicationController < ActionController::Base
  before_action :set_locale

  private
  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
  end

  def default_url_options(options = {})
    { locale: I18n.locale }.merge options
  end
end

default_url_options を指定することで、link_to の時も勝手にロケールが入るみたい。ついでに ActionMailer 側も以下のように設定しておくと、メールに URL 含める場合でもロケールが考慮される。

class ApplicationMailer < ActionMailer::Base
  default from: 'support@my-worldapp.com'
  layout 'mailer'

  def default_url_options(options = {})
    { locale: I18n.locale }.merge ActionMailer::Base.default_url_options
  end
end

タイムゾーン

さてタイムゾーンの話に移っていこう。時差がある中でどうやってユーザーに最適な日時を表示させるかという問題である。

当然のことながら データベースでの日時は UTC で保存しよう。config/application.rbtime_zone の設定がなければ勝手に UTC になる。

Rails 側で勝手に作られる created_atupdated_at は基本的にサーバー側で UTC で保存されるようになるから問題になることはないだろう。あとはどうやってユーザーに時差付きのタイムゾーンを表示させるか、となる。

んで JavaScript には時差をとってくる方法があるので、JS 側でみんなやっちゃえばいいやんという考え方が1つある。しかし、その方法だと日時のパースのためにユーザーの表示を待たせてしまう必要があるし、そもそも日時を処理する必要があるため非効率的だ。何かしらの SPA フレームワークを使ってない場合はあまり良い方法ではない。

てことで今回は、ユーザーごとにタイムゾーン情報をデータベースに保存する方法を案内する。

1. User に time_zone を保存

stringtime_zone を用意しよう。そんでモデル側で以下のようなバリデーションを用意する。

validates :time_zone, inclusion: { in: ActiveSupport::TimeZone.all.map(&:name) }

これで、例えば Tokyo のようなタイムゾーンをユーザーごとに保存する土台が整う。

2. User ごとのタイムゾーン表示

そんで app/controllers/application_controller.rb で以下のような記述をすると、ログインしたユーザーごとに DateTime を生成してくれる!さすが Rails といったところだ。

class ApplicationController < ActionController::Base
  around_action :user_time_zone, if: :current_user

  private
  def user_time_zone(&block)
    Time.use_zone(current_user.time_zone, &block)
  end
end

3. User のタイムゾーンの選択

最後に、ユーザーがどうやって自分の最適なタイムゾーンを選択するかという話に移ろう。これが厄介だ。 一番簡単なのは、ユーザー登録の時点でタイムゾーンを選択してもらう方法である。 Rails の View では <%= f.time_zone_select :time_zone %> ってのが用意されているので、これを使えばセレクトボックスを簡単に用意できる。

f:id:cevid_cpp:20180106141647p:plain

タイムゾーンを更新する場合は上記方法でも良いかもしれないけど、登録の際にこのセレクトボックスを出させるのは気が引ける方は多いと思う。だって、JavaScript でタイムゾーンを取ってこれるからね。てことで、例えば会員登録の際にタイムゾーンは JavaScript で取ってきたタイムゾーンで登録させるっていうクールな方法を紹介する。

var min = -(new Date().getTimezoneOffset());
var hrs = min / 60;
((hrs > 0) ? "+" : "-") + ((Math.abs(hrs) < 10) ? "0" : "") + parseInt(Math.abs(hrs)) +  ":" + ((min % 60 > 0) ? "" : "0") + min % 60;
// => "+09:00"

なかなかトリッキーな方法だけど、上記のコードを書くことで実現した。なんと時差で "4:30" みたいな 30分ずれるみたいなこともあるらしく、ちょっと複雑なコードになってしまった。

残念ながら、JavaScript では上記のように時差(+9:00) のような文字しかとってくることができない。データベースで保存しているのは、Tokyo といった文字列である。てことで、先ほどの <%= f.time_zone_select :time_zone %> を活用してなんとか動かすように JavaScript 側で工夫しよう。最終的に以下のようなコードで実現した。

    var min = -(new Date().getTimezoneOffset());
    var hrs = min / 60;
    var timezone = ((hrs > 0) ? "+" : "-") + ((Math.abs(hrs) < 10) ? "0" : "") + parseInt(Math.abs(hrs)) +  ":" + ((min % 60 > 0) ? "" : "0") + min % 60;
    $("#time_zone option").each(function(i, elm) {
      if ($(elm).text().includes(timezone)) {
        $("#time_zone").val($(elm).val());
        return false;
      }
    });

time_zone_select に時差を表示してくれている部分があるので、最初にその時差とマッチするセレクトボックスがあったらそれを値として登録するようにした。この方法のデメリットとして、+9:00 の場合は Osaka がデフォルトになる という点だ。まぁ時間さえ一致すればあまり問題にはならないかなということで、この方法で今やってみている。

終わりに

今回は 世界的ウェブサービスを生むために必要な、I18N について記載した。

こんな記事を書いているということは、私自身そういう想いで作っている何かがあるということである。是非みなさんも世界へ向けたサービス開発にチャレンジしてみてほしい。それに必要なのは、根拠のない自信と実際にやるという行動力だ!

rails-ujs と form_with の使い方

ども、@kimihom です。

Rails 5.0 までは jquery-rails を使ってフォームやリンクの Ajax 通信を可能にしていたけど、Rails 5.1 からは rails-ujs として切り出され、晴れて jQuery からの脱却を可能にした。

そこで、本記事ではこの rails-ujs と関連深い form_with の使い方や注意点についてまとめる。

rails-ujs

rails-ujs は、Ajax の送受信の "送" の部分を JavaScript で実装せずに、よしなにやってくれるライブラリだ。私たちは Rails 5.1 から導入された form_with を使ってフォームを構築し、その form でデータを送った後のレスポンスのハンドリングだけを JavaScript で書けば良くなる。

rails-ujs を使わない場合は、フォーム送信ボタンを押した時のイベントをハンドリングし、各フォームの値を詰めて Ajax で送るみたいな実装が必要になる。それらの実装を全部 rails-ujs と form_with が肩代わりしてくれると考えるといいだろう。実際は form_with だけでなく、View の button_to や link_to などに remote: true をつけるだけで Ajax 化することもできる。

rails-ujs の魅力てのは、やはり Rails のレールに乗ることができることだ。お分かりの通り、フォームの実装はフロントエンドの鬼門だ。各種項目のバリデーションや 各項目のI18n、デフォルト値の設定、値の送信など必要なことがたくさんある。これら実装を React や Angular で実装してもいいだろうけど、それらを使う場合には先の実装を全部 フロントエンドフレームワーク側に寄せる必要が出てきてしまう。しかし、実際には Rails 側のデータベースのモデルと関連づけることで、フォームの実装はより手軽に、より密で効果的に実装が可能である。

rails-ujs のインストール

さて、そんな素敵な rails-ujs を早速インストールしよう。Rails 5.1 ではデフォルトで rails-ujs が入っているので、require するだけだ。

$ cat app/assets/javascripts/application.js

//= require rails-ujs

これで読み込みが完了する。

form_with

Rails 5.1 から導入された form_with であるが、今までの form_for と同じように考えると手痛い思いをすることがあるので気をつけよう。

form_with のデフォルトは remote: true の状態である

form_with の場合、何もオプションを指定しなくても Ajax 送信となる。そのため、form_with でフォームを作って submit した時に何も動作しない?ってなることだろう。一般的なページ遷移するフォーム送信をしたい場合には local: true を明示する必要がある。

form_with で構築した form や text_field には id などが付与されない

これは大きな変化なので注意が必要だ。例えば以前のバージョンで form_for を使って構築していた場合、勝手に form や text_field にはモデルの属性名などに応じて id が割り振られていた。この id をベースに JavaScript 側で処理を実装していた方もいることだろう(私もそうだった)。

しかし、form_with で構築した form や text_field には id や class などが自動で生成されない。そのため、明示的に id: "name" のように割り当てる必要がある。てことで、各フォーム項目に明示的に id や class を割り当てたあと、JavaScript 側で処理しているコードがある場合は修正する必要がある。

rails-ujs の使い方

さて、ここからが rails-ujs の出番だ。フォームを送った後のイベントを定義してあげよう。そのためには、送り元の <form><a> タグから発火されるイベント ajax:success を定義してあげれば良い。

Controller と View, そして JavaScript の関連を知る必要があるので、以下にまとめて紹介する。本来なら jQuery 使う必要がないけども、今までの差分って意味で jQuery で実装した場合でコードを記す。

class UsersController < ApplicationController

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_param)
    if @user.save
      render json: {result: "ok", user: @user}
    else
      render json: {result: "ng", msg: @user.errors.messages}
    end
  end

  private
  def user_param
    params.require(:user).permit(:name)
  end
end
<%= form_with model: @user, url: users_path, id: "user-create-form" do |f| %>
  <%= f.text_field :name %>
  <%= f.submit %>
<% end %>
$(document).on("ajax:success", "#user-create-form", function(e) {
  console.log(e.detail[0]);
});
$(document).on("ajax:error", "#user-create-form", function(e) {
  console.log(e.detail[2]);
});

JavaScript 側で Ajax を送る必要がなく、終わった後の UI の変更だけをすればいいのでフロント側で書かなければならないコードが減っているのがおわかりいただけたことだろう。 rails-ujs からの変更点として何よりも大事なのが、ajax:success 時の処理だ。コールバック関数の引数は1つにまとめられ、Ajax 送信した後のレスポンスは、e.detail[0] に格納されている。この保存場所はちょっとわかりづらい気もするけど、rails-ujs ではそういう実装になっているので従うしかないだろう。ajax:error 時には detail[2] に status や statusText が格納されている。ここら辺は console.log で出力しながら確認するといい。

ちなみに、フォーム送信前に何かしらチェック処理を挟んでエラーの場合は送信キャンセルをしたいこともあるだろう。その場合は、ajax:beforeSend ではなく、ajax:before 内で return false; をしてあげると実装できる。ajax:beforeSend もあるんだけど、送る直前に呼ばれるコールバックなので return false しても Ajax 通信が走ってしまう。

JavaScript コードから form を明示的に送信したい場合

今までの話は、ユーザーが submit を HTML 上でクリックなり Enter してくれた場合に動くというシンプルなケースだった。しかし、実際には JavaScript 側でごにょごにょしてから JavaScript 側で form 送信をしたいことが出てくるはずだ。

その時に、何も考えずに JavaScript 側から submit() を呼ぶと、Ajax 送信ではなく普通のフォーム送信となってしまう 問題を見つけた。ページ遷移してしまうのである。これに対して JavaScript 側からの submit() を Ajax で送信したい場合にどうすればいいかググったが全く出てこないので、仕方なくソースコードを読んだ。

rails-ujs には Rails.fire というメソッドが提供されており、ここで submit イベントを発火させることで対象フォームを JavaScript からリモート送信することが可能になるようだ。

Rails.fire($("#user-create-form")[0], "submit");

rails_ujs の考え方

今回の例は 純粋にレスポンスを json で返して JavaScript 側でその後の UI 処理を任せる方法だったけど、もっと簡単な方法として create メソッドのレスポンスを json ではなく html.erb を返してあげて、JavaScript 側で一括でレスポンスの HTML へ書き換える方法がある。この方法はかなりおすすめで、ちゃんとやれば JavaScript 側で書かなければならないコードを激減させることができる。

Ajax 終わったあとちょっとしたメッセージ出したい程度だったら JSON、がっつり HTML を書き換えたい場合には HTML を返すって思っておくと、今後の実装が楽になることだろう。

HTML を Ajax のレスポンスとして返した場合は、e.detail[2].response に HTML が入っている。

終わりに

Rails 5.1 から導入された form_with だが、以前の form_for と同じ感じで使うとつまづく点が出てくるので気をつけよう。私が遭遇した問題は上記で全てだが、他に問題が起きた際には yarn から落としてきた node_modules/rails-ujs/lib/assets/compiled/rails-ujs.js のソースを読むことになるだろう。そこまでコード量は多くないので、ググって詰まるより実際にソースを読むことをお勧めする。

そんなわけで快適なレールに乗った Rails プログラミングをこれからも楽しんでいこう!

SaaS における企業毎のデータ管理について

ども、@kimihom です。

SaaS において悩ましいのが、企業毎のデータをどうやって管理していくかという話がある。企業毎にデータは完全に独立しているので、URL やデータそのものも独立している方が望ましいとされる。では SaaS においてデータベースの設計をどのようにやっていくといいのかについて、1つの方法を掲示したいと思う。

データを共通で扱う

私がオススメしたい方法はデータを共通で扱うやり方だ。これにより、Rails から個別のテーブル名にアクセスするといった複雑なテーブル接続からの悩みから解放される。そしてコードとしては至ってシンプルに実現できる点が利点である。

f:id:cevid_cpp:20171210183447p:plain

それ以外のデータも全て、Contracts に紐付けた 1 対多 の関係にすることが大前提だ。そうすれば、案外データを個別に取得することは Rails で簡単に実現できる。例えば、1つの Contract に対して複数の Invoice を取得するには以下のようなコードで実現できる。

class InvoicesController < ApplicationController
  before_action :authenticate_user!

  def index
    @contract = current_user.contract
    @invoices = @contract.invoices
  end

end

大事なのは、@contract.invoices といったようにして Contract に紐づいた Invoices しか取って来ないようにすること。この制約を入れるだけで同じテーブルだけど契約毎に限定されたレコードを取ってくることができる。とてもシンプルでメンテナンスしやすいコードである。データ取得する全てのコードにおいて、この制約を取り入れる必要がある。以下の例を見ていただきたい。

class InvoicesController < ApplicationController
  before_action :authenticate_user!

  def show
    # NG! 全く関係ないログインユーザーが、他の契約の Invoice を取って来れてしまう
    @invoice = Invoice.find(params[:id])
    # OK. 他契約の invoice id を指定したとしても Not Found となる
    @contract = current_user.contract
    @invoice =@contract.invoices.find(params[:id])
  end

end

慣れてくれば Invoice とそのまま呼び出すことに違和感を感じるようになる。そうすれば、間違っても NG なコードは書かなくなる。

他のデータ管理も共通の方法で管理する

例えば検索には Elasticsearch を使っている場合にも、同様の対応で実現が可能だ。Elasticsearch の場合、constat_score を使って contract_id を絞り込むことができる。

query = {
  :bool => {
    :must => [
     {
        :constant_score => {
          :filter => {
            :term => {
              :contract_id => 1
            }
          }
        }
     },
     {
        :query_string => {
          :query => "aaa"
        }
      }
    ]
  }
}

Contact.__elasticsearch__.search(query)

そうすれば Elasticsearch 側にも複数インデックスを作るみたいな大変なことをする必要がなくなり、コードがたいへんシンプルになる。

考察

別テーブルや別データベース、別サーバーといった形で契約毎に分けるのと、上記のような共有テーブルの方法のメリットデメリットについて考えてみたい。

1つのテーブルで共有する場合に真っ先に思いつくデメリットは、1つの企業が例えば超大量データを保有した時に、他の企業にもパフォーマンスで影響が出てしまうという点だ。そのため、例えば基本的に契約したらデータは無制限に保存できるみたいなサービスだと、この心配事が常につきまとうだろう。例え大きめの契約企業がいたとしても、データ保存容量を平等にすることで、心の平穏を保たなければならない。 でも、この件に関してはあまり心配する必要がないと思う。なぜならそもそも何億データも保存して共通データをやりとりしている BtoC のサービスが既に存在するからだ。SaaS でも今後データが増えてくればテーブルの再設計やテーブル分割など必要になってくるかもしれないけど、そこまで悩むレベルになってきたらサービスは成功したということなのだ。データベースをより良いものにアップグレードするなり専門のデータベーススペシャリストを雇うなりできるだろう。

共有テーブルの他のデメリットとして意図しないデータの共有が発生してしまうリスクもある。1人で開発している分には先ほどの NG なコードは書かないだろうけど、誰かがコードを書いている時に 一般的なモデル#find をやらかすケースは当然起こり得るだろう。そもそもそんなコードを書かないような仕組みや意識付けが必要となってくる。

そして最後に、上記のテーブル設計は 1ユーザーに1つの契約しか所属することができない。それを許容するのなら良いに越したことはないけど、例えばログインした時に複数の契約にスイッチできるようなことを考え出すと、このテーブル設計は破綻する。Contract-User の設計を多対多にして URL などで契約を識別できるようにする必要があるだろう。

実際運営している私からの意見として、デメリットというか意識しなければならないのはその点くらいで、他で問題になったことは今まで一度もない。今後、何かしらで共有テーブルで問題が出てくるかもしれないけど、別テーブルや別データベースで設計した時よりは深刻な問題にはならないだろう。"早すぎる最適化は諸悪の根幹" を忘れずにやっていきたいところである。

終わりに

今回は SaaS における企業毎のデータ管理について 1つの方法を掲示した。この件に関して難しく考えるよりも、まずは顧客が欲しいと思ってくれる機能を開発することに集中して欲しいと思う。そしてデータが多くなってきて管理に悩むくらいになったら、次のステップとして最適化について悩んでいけば良いのだ。

てな訳で難しそうと思わずにまずは作ってみてみよう!

Rails 5.1 アプリを Heroku にデプロイする

ども、@kimihom です。 この記事は Heroku Advent Calendar 8日目の記事です。まだ3枠空きがありますので、Heroku ユーザーの方はぜひご登録を!

f:id:cevid_cpp:20170929233715p:plain

Rails 5.1 から Yarn のサポートが入り、フロントエンドの JavaScript ライブラリの管理が容易になった。そこで Rails 5.1 で作った破壊的イノベーションを生むアプリを意気揚々と Heroku へデプロイしようとしたところ、色々詰まったので残しておく。

遭遇した問題と対応

Rails 5.1 の Yarn サポートにより、yarn install を実行しないといけなくなった。 しかし、 Heroku の Ruby ビルドパックに yarn install のコマンド実行が入っていないので、git push した時点で JavaScript パッケージをインストールしてくれない問題が発生した。結果、JavaSciript を読み込むことができず Heroku 側で JavaScript エラーが発生してしまった。

こういう時こそ Release Phase の出番かと思ってやってみたけど、うまくいかず。

色々と調べていると、Node.js のビルドパックを追加する方法があった。具体的には

heroku buildpacks:add --index 1 heroku/nodejs

とすることで、 heroku/ruby の前に nodejs のビルドパックを埋め込んでくれるようだ。ビルドパックを複数指定して、git push heroku master 後にその順番でビルドしてくれる。そうすれば、Node.js のビルドパックには yarn.lock を読み込んで node_modules を生成してくれるので、デプロイがうまくいく。実際に以下のコマンドを叩いてみると、

$ heroku buildpacks
=== my-awesome-app Buildpack URLs
1. heroku/nodejs
2. heroku/ruby

といった感じで複数のビルドパックが入っていることが確認できる。この状態で、git push heroku master をやってやると、Rails ルートにある yarn.lock を読み込んで勝手にライブラリをインストールし始めてくれる。

$ git push heroku master

Counting objects: 7, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (7/7), done.
Writing objects: 100% (7/7), 676 bytes | 0 bytes/s, done.
Total 7 (delta 6), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Node.js app detected
remote:
remote: -----> Creating runtime environment
remote:
remote:        NPM_CONFIG_LOGLEVEL=error
remote:        NPM_CONFIG_PRODUCTION=true
remote:        NODE_VERBOSE=false
remote:        NODE_ENV=production
remote:        NODE_MODULES_CACHE=true
remote:
remote: -----> Installing binaries
remote:        engines.node (package.json):  unspecified
remote:        engines.npm (package.json):   unspecified (use default)
remote:        engines.yarn (package.json):  unspecified (use default)
remote:
remote:        Resolving node version 8.x...
remote:        Downloading and installing node 8.9.1...
remote:        Using default npm version: 5.5.1
remote:        Resolving yarn version 1.x...
remote:        Downloading and installing yarn (1.3.2)...
remote:        Installed yarn 1.3.2
remote:
remote: -----> Restoring cache
remote:        Loading 2 from cacheDirectories (default):
remote:        - node_modules
remote:        - bower_components (not cached - skipping)
remote:
remote: -----> Building dependencies
remote:        Installing node modules (yarn.lock)
remote:        yarn install v1.3.2
remote:        [1/4] Resolving packages...
remote:        success Already up-to-date.
remote:        Done in 0.12s.
remote:
remote: -----> Caching build
remote:        Clearing previous node cache
remote:        Saving 2 cacheDirectories (default):
remote:        - node_modules
remote:        - bower_components (nothing to cache)
remote:
remote: -----> Build succeeded!
remote: -----> Ruby app detected
remote: -----> Compiling Ruby/Rails
remote: -----> Using Ruby version: ruby-2.4.2
remote: -----> Installing dependencies using bundler 1.15.2
remote:        Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin -j4 --deployment
remote:        Warning: the running version of Bundler (1.15.2) is older than the version that created the lockfile (1.16.0). We suggest you upgrade to the latest version of Bundler by running `gem install bundler`.
remote:        Fetching gem metadata from https://rubygems.org/...........
remote:        Fetching version metadata from https://rubygems.org/..
remote:        Fetching dependency metadata from https://rubygems.org/.
remote:        Using rake 12.2.1
remote:        Using concurrent-ruby 1.0.5

... 以下 Ruby のビルドパックの実行

その他の方法

どうやら Webpacker を入れておけば、勝手に Yarn のインストールが走る? そうだ。参照元がどこだか忘れたので、もしかしたら間違ってるかも。

ただ自分は AssetPipeline で普通に Rails のフロントエンドを扱いたかったのでそのために Webpacker の形式に最適化することはしたくなかったので、やっていない。

(余談) Webpacker を入れると、以下のコマンドを実行するわけだけども、

bundle exec rails webpacker:install

てすると

app/javascript:
  ├── packs:
  │   # only webpack entry files here
  │   └── application.js
  └── src:
  │   └── application.css
  └── images:
      └── logo.svg

っていうディレクトリ構成になる。なんかもうこの構成の時点で使いたくなくなるよね。なぜ javascript の中に csssvg が入るのか?そして layouts に <%= javascript_pack_tag 'application' %> という 記載しなければならないもクールじゃないので使用するのを止めた。今後のアップデートに期待、かな。

終わりに

現在、heroku.yml という機能が Developer Preview で公開されていて、今後は heroku.yml でこの問題が対応できるようになりそうだ。これも注目していきたい。

まだまだ Rails のフロントエンドの開拓は進んでる最中なので、色々と躓くところがあるかとは思う。でもYarn をちゃんと使って JavaScript ライブラリの管理ができれば、それはそれで良いに越したことはない。

Heroku は Ruby の最新版をリリースした数時間後に最新版 Ruby のサポートを発表したり、最新の Rails アプリケーションもどんどんサポートしてくれるので、新しい何かを試すのにいいプラットフォームだと思う。

Rails 5.1 以降のフロントエンド周りの発展にも期待していきたい。

Rails 5.1 の First Impression

ども、@kimihom です。

f:id:cevid_cpp:20171105213739j:plain

この連休はひたすらプログラミングやってた。そんな中で手をつけ始めた Rails 5.1 について感想を書いてみるとする。こういうちょっとしたことでも記事にできるのがブログのいいところよね。

Yarn マジ便利

JavaScript の パッケージ管理の Yarn は本当に便利だと感じた。今までの Rails の AssetPipeline での外部ライブラリの管理って、専用の Gem をインストールして application.js に書く感じだった。例えば momentjs-rails 。これの大きな弱点ってのは、Gem の管理者が更新をやめれば バージョンは古いままだってこと。そもそもフロントエンドの JavaScript の管理も Gem でやろうって時点でモヤモヤ感を感じてはいた。

そこで Yarn の登場だ。Rails プロジェクトのトップで yarn add moment ってするだけで moment をインストールすることができる。すると Rails 直下にお馴染みの node_modules フォルダができるので、それを見ながら application.js を以下のように追加するだけで OKだ。

//= require moment/moment

これは Yarn を入れた後の JavaScript ライブラリの構成によって、 require 以降の書き方は変わるので注意する必要がある。例えば JavaScript ライブラリ hoge の実体ファイルが dist/hoge.js だったら //= require dist/hoge って感じにすれば OK だ。

これで例えば moment をアップデートしたいってなら yarn upgrade moment で終わりだ。最高にクール!

フロントエンドの選択

以下の記事を読んで、自分が Rails プロジェクトの中でフロントエンドをどうしていきたいかを考える必要がある。

blog.toshimaru.net

私は現状の Asset Pipeline に大満足している派だったので Asset Pipeline を使い続けることにした。この記事では ES6 使いたいなら browserify-rails を使うやり方が紹介されていたけど、普通に Asset Pipeline 使ってでも ES6 使えるような気がするため、メリットがいまいちピンとこなかった。唯一 require が使える? 的な話を他のページで見かけたけど、結局 Asset Pipeline で一発でまとめてくれるから require 自体必要ないよな、とか思ったりした。ここら辺詳しい方いたら教えて欲しい。

-- 2017/11/19 追記 -- rails server で普通に開発してる分には問題起きないけど、assets:precompile で ES6 形式で書くと失敗することが判明。。やはり Asset Pipeline では ES6 構文を書くのに対応させることは難しいようだ。

今までは AssetPipeline のコンパイルとかで ES6 の書き方をするとエラーになってたりしたけど、それがスラスラ書けるようになったのは嬉しい限りだ。

以下の本は是非読んでおくといいかと思う。このために Kindle の Mac アプリをインストールして読んだ甲斐があった。この値段でこのコンテンツはありがたすぎる。

let, class, Arrow Function.. こうした新しい JavaScript コーディングによって、 Rails のフロントエンドの書き方も大きく変化する。んで意気揚々とアロー関数を定義しまくってたら、this の挙動で悩まされることになるので、これも勉強しておいたほうがいいかもしれない。こんなん、初めて書いたら絶対ハマるやろっていう感想しかなかった。特に メソッド Callbasck 内での this の挙動が怪しい。。ここら辺、いい書き方を今後模索していきたい。

【JavaScript】アロー関数式を学ぶついでにthisも復習する話 - Qiita

おまけ: 地味に使えなくなってる

render text: "ok" 的な書き方ができなくなってたw render plain: "ok" って書くのが適切なようだ。まぁそもそも render json とかにしろって話かもしれないが。

終わりに

Rails 5.1 といえばフロントエンドの大きな変化なので、そこに焦点が移った。ひとまず Yarn の導入と ES6 形式で Asset Pipeline が書けるってのが個人的には嬉しい点だった。

Rails 5.1 のレールにも乗っていけそうで、一安心した週末となった。

Heroku Addon 「Scout」 の紹介

ども、@kimihom です。

Heroku Addon の1つである Scout はRuby on Rails 専用のメトリクスサービスだ。一般的にこの類の Addon だと NewRelic が一般的だけども、重いしメモリも食うので Scout を使っている。今回はそんな Scout について簡単にご紹介しようと思う。

概要

Heroku は Heroku 自体が Metrics を提供しており、ここでサーバー全体でどんなパフォーマンスを発揮しているのかを見ることができる。しかしあくまで概要しかわからないので、具体的にどの URL のアクションが問題を起こしているのかまでは教えてくれない。そのため Scout や NewRelic のような、より詳細を見ることのできるメトリクスの Addon が存在する。

以降は Scout のスクショ見せながらの方が早いと思うので、色々と貼りながらアプリのチューニングについて解説していく。

1, 概要チャート

まずは直近でのレスポンスタイムや、メモリの上昇具合などをグラフで表示してくれるチャートがある。チャートとして表示する項目自体は Heroku の Metrics と何ら変わらないのだけど、Scout のすごいのは日時で限定して、その詳細を表示してくれるところにある。例えば上図のようにレスポンス速度が遅かった時間帯にフォーカスすると、具体的にどのアクションが最も遅かったのかを一発で表示してくれる。

特定の1アクションが裏でめちゃめちゃ重い処理をしていて、ユーザーがたまにしかそこにアクセスしないような場合を考えてみよう。この場合、そのアクションを実行された瞬間に一気にレスポンス速度が遅くなるし、メモリが上昇することもあるだろう。そんな時は上記の図のような感じで期間を指定しながら最も遅かったリクエストを調査していくことが可能になる。ちなみに "Slowest Resopnse Time" が現在選択されているが、その他に Largest Memory Increase がある。

2, Memory Bloat Insights

特定の1アクションが原因で、サーバー内のメモリが急上昇するのが Memory Bloat と呼ばれる現象だ。Scout ではそれを一発で見つけてくれる。私の経験則としては、上図のように 単一リクエストで 10M くらいだったらあまり気にする必要はないと思う。全てのリクエストで Memory Bloat が 50M 以上だったりする場合は、それは明らかに直すべきという感じで見るのがいいと思う。

じゃあ具体的にどんな原因で Memory が増えるのかってのは、以下のようなケースがある。

  • 重い DB クエリ
  • 1度に大量のクエリを発行
  • ファイルアップロード
  • blob など大容量のデータを読み込んでしまう

それぞれに最適化の方法があるので、もし原因がわかった場合には調べて対応していこう。

3, n+1 Insights

Rails において N+1 は深刻なコードミスだ。簡単にいうと、毎回 SQL のクエリを投げるコードを each の中に書いちゃった場合に発生する。

@artilces = Article.all

<% @articles.each do |article| %>
  name: <%= article.user.name %>
<% end %>

残念ながら、このコードを書いた時点で N+1 が発生してしまう。aritcle.user の時点で Article に紐づく User を View のループの中で SQL で探しに行かないといけないからだ。具体的には @artilces = Article.includes(:user).all とするのが正解だ。これでわざわざ View の中で 複数の SQL を発行しに行かなくなる。

てな対策をしろってのを Scout が教えてくれる。本来ならローカル環境で Bullet Gem を入れたりして見つけるべきことなんだけど、本番データとしてちゃんとある場合にしか見つけられないケースとかたまにあったりするので便利だ。

各アクションの分析

どのアクションが重いかわかったら、具体的にその中のどの部分が遅いのかを見ていこう。アクションの詳細にいけば、それが全てわかる。

今回はわかりやすいようにわざと外部への HTTP リクエストを送るコントローラーを例にしてみた。上記のように、このリクエストの大部分は HTTP が原因であることがわかる。これが例えば DB クエリが遅いってなら SQL のチューニングなどをしたり、コントローラー自体が重いってならコントローラーのロジックを再検討する必要があるということがわかるというわけだ。

終わりに

こんな感じで遅いアクションを見つけて、その原因を1つ1つ取り除いていくっていう地味だけど大事な作業が安定したサービス運営につながっていく。Scout はこの他にも DB クエリのメトリクス、Github 連携、アラート通知機能などを提供している。無料でもそれなりに原因を見つけられたりするので、まずは試しに入れてもらえたら幸いだ。課金はそれなりにサービスが運営できるようになったらでいいと思う。

こういうのって実際にそれなりの Web サービスを運営していないとわからないことなので、当てた Web サービスを作った後にはぜひ知っておいていただきたいと思う。

"このサービス重すぎて使えない" ってなった時にはもう遅いので、その悲劇が起きる前に一つずつ解決していこう。

Rails 5.1 のフロントエンド周りの所感

ども、@kimihom です。

常に話題に上がってくる Rails のフロントエンド事情だけども、今回 Rails5.1 を色々みた中で自分が感じたことについて書いていく。予め断っておくと、自分もまだそこまでフロントエンドをマスターしている身ではないので間違った考え方の部分もあるかと思う。その場合はぜひコメントなどいただけると幸いだ。

Rails Guide は今でも rails-ujs

jquery-ujs が rails-ujs に変わったなどで、脱 jQuery を果たした Rails5.1。この時点で他のフロントエンドフレームワークに移ることを検討した方も多いかもしれない。

そもそも、jquery-ujs って Rails において結構大事な役割を果たしていると思うので軽く説明すると、要は <%= form_for @article, remote: true %> なフォームで Submit した時に、 Ajax でリクエストを送って対象 Form のイベントに ajax:success などの形式でコールバックが呼ばれる仕組みだ。これが今までは jQuery 依存だったけど、その依存が取っ払われた感じになる。Rails 5.1 では form_with ってのが登場した。

この rails-ujs についてよくよく考えてみると、その他のフロントエンドフレームワークを使おうと思った時点で、rails-ujs のような仕組みは不要になっちゃう気がする。なぜなら、その他のフロントエンドフレームワークでも Form を作る機能を兼ねそろえていて、そっちを使っていくことになるはずだからだ。特にモデルを フロントエンド側で管理しているなら、完全に form_with などは機能が重複してしまうので rails-ujs 自体要らない子になる訳だ。

でも Rails Guide には今でも rails-ujs を使ったサンプルがあるし、残り続けている。それはつまり モデルのデータバインディングの仕組みのない何か を使うことが今でもベースになっているように思う。これはとても理にかなっている。なぜならサーバー側でフォームを生成すれば、フォーム生成は form_with を使って生成できて、I18n は Rails 側で管理された言語ファイルから読み出すことができ、バリデーションやエラーメッセージの表示など、全て Rails の ActionView 側に一任することができるからである。Rails を使っているのに、これらの処理をフロントエンドフレームワーク側でやるってのは機能の使い方として重複しており、それやるんだったら ActionView 自体の機能をごっそり削ぎ落とした RailsAPI 使ったほうがよくね、となるのは当然のことだ。だから Rails API が Rails5.1 から標準で使えるようになったのだろう。

link_toform_withremote: true な Ajax 通信を行い、フロントエンドではレスポンス時の UI 更新だけをする。こうすればフロントエンド側の責務はシンプルになる。Gmail や Facebook 並みのガリガリのリッチな Web アプリケーション作りたいってなら話は別だけど、たいていの Web アプリケーションは "検索して詳細見てフォーム投稿" といったオーソドックスな仕組みだろう。それをわざわざフロントエンドフレームワークに適用するのは、やはり Too Much だと考える。ちょっとしたページ遷移を素早くしたいってなら Turbolinks を使えばうまく History API をラップしてくれるし、Rails はあくまでもそうした用途向きに作られているのだと以下の動画で教えてくれる。

RailsConf 2016 - Turbolinks 5: I Can’t Believe It’s Not Native! by Sam Stephenson - YouTube

f:id:cevid_cpp:20171027225345p:plain

そう、あなたは Google でもないし Facebook でもないのだ。 個人的には Turbolinks は使わないで History API を自前で作ってるのだけど、基本的な Rails のフロントエンドの考え方について上記の Rails Conf での動画は参考になる。

で、お前は Rails 5.1 のフロントエンドでどうするの

まだ模索中である。少なくともバインディングがあるような Too Much な機能は必要とせず、素の JavaScript が簡素化されたくらいのものでちょうどいいと考えている。素の JavaScript で書くのが面倒だったら、もしかしたら jQuery 3.0 の採用もありうる。

そんで ActionView をちゃんと使って form_with でフォームを作成する。Ajax 通信時に断片的な HTML もしくは JS を返してあげて、jQuery でいう $().html()$().append() などで動的に HTML を変えていくシンプルな実装にしたい。部分的なところだけをアップデートするみたいなことをするのは正直複雑になるだけだと思ってて、だったらページごと body.html() で変えたり、大枠の div だけをごっそりと変えるような Turbolinks 的な方法で問題ないように思う。こうすることで、ちゃんと Rails のレールに乗って開発効率を高めて無駄な時間を省き、良いプロダクトを作ることに専念できるはずだ。

もちろん Gmail 並みのガリガリのプロダクトを自分が作るってなら話は別だ。でも、そもそもそういうのを作るんだったら、Rails 自体を採用するのがナンセンス だと私は思う。

終わりに

Rails 5.1 フロントエンド周りの所感についてまとめてみた。当然それぞれ考え方はあるだろうから、1つの考えくらいな感じで参考にしていただければと思う。

少なくとも私は Rails の提唱する実装方法に素直に従って開発し、Rails と共に 良いサービスを作ることに専念していきたい。そういう考えのもとで得た答えが、上記のような開発スタイルである。

今後 Rails 5.1 でちょいちょい開発していく予定なので、何か新しいことがわかったら記事にしていく予定だ。

Elasticsearch と Rails のデータ同期方法

ども、@kimihom です。

全文検索の仕組みとして Elasticsearch を使ってサービスを運用している。Elasticsearch と Rails を使っている上で考慮しなきゃいけないデータ同期の方法について、それぞれのメリット/デメリットを紹介した上で最終的な提案まで記す。

本記事は Elasticsearch-Rails を利用した場合となるので、基本的な使用方法などはリンク先を参照いただければ幸いだ。

GitHub - elastic/elasticsearch-rails: Elasticsearch integrations for ActiveModel/Record and Ruby on Rails

では ActiveRecord と Elasticsearch でのデータ同期方法についていくつかご紹介しよう。

方法1. ActiveRecordコールバックによる同期

まず一番簡単な elasticsearch-model を使った ActiveRecord コールバックの方法だ。これは、ActiveRecord オブジェクトで 作成/更新/削除 されたタイミングで Elasticsearch のデータもそれぞれ更新する方法である。

class Article < ActiveRecord::Base
  include Elasticsearch::Model

  after_commit on: [:create] do
    __elasticsearch__.index_document
  end

  after_commit on: [:update] do
    __elasticsearch__.update_document
  end

  after_commit on: [:destroy] do
    __elasticsearch__.delete_document
  end
end

ほとんど 登録/更新処理のないサービスの場合だったら、この方法で十分だろう。まずは ActiveRecord コールバックで問題ないか試しにやってみるといいと思う。

この方法のデメリットは2つある。1つは ActiveRecord オブジェクトの作成/更新のたびに Elasticsearch との接続が走ってしまって1つ1つのリクエストが重くなる点だ。当然 Rails サーバー側と Elasticsearch のサーバーは分けることになると思うので、その間のネットワーク接続で処理を待たされることになる。 2つめは ActiveRecord 限定で使っていかないといけない点だ。例えば裏側で PostgreSQL を使っていて、生の SQL でレコードをアップデートしても Elasticsearch 側に反映させることができない。あくまで #update#create などの ActiveRecord のメソッドを呼ばない限り Elasticsearch に登録されない。これは運用時に結構ネックになることがあるので気をつけたいところだ。

方法2. バッチ処理で一括取り込みを行う

もう一つの方法は elasticsearch-rails を使って一括取り込みを行う方法だ。

これであれば、前者のパフォーマンス問題と、生SQL で同期できない問題を解決できる。具体的には、以下の rake タスクを定期的に実行するような形となる。

bundle exec rake environment elasticsearch:import:all

これは、include Elasticsearch::Model が読み込まれた ActiveRecord モデルを一括で読み込み、その全データを Elasticsearch に投入することを意味する。全データなので、これが1万件くらいあると、当然無駄に時間がかかってしまう。 そして時間がかかるということで 1時間に1回の実行にしてしまうと、その間に登録されたデータは検索対象に入らなくなってしまう。この問題をなんとか解決したいところだ。

方法3. コールバックと バッチ処理の混合技

最終的なオススメは、上記2つを混合させる方法である。方法2で紹介した rake タスクは、対象の scope を指定することが可能だ。

bundle exec rake environment elasticsearch:import:all SCOPE='recent'

ActiveRecord 側で今回適当に用意した recent スコープを定義してあげる。

class Article < ActiveRecord::Base
  include Elasticsearch::Model

  scope :recent, -> {where("updated_at > ?", 1.hour.ago}

end

これで更新日時が 1時間以内のデータを一括で取ってきて、それだけを Elasticsearch に登録することが可能だ。しかし、この方法をしてしまうと、データ削除に対応できない という新たな問題が発生する。方法2の一括読み込みの方法では削除されてもデータ同期ができたが、今回は差分だけなので削除したレコードを取ってこれないのである。

てことで、削除の時だけ 方法1のコールバックを活用しよう。今回の結論としては以下のような形となる。

class Article < ActiveRecord::Base
  include Elasticsearch::Model

  scope :recent, -> {where("updated_at > ?", 1.hour.ago}

  after_commit on: [:destroy] do
    __elasticsearch__.delete_document
  end
end

そして以下のコマンドを10分に1回などの Cron で回すようにしよう。

bundle exec rake environment elasticsearch:import:all SCOPE='recent'

1時間前の更新日時にしたのは、その間で Elasticsearch の同期に失敗しても、10分に1回なら6回のチャンスを与えるためにそうした。 import 自体は重複しても上書きするだけなので、確実にデータ同期する上でもこの方法で良いと思う。そしてこの方法なら、PostgreSQL の生 SQL でレコードをアップデートしたとしても、updated_at を正しく更新さえずれば Elasticsearch のデータ同期対象となる点も魅力的だ。 ※外部SQLでの削除 or ActiveRecord#delete(_all) 等は同期無理なのでやらないでね。

終わりに

今回は Elasticsearch Rails を使った活用方法をご案内した。

最終的には方法3をオススメしたけども、これはデータがリアルタイムに反映されるってわけでもないので、これでも完璧とは言えないとは思う。リアルタイムが求められるサービスなら、方法1 の同期処理を ActiveJob でバックグラウンド実行すること(方法4)を検討する価値もあるだろう。

本記事が Elasticsearch と Rails を使っている方に参考になれば幸いだ。

Heroku Pipeline にステージング環境を乗せてみた

ども、@kimihom です。

今回はステージング環境と本番環境で Heroku アプリを分けていたものを、 Heroku Pipeline でまとめたのでその手順についてご紹介する。

今まで Heroku Pipeline を勘違いしていたんだけど、ステージング環境の Heroku アプリは Heroku アプリの設定を保ったまま残る。つまり、アドオンや、アドオン内で保存されたデータなどはそのまま保たれるということを意味する。ステージング環境と本番環境の切り替えだけの場合は特に大きな変更をすることなく Pipeline へ移行できた。

RAILS_ENV での環境切り替えは非推奨

Rails アプリは、 RAILS_ENV の環境変数によって、読み込みに行く config/environments/~.rb を変えてくれる。これをうまく利用して、Heroku の RAILS_ENV を staging にすると、config/environments/staging.rb を読み込んだ状態でアプリを起動してくれる。また、Rails.env.staging? といったメソッドを利用できるため、コード内で ステージング環境と本番環境を分けたコードを書くことができる。

ただし、この方法は Heroku では非推奨とされる。このようなアプリを書いて Heroku にデプロイすると、以下のような警告が表示される。

Heroku アプリは全てproduction環境であるべきとされ、アプリごとの差分は環境変数で管理しなければならない。デプロイするたびにこの警告を見るのは気分がよろしくないから、思い切って同じ Production 環境の Heroku アプリとして Heroku Pipeline に移しつつ、警告が出る問題をクリアしようってわけ。

既存本番アプリに Pipeline を作成

さて、Heroku Pipeline を使ってみよう。Heroku Pipeline を使えば修正内容を ステージング環境でチェックしたら 本番へデプロイするみたいな流れを Heroku と Github のプルリクエストで管理できるようになる。それぞれの Heroku アプリは継続的デリバリーのワークローとして例えば以下のようなステップで表すことができる。

  • Review
  • Development
  • Staging
  • Production

ひとまず、本番環境で運用する Heroku アプリの画面から Heroku Pipeline を有効にしよう。

そんで Pipeline の画面に行ったら、Add Existing App をクリックし、既存で管理してた別のステージング環境のアプリを指定する。

無事追加すると、Heroku のアプリ一覧は以下のような感じに統合される。2 Apps となって一緒のアプリになった感が出たね!そして Pipeline には Staging でのアプリが表示されるようになった。

本来ならこれで Github と連携すれば、プルリクエストに応じてパイプラインのアプリが Heroku にデプロイされ、Accept すると勝手に Staging から Production へ昇格させたりできるらしいけど、Bitbucket での運用の場合は普通に git push heroku master とかでデプロイする感じになる。つまり、今までと特に何も変わらないってこと。今後 Heroku の Bitbucket 対応を待つか、諦めて Github にお金を払ってプライベート環境を用意するかになりそうである。

ソースの修正

さて、Bitbucket Pipeline はこのくらいにしておいて、Heroku デプロイの際に表示されるあの忌まわしい Waring を消す作業をしていこう。

これを実施するには、以下のような流れとなる。

  1. config/environments/staging.rbconfig/environments/production.rb の差分を確認
  2. 適宜 環境変数に置き換える
  3. その他コードで Rails.env.staging? のコードを探し、必要に応じて環境変数に置き換える
  4. デプロイ
  5. ステージングアプリで heroku config:set RAILS_ENV=production を適用   環境ごとの差分は staging, productionif 文で切り替えるのではなく、ENV環境変数 で切り替えよう。ここはソースを grep しながら、慎重に修正していかないといけない。

1 の environments の切り替えで特にオススメしたいのはログレベルの設定だ。LOG_LEVEL=infoが本番、LOG_LEVEL=debug がステージングって感じにして config/environments/production.rbconfig.log_level = ENV['LOG_LEVEL'].to_sym にしておくと何かと便利である。

これでステージング環境に Heroku をデプロイすると、見事警告を消去することができた!Heroku に怒られないようになったのはとても喜ばしいことである。

終わりに

今回は、本番環境とステージング環境を別々で管理していた Heroku アプリケーションを、 Heroku Pipeline 上で一まとまりとして管理できるようにした。さらに個々のアプリを 環境変数で対応するようにすることで、デプロイ時の警告を消去することに成功した。

Heroku Pipeline のより具体的な運用事例に関しては、前回の Heroku Meetup の tambourine の安部さんの発表資料を見るといいと思う。

www.bokukoko.info

それでは素敵な Heroku ライフを。