Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all 1381 articles
Browse latest View live

Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

画像はすべて元記事からの引用です。

Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)

まとめ: アプリのサーバー設定はRuby Webアプリのスループットやコストあたりのパフォーマンスに大きな影響を与えます。設定の中でも最も重要なものについて解説します(2846 word、13分)

RubyのWebアプリサーバーは、ある意味で自動車のガソリンに似ています。よいものを使ってもそれ以上速くなりませんが、粗悪なものを使えば止まってしまいます。実際にはアプリサーバーでアプリを著しく高速化することはできません。どのサーバーもだいたい同じようなものであり、取っ替え引っ替えしたところでスループットやレスポンスタイムが向上するわけではありません。しかしダメな設定を使ったりサーバーで設定ミスしたりすれば、たちまち自分の足を撃ち抜く結果になります。クライアントのアプリでよく見かける問題のひとつがこれです。

本記事では、3つの主要なRuby アプリサーバーであるPuma、Unicorn、Passengerについてリソース(メモリやCPU)の使用状況の最適化やスループット(要するに1秒あたりのリクエスト数です)の最大化について解説します。本記事では仮想環境に特化した話はしませんので、「サーバー」と「コンテナ」という言葉を同じ意味で使います。

3つのサーバーの設計は本質的に同じなので、1つのガイドで3つの著名なアプリサーバーをカバーできます。どのサーバーもfork()システムコールを使っていくつもの子プロセスを生成してからリクエストを処理します1。これらサーバーの違いのほとんどは細かい部分にとどまります(本記事ではパフォーマンス最大化が重要な箇所でこうした詳細にも触れる予定です)。

本ガイドを通じて、コストあたりのサーバースループットの最大化を試みます。サーバーリソース(とキャッシュも)が最小の状態から、サーバーが扱える秒あたりのリクエスト数を最大化したいと思います。

パフォーマンス最大化でもっとも重要な設定

アプリサーバーには、パフォーマンスやリソース消費を決定する基本的な設定が4つあります。

  • 子プロセスの数
  • スレッドの数
  • Copy-on-Write
  • コンテナのサイズ

それぞれの設定を見ていきましょう。


* 看護師: 「あなたは31秒前からdynoですね」
* 患者: 「マジで!すぐユーザーのところに戻って、せっかく作ったこのレスポンスを返しに行かなきゃ…」
タイムアウトもそれなりに重要ですが、スループットにはそれほど関連しません。私は今後のためにタイムアウトは変更しないでおきます。

子プロセスの数

Unicorn、Puma、Passengerは、いずれもforkを使う設計になっています2。つまり、アプリのプロセスを1つ作成し、そこから多数のコピーを作成します。これらのコピーは子プロセスと呼ばれます。サーバーごとの子プロセス数は、コストあたりのスループット最大化でおそらく最も重要な設定でしょう3

私が推奨する設定は、すべてのRuby Webアプリで1つのサーバーにつきプロセスを3つ以上実行することです。この設定によってルーティングで最大のパフォーマンスを得られます。PumaとUnicornは、どちらも複数の子プロセスが1つのソケットで直接リッスンする設計になっており、プロセス間のロードバランシングはOSが行います。PassengerはnginxやApacheなどのリバースプロキシを用いて多数のリクエストを1つの子プロセスにルーティングします4。どちらのアプローチも効率はかなり高く、リクエストはアイドリング中のワーカーに素早くルーティングされます。同じことを上位レイヤでのルーティング(ロードバランサーやHerokuのHTTPメッシュを指します)で効率よく行うのは、ルーティング先のサーバーがビジーかどうかをロードバランサー側から確認できないことが多いため、かなり難しくなります5

サーバーが3個、1サーバーあたり1プロセス(つまりプロセスは全部で3個)の編成で考えてみましょう。このときロードバランサーはどのようにして1つのリクエストを3つのサーバーのいずれかに適切にルーティングするのでしょうか。「ランダムに選ぶ」方法や「ラウンドロビン」方式でも可能ですが、その場合アイドリング状態のサーバーへのルーティングは保証されません。たとえばラウンドロビン戦略で、リクエストAがサーバー#1にルーティングされるとします。リクエストBはサーバー#2にルーティングされ、リクエストCはサーバー#3にルーティングされます。


子プロセスが全部ビジーな状態でリクエストを1つ受け取ったときの私の顔。

ここで4つ目のリクエストDが来たとします。リクエストBとCの処理が首尾よく完了したおかげでサーバー#2と#3が暇になっているのに、リクエストAは誰かがCSVをエクスポートしようとしていて完了までに20秒かかるとしたらどうでしょう。ロードバランサーはサーバー#1がビジーであることには構わずリクエストを投げつけるので、リクエストAが完了するまで処理できません。サーバーが完全に死んでいるかどうかを確認する手段はどんなロードバランサーにもありますが、そうした手段はほとんどの場合かなりのタイムラグを伴います(遅延が30秒以上など)。1つのサーバーで実行するプロセス数をもっと増やせば、サーバーレベルではリクエストがビジーなプロセスに割り当てられなくなるため、多くの子プロセスが処理に時間のかかるリクエストで手一杯になってしまうリスクを断ち切ることができます。代わりに、リクエストはワーカーが空くまでソケットレベルまたはリバースプロキシでバックアップされます。これを達成するには、私の経験上1サーバーあたり3プロセス以上が最小値として適切です。リソースの制約のために1サーバーで最小3プロセスを実行できないのであれば、もっと大きなサーバーにしましょう(後述)。

つまり、1つのコンテナでは子プロセスを少なくとも3つは実行すべきです。しかし最大値はどうすればよいでしょうか。これについてはリソース(メモリとCPU)で制限されます。

まずはメモリから考えてみましょう。各子プロセスはある量のメモリを利用します。明らかに、サーバーのRAMがサポートできる個数を上回る子プロセスを追加するべきではありません。


Rubyプロセスの実際のメモリ使用量は対数的に増加します。メモリ断片化が発生するため、増加は水平にならず、単に上限に向かって増加し続けます。

しかし、Rubyアプリの単体プロセスにおける実際のメモリ使用量を調べる方法は単純ではありません。PCやproduction環境でプロセス起動直後の個数を調べる方法では不十分です。理由はいろいろありますが、Ruby Webアプリのプロセスは時間とともにメモリ使用量が増加するからです。ときには生成後の2倍から3倍に達することもあります。Rubyアプリのプロセスで使われるメモリ使用量を正確に測定するには、プロセスの再起動(ワーカーキラー)を無効にしてから、12時間から24時間待ってからpsコマンドで測定します。Herokuユーザーなら、新しい[Heroku Exec]を使って実行中のdynoでpsを実行するか、単にHerokuのメモリ使用量の測定値を1 dynoあたりのプロセス数で割って求めます。多くのRubyアプリは1プロセスあたり200 MBから400 MBのメモリを使いますが、ときには1 GBに達することがあります。


Pumaのワーカーはしばらく経つと…かなり太ります。

メモリ使用量には必ず余裕を見ておいてください。何か子プロセス数を求める公式が欲しいのであれば、(TOTAL_RAM / (RAM_PER_PROCESS * 1.2))を目安にしてください。


レアキャラ「ドット絵DHH」が現れた!
5000デヴィッドに1度だけ出現し、一生使える適切なメモリ使用量を5秒かそこらで受け取る
1いいね = お祈り1回

サーバーやコンテナの上限メモリ量を超えると、メモリが限界に達してスワップが始まるため、速度が大きく低下します。アプリのメモリ使用量を予測可能かつスパイクのない平らな状態にしておきたい理由がこれです。メモリ使用量の急増は、私が「メモリ膨張」と呼んでいる条件です。この問題の解決はまたの機会に別記事で扱いますが、The Complete Guide to Rails Performanceでも扱っています。

次に、サーバーの利用可能なCPUキャパシティを超えないようにしたいと思います。理想的には、CPU使用率100%になる総割り当て時間の5%を超えないことです。これを超えている場合、利用可能なCPUキャパシティでボトルネックが発生していることを示します。多くのRuby on Railsアプリは、クラウドプロバイダのほとんどでメモリリソースがボトルネックになる傾向がありますが、CPUリソースもボトルネックを生じることがあります。どうやって検出すればよいでしょうか。それには、お好みのサーバー監視ツールが使えます。おそらくAWSのビルトインツールなら、CPU使用率が頻繁に上限に達してないかどうかのチェックは十分可能でしょう。


OSのコンテキストスイッチはコストが高いと言っとったじゃないか。productionで実際に使った結果を見ると、あんたがウソ言ってたってことだな。

「CPUの個数より多くの子プロセスを1サーバーに割り当てるべきではない」とよく言われます。その一部は本当ですし、出発点としては適切です。しかし実際のCPU使用率は、自分で監視と最適化を行うべき値です。実際には、多くのアプリのプロセス数は、利用できるハイパースレッド数の1.25〜1.5倍に落ち着くでしょう。

Herokuでは、ログに出力されたCPU負荷の測定値をlog-runtime-metricsで取得します。私は5分間〜15分間の平均負荷をチェックします。値が常に1に近かったり超えたりすることがあるようなら、CPU使用率を超えているので子プロセス数を減らす必要があります。

子プロセス数の設定はどのサーバーでも割りと簡単です。

# Puma
$ puma -w 3 # コマンドラインオプションの場合
workers 3   # config/puma.rbに書く場合

# Unicorn
worker_processes 3 # config/unicorn.rbに書く

# Passenger (nginx/Standalone)
# Passengerのワーカー数は自動で増減します: この設定はあまり便利とは思えなかったので
# 単にmaxとminを一定の数に設定しています。
passenger_max_pool_size 3;
passenger_min_instances 3;

数値を設定ファイルに書く代わりに、WEB_CONCURRENCYなどの環境変数に設定することもできます。

workers Integer(ENV["WEB_CONCURRENCY"] || 3)

まとめると、多くのアプリは使えるリソース量に応じて1サーバーあたり3〜8プロセスを割り当てます。メモリ制約の厳しいアプリや、95パーセンタイル時間(5〜10秒以上)のアプリなら、利用可能なハイパースレッド数の4倍までプロセス数を増やしてもよいでしょう。多くのアプリでは、子プロセスの数を、利用可能なハイパースレッド数の1.5倍を超えないようにすべきです。

スレッド数

PumaやPassenger Enterpriseはアプリでマルチスレッドをサポートするので、このセクションではこの2つのサーバーを対象にします。

スレッドは、アプリの並列性(ひいてはスループット)を軽量なリソースで改善する方法です。Railsは既にスレッドセーフであり、独自のスレッドを作るとかデータベース接続などの共有リソースにグローバル変数でアクセスする($redisのことだよ!)といった妙なことをするアプリはあまりありません。つまり、多くのRuby Webアプリはスレッドセーフということになります。本当にスレッドセーフかどうかを知るには、実際にやってみるしかありません。Rubyアプリのスレッドバグは例外のraiseという派手な方法で顕在化する傾向があるので、簡単に試して結果を見ることができます。

ではスレッド数はいくつにすべきでしょうか。並列性を追加して得られるスピードアップは、プログラムがどの程度並列に実行されるかに依存します。これはアムダールの法則として知られています。MRI(CRuby)の場合、IO待ち(データベースの結果待ちなど)だけが並列化可能です。これは多くのWebアプリでおそらく総時間の10〜25%を占めるでしょう。自分のアプリで、リクエストごとにデータベースで使われる総時間をチェックできます。残念なことに、アムダールの法則によれば、並列性の占める割合が小さい(50%未満)の場合、手頃なスレッド数をさらに増やすメリットはほとんど(あるいはまったく)ありません。そしてこのことは私の経験とも整合します。Noah GibbsもDiscourseホームページのベンチマークでこれをテストした結果、スレッド数は6に落ち着いたそうです。


アムダールの法則

プロセス数の場合は現在の設定による測定値を定期的にチェックして適切にチェックすることをおすすめしますが、スレッド数の場合はそれとは異なり、アプリサーバーのプロセスごとのスレッド数を5に設定して「後は忘れる」でもたいてい大丈夫です。


「設定したら忘れよう」

MRI(CRuby)の場合、スレッド数はメモリに驚くほど大規模な影響を与えることがあります。これはホスト側に複雑な理由がいくつもあるためです(これについては今後別記事を書こうかと思います)。アプリのスレッド数を増やす場合、その前後でメモリ消費を必ずチェックしましょう。各スレッドがスタック空間で余分に消費するメモリが8 MBにとどまると期待しないことです。総メモリ使用量はしばしばこれよりずっと多くなります。

スレッド数の設定方法は次のとおりです。

# Puma: 繰り返しますが、私は「自動」スピンアップ/スピンダウン機能は本当に使わないので
# minとmaxには同じ値を設定しています
$ puma -t 5:5 # コマンドライン・オプション
threads 5, 5  # config/puma.rbに書く場合

# Passenger (nginx/Standalone)
passenger_concurrency_model thread;
passenger_thread_count 5;

JRubyをお使いの方へ: スレッドは完全に並列化されるので、アムダールの法則によるメリットをすべて得られます。JRubyでのスレッド数の設定は、上述したMRIでのプロセス数の設定にむしろ似ていて、メモリやCPUリソースを使い切るところまで増やせば済みます。

Copy-on-writeの振舞い

あらゆるUnixベースのOSではメモリの挙動にcopy-on-writeが実装されています。copy-on-writeはかなりシンプルです。プロセスがforkして子プロセスが作成された時点では、その子プロセスのメモリは親プロセスと完全に共有されます。しかしメモリに変更が生じるとコピーが作成され、その子プロセス専用のメモリになります。子プロセスは(理論的には)共有ライブラリやその他の「読み取り専用」メモリを(独自のコピーを作成する代わりに)親プロセスと共有できるようになっているべきなので、(copy-on-writeは)forkを繰り返すWebサーバーでメモリ使用量を減らすうえで大変役に立ちます。

copy-on-writeは、単に発生するものです5。copy-on-writeは「オフにできません」が、効率を高めます。基本的に私達がやりたいのは、forkの前にアプリをすべて読み込むことであり、多くのRuby Webサーバーでは「プリロード」と呼ばれています。copy-on-writeがあることで変わる点は、アプリが初期化される前と後でfork呼び出しが変わるだけです。

fork後、利用しているデータベースへの再接続も必要です。ActiveRecordの例を以下に示します。

# Puma
preload_app!
on_worker_boot do
  # Rails 4.1で`config/database.yml`を使って`pool`サイズを設定するのは有効
  ActiveRecord::Base.establish_connection
end

# Unicorn
preload_app true
after_fork do |server, worker|
  ActiveRecord::Base.establish_connection
end

# Passengerはデフォルトでプリロードを行うのでオンにする必要はない
# Passengerは自動でActiveRecordへの接続を確立するが、
# 他のDBの場合は以下を行わなければならない
PhusionPassenger.on_event(:starting_worker_process) do |forked|
  if forked
    reestablish_connection_to_database # DBによって異なる
  end
end

理論上は、アプリで使われるすべてのデータベースに対してこれを行わなければなりません。しかし実際には、Sidekiqは実際に何か行うまでRedisへの接続を試行しないので、アプリ起動時にSidekiqジョブを実行しているのでなければ、fork後に再接続する必要はありません。

残念なことに、copy-on-writeのメリットには限りがあります。透過的で巨大なページでは、メモリが1ビット変更されただけでも2 MBものページ全体がコピーされますし、メモリ断片化によっても上限が生じます。しかしそれで問題が生じるわけではないので、プリロードはとにかくオンにしておきましょう。

コンテナのサイズ


もっとメモリよこせや(゚Д゚)ゴルァ!!

一般に、サーバーで利用可能なCPUやメモリの利用率は70〜80%ぐらいにとどめておきたいものです。こうしたニーズはアプリによって異なりますし、CPUコア数とメモリのGB数の比率によっても変わります。あるアプリでは、4 vCPU/4 GB RAMのサーバーでRubyプロセスが6つ動くのがもっとも良好かもしれませんし、メモリ要求がより少なくCPU負荷のより高いアプリなら8 vCPU/2GB RAMがよいかもしれません。コンテナのサイズに完全なものはありませんので、CPUとメモリの比率は実際のproductionでの測定値に基いて選択すべきです。


Rails(有名なWebフレームワーク)とHeroku(RAM 512MB)、どっちが勝つか

サーバーで利用可能な総メモリ容量は、チューニング可能なリソースのうちでおそらく非常に重要なものです。多くのプロバイダは極めて低い値が採用されており、Herokuの標準的なdynoでは512 MBとなっています。Rubyアプリ、特に複雑かつ成熟したアプリは多くのメモリを要求するので、与えるべき総メモリ容量はおそらく非常に重要なリソースでしょう。

多くのRailsアプリで使われるRAMは300 MB以下なので、1サーバーあたり3プロセス以上を常に実行しているとすれば、多くのRailsアプリのRAMは少なくとも1 GBになるでしょう。

サーバーのCPUリソースも同じくチューニング可能な設定として重要です。利用可能なCPUコア数を知る必要がありますし、同時に実行可能なスレッド数も知る必要があります(そもそもサーバーでハイパースレッディングをサポートしているかどうかも知る必要があります)。

子プロセス数のところで解説したように、コンテナは少なくとも子プロセスを3つ以上サポートすべきです。1サーバー(またはコンテナ)あたり8プロセス以上にできればさらに改善されるでしょう。1コンテナあたりのプロセス数を増やせば、リクエストのルーティング改善やレイテンシの逓減に効果を発揮します。

まとめ

Ruby Webアプリサーバーのスループットを最大化する方法の概要を以下にまとめました。短いリスト形式になっているので、以下の手順に沿って進められます。

  1. スレッド数5のワーカー1つが使うメモリ容量を決定する。Unicornをお使いの場合は、明らかにスレッドは不要です。production環境の単一サーバー上でいくつかのワーカーを実行して少なくとも12時間は再起動せずに動かし続けてから、典型的なワーカーのメモリ容量をpsで調べます。
  2. コンテナサイズの値は、上のメモリ容量の少なくとも3倍以上にする。多くのRailsアプリでは1ワーカーあたり最大300 MB〜400 MBのRAMを使いますので、多くのRailsアプリは1コンテナ(サーバー)あたり 1 GB必要になります。これによって、1サーバーあたり3プロセスを実行する余裕のあるメモリ容量になります。実行できる子プロセス数は、(TOTAL_RAM / (RAM_PER_PROCESS * 1.2))に等しくなります。

  3. CPUコア/ハイパースレッド数をチェックする。コンテナのハイパースレッド数(AWSの場合はvCPU)が、メモリがサポート可能な数より少ない場合は、メモリが少なくCPUが多いコンテナに適したコンテナサイズを選択します。実行すべき子プロセス数は、ハイパースレッド数の1.25倍〜1.5倍が理想です。

  4. デプロイ後にCPUとメモリの消費を監視する。使用量を最大化するのに適切な子プロセス数とコンテナサイズを調整します。

関連記事

https://techracho.bpsinc.jp/hachi8833/2017_06_15/41465

RailsConf 2017のパフォーマンス関連の話題(3)「あなたのアプリサーバーの設定は間違っている」など(翻訳)

Rails 5のWebSocket対応アプリでDoS脆弱性を見つけるまで(翻訳)


  1. 3つのアプリサーバーで子プロセスを作成するmasterプロセスは、いずれも実際にはリクエストを処理しません。Passengerでforkが最近実行されていない場合、実際にはしばらくしてからmasterのプリロード処理を終了します。 
  2. JRubyの人なら次のセクションをスキップしてもよいでしょう。 
  3. これはCRubyのGlobal VM Lock(GVL)が原因です。Rubyコードを実行できるのは1度に1つのスレッドに限られるので、Rubyの並行処理を達成するにはプロセスを複数実行する以外に方法はありません。私たちは、サーバーのリソースを超えないようにしながら、サーバーあたりのプロセス数をできるだけ多く実行したいのです。 
  4. 私はPassengerの「least-busy-process-first」ルーティングが実は大好きです。 
  5. メモリをもっと効果的に節約するためにcopy-on-writeをさらに「サポートする」といったことはできません。 

Ruby: 認証gem ‘Rodauth’ README(翻訳)

$
0
0

こんにちは、hachi8833です。

今回は、「Railsアプリの認証システムをセキュアにする4つの方法」でも取り上げられていたRodauthのREADMEを翻訳しました。

現時点では、残念ながらRodauthをRailsで使うためのroda-rails gemがRails 4.2までしか対応していないのと、ルーティングにDSLを使うことから、おそらくRails 5で使うには一手間かける必要がありそうです。

しかし、認証のセキュリティを考えるうえで参考になる情報が多く、ドキュメントの質が(少なくともDeviseと比べて)非常に高いのが特徴です。大きな概要をまず示し、必要な概念も適宜示しながら、先に進むに連れて詳細に説明するという書き方が見事です。

さらに、READMEから読み取れる筋のよい認証システム設計も参考になると思います。パスワードハッシュの保存場所を完全に隔離した上で可能な限り自由度を高めている点や、機能ごとにカラムを足したり減らしたりするのではなくテーブルを足したり減らしたりする点など、学ぶところが多そうです。

概要

MITライセンスに基いて翻訳・公開します。


http://rodauth.jeremyevans.net/より

Rodauth README(翻訳)

RodauthはRackアプリケーション向けの認証・アカウント管理フレームワークです。ビルドにはRodaとSequelを使っていますが、他のWebフレームワーク・データベースライブラリ・データベースでも利用できます。PostgreSQL・MySQL・Microsoft SQL Serverをデフォルト設定で使うと、データベース関数経由でのアクセスが保護されるようになり、パスワードハッシュのセキュリティを強化できます。

設計上のゴール

  • セキュリティ: デフォルト設定で最大のセキュリティを利用できること
  • 簡潔性: DSLで簡単に設定できること
  • 柔軟性: フレームワークのどの部分でも機能をオーバーライドできること

機能リスト

  • ログイン
  • ログアウト
  • パスワードの変更
  • ログインの変更
  • パスワードのリセット
  • アカウントの作成
  • アカウントの無効化
  • アカウントのバリデーション
  • パスワードの確認
  • パスワードの保存(トークン経由での自動ログイン)
  • ロックアウト(総当たり攻撃からの保護)
  • OTP (TOTP経由の2要素認証)
  • リカバリーコード(バックアップコード経由の2要素認証)
  • SMSコード(SMS経由の2要素認証)
  • ログイン変更のバリデーション(ログイン変更前の新規ログインバリデーション)
  • アカウントの許容期間(ログイン前のバリデーションを不要にする)
  • パスワードの許容期間(パスワードを最近入力した場合はパスワード入力を不要にする)
  • パスワードの強度(より洗練されたチェック)
  • パスワードの使い回し禁止
  • パスワードの有効期限
  • アカウントの有効期限
  • セッションの有効期限
  • シングルセッション(アカウントのアクティブセッションを1つに限定)
  • JWT(他のすべての機能でJSON APIをサポート)
  • パスワードハッシュの更新(ハッシュのcostが変更された場合)
  • HTTP BASIC認証

リソース

Webサイト
http://rodauth.jeremyevans.net
デモサイト
http://rodauth-demo.jeremyevans.net
ソースコード
http://github.com/jeremyevans/rodauth
バグ報告
http://github.com/jeremyevans/rodauth/issues
Google Group
https://groups.google.com/forum/#!forum/rodauth
IRC(チャット)
irc://chat.freenode.net/#rodauth

依存関係

Rodauthがデフォルトで依存しているgemが若干ありますが、それらについてはRodauthの開発上依存しているものであり、運用上はgemなしで実行することも可能です。

tilt、rack_csrf
すべての機能で利用(JSON API onlyモードの場合を除く)
bcrypt
パスワードの一致チェックでデフォルトで利用(カスタム認証でpassword_match?をオーバーライドすればスキップ可能)
mail
パスワードリセット時・アカウント確認時・ロックアウト機能のメール送信で利用
rotp、rqrcode
OTP機能で利用
jwt
JWT機能で利用

セキュリティ

データベース関数経由でのパスワードハッシュアクセス

RodauthでPostgreSQL・MySQL・Microsoft SQL Serverを利用する場合、デフォルトでパスワードハッシュにアクセスするためのデータベース関数を使います。これにより、アプリケーションを実行するユーザーはパスワードハッシュに直接アクセスできないようになっています。この機能によって攻撃者がパスワードハッシュにアクセスするリスクや、パスワードハッシュを他のサイトの攻撃に利用されるリスクを減らします。

本セクションでは以後この機能についてもっと詳しく説明します。なお、Rodauthはこの機能を使わなくても利用できます。異なるデータベースを利用している場合や、データベースの権限が不足している場合などには、この機能を利用できないことがあります。

パスワードはbcryptでハッシュ化され、アカウントテーブルとは別のテーブルに保存されます。また、2つのデータベース関数を追加します。1つはパスワードで使うsaltを取得する関数、もう1つは渡されたパスワードハッシュがユーザーのパスワードハッシュと一致するかどうかをチェックする関数です。

Rodauthでは2つのデータベースアカウントを使います。1つはアプリで使うアカウント用(以下「appアカウント」)、もう1つはパスワードハッシュ用(以下「phアカウント」)です。phアカウントは、渡されたパスワードのsaltを取得するデータベース関数と、渡されたアカウントでパスワードハッシュが一致するかどうかをチェックする関数を設定します。2つの関数は、appアカウントでphアカウントのパーミッションを使って実行されるようになっています。これにより、appアカウントでパスワードハッシュを読み取らずにパスワードをチェックできます。

appアカウントではパスワードハッシュを読み出すことはできない代わりに、パスワードハッシュのINSERT、UPDATE、DELETEはできるので、この機能によって追加されたセキュリティで大きな不便は生じません。

appアカウントでのパスワードハッシュ読み取りを禁止したことによって、仮にアプリのSQLインジェクションやリモートでのコード実行の脆弱性を攻撃された場合であっても、攻撃者によるパスワードハッシュの読み取りはさらに難しくなっています。

パスワードハッシュにこのようなセキュリティ対策を追加した理由は、弱いパスワードを使うユーザーやパスワードを使いまわすユーザーが後を絶たず、あるデータベースのパスワードハッシュが盗み出されると他のサイトのアカウントにまでアクセスされる可能性があるからです。そのため、たとえ保存されている他のデータの重要性が低いとしても、パスワードハッシュの保存方法はセキュリティ上きわめて重要度が高くなっています。

アプリのデータベースに機密情報が他にも保存されているのであれば、他の情報(あるいはすべての情報)についてもパスワードハッシュと同様のアプローチを行うことを検討すべきです。

トークン

アカウントの検証トークン、パスワードリセットのトークン、パスワード保存のトークン、ロックアウトのトークンでは同様のアプローチを採用しています。これらはすべてアカウントID_長いランダム文字列形式のトークンを提供します。トークンにアカウントIDを含めることで、攻撃者は全アカウントに対してトークンを総当り(ブルートフォース)攻撃で推測できなくなり、一度に1人のユーザーに対してしか総当たり攻撃を行えなくなります。なお、トークンがランダム文字列だけで構成されていると、全アカウントに対して総当たり攻撃が可能になる場合があります。

さらに、トークンの比較にはタイミング攻撃に対して安全な関数を採用し、タイミング攻撃のリスクを低減しています。

PostgreSQLデータベースの設定

PostgreSQLでRodauthのセキュリティ設計をすべて利用するには、複数のデータベースアカウントを使います。

  1. データベースのsuperuserアカウント(通常はpostgres)
  2. appアカウント(実際はアプリと同じ名前にします)
  3. phアカウント(実際はアプリ名に_passwordを追加した名前にします)

データベースのsuperuserアカウントは、データベースに関連する拡張(extension)の読み込みに使われます。アプリでは絶対にデータベースのsuperuserアカウントを使ってはいけません。

HerokuのPostgreSQLデータベースについては、上のようなシンプルな方法で複数のデータベースアカウントを設定する方法がありません。もちろん、HerokuでRodauthを使うことはできますが、セキュリティ上の利点は同じにはなりません。ただしこれはセキュリティ上危険ということではなく、パスワードハッシュの保存方法が他のメジャーな認証ソリューションと同じレベルになるということです。

データベースアカウントの作成

アプリがデータベースのsuperuserアカウントを使って実行されているのであれば、最初にappデータベースアカウントの作成が必要です。このアカウント名はデータベース名と同じにしておくのが多くの場合ベストです。

続いてphデータベースアカウントを作成します。このアカウントはパスワードハッシュへのアクセスに使われます。

  • PostgreSQLでの実行例
createuser -U postgres ${DATABASE_NAME}
createuser -U postgres ${DATABASE_NAME}_password

superuserアカウントがデータベース内の全アイテムの所有者になっている場合、上で作成したオーナーシップの変更が必要です。詳しくはhttps://gist.github.com/jeremyevans/8483320をご覧ください。

データベースの作成

一般に、アプリのアカウントはほとんどのテーブルを所有するので、アプリのアカウントがデータベースのオーナーとなります。

createdb -U postgres -O ${DATABASE_NAME} ${DATABASE_NAME}

上の方法はアプリ開発方法として最もセキュアとは言えないため、注意が必要です。セキュリティを最大化したい場合は、テーブルのオーナーとして独自のデータベースアカウントを使い、アプリのアカウントはテーブルのオーナーにならないようにし、正常動作に必要な最小限のアクセス権だけをアプリのアカウントに許可します。

拡張の読み込み

Rodauthのログイン機能で大文字小文字を区別しないログインをサポートするには、citext拡張を読み込む必要があります。

例:

psql -U postgres -c "CREATE EXTENSION citext" ${DATABASE_NAME}

Herokuの場合、citextは標準のデータベースアカウントで読み込まれます。ログインで大文字小文字を区別したいのであれば(ただし一般にはよくないとされています)、PostgreSQLのcitext拡張を読み込む必要はありません。その場合は、マイグレーション内のcitextStringに変更し、メールアドレスに対応できるようにしてください。

デフォルト以外のスキーマを使う

PostgreSQLは、デフォルトでパブリックなスキーマで新規テーブルをセットアップします。ユーザーごとに個別のスキーマを使いたい場合は、次のようにします。

psql -U postgres -c "DROP SCHEMA public;" ${DATABASE_NAME}
psql -U postgres -c "CREATE SCHEMA ${DATABASE_NAME} AUTHORIZATION ${DATABASE_NAME};" ${DATABASE_NAME}
psql -U postgres -c "CREATE SCHEMA ${DATABASE_NAME}_password AUTHORIZATION ${DATABASE_NAME}_password;" ${DATABASE_NAME}
psql -U postgres -c "GRANT USAGE ON SCHEMA ${DATABASE_NAME} TO ${DATABASE_NAME}_password;" ${DATABASE_NAME}
psql -U postgres -c "GRANT USAGE ON SCHEMA ${DATABASE_NAME}_password TO ${DATABASE_NAME};" ${DATABASE_NAME}

スキーマを指定する拡張の読み込み部分のコードの変更が必要です。

psql -U postgres -c "CREATE EXTENSION citext SCHEMA ${DATABASE_NAME}" ${DATABASE_NAME}

phユーザーでマイグレーションを実行する場合、スキーマ変更に対応するいくつかの変更が必要です。

create_table(:account_password_hashes) do
  foreign_key :id, Sequel[:${DATABASE_NAME}][:accounts], :primary_key=>true, :type=>:Bignum
  String :password_hash, :null=>false
end
Rodauth.create_database_authentication_functions(self, :table_name=>"${DATABASE_NAME}_password.account_password_hashes")

# if using the disallow_password_reuse feature:
create_table(:account_previous_password_hashes) do
  primary_key :id, :type=>:Bignum
  foreign_key :account_id, Sequel[:${DATABASE_NAME}][:accounts], :type=>:Bignum
  String :password_hash, :null=>false
end
Rodauth.create_database_previous_password_check_functions(self, :table_name=>"${DATABASE_NAME}_password.account_previous_password_hashes")

また、次のRodauth設定メソッドを使って、アプリのアカウントが個別のスキーマで関数を呼び出すようにします。

function_name do |name|
  "${DATABASE_NAME}_password.#{name}"
end
password_hash_table Sequel[:${DATABASE_NAME}_password][:account_password_hashes]

# disallow_password_reuse でパスワード再利用を禁止する場合:
previous_password_hash_table Sequel[:${DATABASE_NAME}_password][:account_previous_password_hashes]

MySQLデータベースの設定

MySQLにはオブジェクトの所有者という概念がなく、MySQLのGRANTやREVOKEのサポートはPostgreSQLと比べて限定されています。MySQLを使う場合、以下のようにphアカウントにGRANT ALLしてすべてのパーミッションを与え、さらにWITH GRANT OPTIONphアカウントからappアカウントにGRANTできるようにすることをおすすめします。

CREATE USER '${DATABASE_NAME}'@'localhost' IDENTIFIED BY '${PASSWORD}';
CREATE USER '${DATABASE_NAME}_password'@'localhost' IDENTIFIED BY '${OTHER_PASSWORD}';
GRANT ALL ON ${DATABASE_NAME}.* TO '${DATABASE_NAME}_password'@'localhost' WITH GRANT OPTION;

マイグレーションの実行は常にphアカウントで行い、appアカウントには必要に応じてGRANTで特定のアクセス権を与えなければなりません。

MySQLでデータベース関数を追加するには、MySQLの設定にlog_bin_trust_function_creators=1が必要になることがあります。

Microsoft SQL Serverデータベースの設定

Microsoft SQL Serverにはデータベースの所有者という概念はありますが、MySQLの場合と同様、phアカウントをデータベースのスーパーユーザーとして使い、phからGRANTでappアカウントにパーミッションを与えられるようにすることをおすすめします。

CREATE LOGIN rodauth_test WITH PASSWORD = 'rodauth_test';
CREATE LOGIN rodauth_test_password WITH PASSWORD = 'rodauth_test';
CREATE DATABASE rodauth_test;
USE rodauth_test;
CREATE USER rodauth_test FOR LOGIN rodauth_test;
GRANT CONNECT, EXECUTE TO rodauth_test;
EXECUTE sp_changedbowner 'rodauth_test_password';

マイグレーションの実行は常にphアカウントで行い、appアカウントには必要に応じてGRANTで特定のアクセス権を与えなければなりません。

テーブルの作成

異なる2種類のデータベースアカウントを使っているため、マイグレーションもデータベースアカウントごとに実行する必要があります(2つの異なるマイグレーションを実行します)。マイグレーションの例を以下に示します。このマイグレーションを変更して追加カラムをサポートしたり、Rodauthの不要な機能に関連するカラムやテーブルを削除することもできます。

1回目のマイグレーション

PostgreSQLの場合はappアカウントで実行する必要があります。MySQLやMicrosoft SQL Serverの場合はphアカウントで実行する必要があります。

マイグレーションの実行にはSequel 4.35.0以降が必要です。これより前のバージョンのSequelを使っている場合は、:BignumシンボルをBignum定数に変更してください。

Sequel.migration do
  up do
    extension :date_arithmetic

    # アカウントの検証やアカウントの無効化機能で使用
    create_table(:account_statuses) do
      Integer :id, :primary_key=>true
      String :name, :null=>false, :unique=>true
    end
    from(:account_statuses).import([:id, :name], [[1, 'Unverified'], [2, 'Verified'], [3, 'Closed']])

    db = self
    create_table(:accounts) do
      primary_key :id, :type=>:Bignum
      foreign_key :status_id, :account_statuses, :null=>false, :default=>1
      if db.database_type == :postgres
        citext :email, :null=>false
        constraint :valid_email, :email=>/^[^,;@ rn]+@[^,@; rn]+.[^,@; rn]+$/
        index :email, :unique=>true, :where=>{:status_id=>[1, 2]}
      else
        String :email, :null=>false
        index :email, :unique=>true
      end
    end

    deadline_opts = proc do |days|
      if database_type == :mysql
        {:null=>false}
      else
        {:null=>false, :default=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, :days=>days)}
      end
    end

    # パスワードのリセット機能で使用
    create_table(:account_password_reset_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      DateTime :deadline, deadline_opts[1]
    end

    # アカウントの検証機能で使用
    create_table(:account_verification_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      DateTime :requested_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
    end

    # ログイン変更の検証機能で使用
    create_table(:account_login_change_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      String :login, :null=>false
      DateTime :deadline, deadline_opts[1]
    end

    # パスワード保存機能で使用
    create_table(:account_remember_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      DateTime :deadline, deadline_opts[14]
    end

    # ロックアウト機能で使用
    create_table(:account_login_failures) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      Integer :number, :null=>false, :default=>1
    end
    create_table(:account_lockouts) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      DateTime :deadline, deadline_opts[1]
    end

    # パスワードの有効期限機能で使用
    create_table(:account_password_change_times) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      DateTime :changed_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
    end

    # アカウントの有効期限機能で使用
    create_table(:account_activity_times) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      DateTime :last_activity_at, :null=>false
      DateTime :last_login_at, :null=>false
      DateTime :expired_at
    end

    # シングルセッション機能で使用
    create_table(:account_session_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
    end

    # OTP機能で使用
    create_table(:account_otp_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      Integer :num_failures, :null=>false, :default=>0
      Time :last_use, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
    end

    # リカバリーコード機能で使用
    create_table(:account_recovery_codes) do
      foreign_key :id, :accounts, :type=>:Bignum
      String :code
      primary_key [:id, :code]
    end

    # SMSコード機能で使用
    create_table(:account_sms_codes) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :phone_number, :null=>false
      Integer :num_failures
      String :code
      DateTime :code_issued_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
    end

    case database_type
    when :postgres
      user = get{Sequel.lit('current_user')} + '_password'
      run "GRANT REFERENCES ON accounts TO #{user}"
    when :mysql, :mssql
      user = if database_type == :mysql
        get{Sequel.lit('current_user')}.sub(/_password@/, '@')
      else
        get{DB_NAME{}}
      end
      run "GRANT ALL ON account_statuses TO #{user}"
      run "GRANT ALL ON accounts TO #{user}"
      run "GRANT ALL ON account_password_reset_keys TO #{user}"
      run "GRANT ALL ON account_verification_keys TO #{user}"
      run "GRANT ALL ON account_login_change_keys TO #{user}"
      run "GRANT ALL ON account_remember_keys TO #{user}"
      run "GRANT ALL ON account_login_failures TO #{user}"
      run "GRANT ALL ON account_lockouts TO #{user}"
      run "GRANT ALL ON account_password_change_times TO #{user}"
      run "GRANT ALL ON account_activity_times TO #{user}"
      run "GRANT ALL ON account_session_keys TO #{user}"
      run "GRANT ALL ON account_otp_keys TO #{user}"
      run "GRANT ALL ON account_recovery_codes TO #{user}"
      run "GRANT ALL ON account_sms_codes TO #{user}"
    end
  end

  down do
    drop_table(:account_sms_codes,
               :account_recovery_codes,
               :account_otp_keys,
               :account_session_keys,
               :account_activity_times,
               :account_password_change_times,
               :account_lockouts,
               :account_login_failures,
               :account_remember_keys,
               :account_login_change_keys,
               :account_verification_keys,
               :account_password_reset_keys,
               :accounts,
               :account_statuses)
  end
end

2回目のマイグレーション

phアカウントで実行します。

require 'rodauth/migrations'

Sequel.migration do
  up do
    create_table(:account_password_hashes) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :password_hash, :null=>false
    end
    Rodauth.create_database_authentication_functions(self)
    case database_type
    when :postgres
      user = get(Sequel.lit('current_user')).sub(/_password\z/, '')
      run "REVOKE ALL ON account_password_hashes FROM public"
      run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
      run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
      run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
      run "GRANT SELECT(id) ON account_password_hashes TO #{user}"
      run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO #{user}"
      run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO #{user}"
    when :mysql
      user = get(Sequel.lit('current_user')).sub(/_password@/, '@')
      db_name = get(Sequel.function(:database))
      run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
      run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
      run "GRANT SELECT (id) ON account_password_hashes TO #{user}"
    when :mssql
      user = get(Sequel.function(:DB_NAME))
      run "GRANT EXECUTE ON rodauth_get_salt TO #{user}"
      run "GRANT EXECUTE ON rodauth_valid_password_hash TO #{user}"
      run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
      run "GRANT SELECT ON account_password_hashes(id) TO #{user}"
    end

    # disallow_password_reuse 機能で使われる
    create_table(:account_previous_password_hashes) do
      primary_key :id, :type=>:Bignum
      foreign_key :account_id, :accounts, :type=>:Bignum
      String :password_hash, :null=>false
    end
    Rodauth.create_database_previous_password_check_functions(self)

    case database_type
    when :postgres
      user = get(Sequel.lit('current_user')).sub(/_password\z/, '')
      run "REVOKE ALL ON account_previous_password_hashes FROM public"
      run "REVOKE ALL ON FUNCTION rodauth_get_previous_salt(int8) FROM public"
      run "REVOKE ALL ON FUNCTION rodauth_previous_password_hash_match(int8, text) FROM public"
      run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
      run "GRANT SELECT(id, account_id) ON account_previous_password_hashes TO #{user}"
      run "GRANT USAGE ON account_previous_password_hashes_id_seq TO #{user}"
      run "GRANT EXECUTE ON FUNCTION rodauth_get_previous_salt(int8) TO #{user}"
      run "GRANT EXECUTE ON FUNCTION rodauth_previous_password_hash_match(int8, text) TO #{user}"
    when :mysql
      user = get(Sequel.lit('current_user')).sub(/_password@/, '@')
      db_name = get(Sequel.function(:database))
      run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
      run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
      run "GRANT SELECT (id, account_id) ON account_previous_password_hashes TO #{user}"
    when :mssql
      user = get(Sequel.function(:DB_NAME))
      run "GRANT EXECUTE ON rodauth_get_previous_salt TO #{user}"
      run "GRANT EXECUTE ON rodauth_previous_password_hash_match TO #{user}"
      run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
      run "GRANT SELECT ON account_previous_password_hashes(id, account_id) TO #{user}"
    end
  end

  down do
    Rodauth.drop_database_previous_password_check_functions(self)
    Rodauth.drop_database_authentication_functions(self)
    drop_table(:account_previous_password_hashes, :account_password_hashes)
  end
end

マイグレーションを複数ユーザーで手分けして行いたい場合、SequelのマイグレーションAPIを使ってパスワードユーザーのマイグレーションを実行できます。

Sequel.extension :migration
Sequel.postgres('DATABASE_NAME', :user=>'PASSWORD_USER_NAME') do |db|
  Sequel::Migrator.run(db, 'path/to/password_user/migrations', :table=>'schema_info_password')
end

PostgreSQL・MySQL・Microsoft SQL Server以外のデータベースを使う場合や、(データベースの)ユーザーアカウントを複数使えない場合は、単に2つのマイグレーションを1つのマイグレーションにまとめます。

上のマイグレーションを読むとわかるように、Rodauthでは1つのテーブルにさまざまなカラムを追加するのではなく、追加機能ごとにテーブルを追加する設計になっています。

使い方

基本的な使い方

RodauthはRodaのプラグインなので、以下のように他のRodaプラグインと同じ方法で読み込みます。

plugin :rodauth do
end

plugin呼び出しでは、Rodauthの設定用DSLをブロックとして受け取ります。読み込む機能を指定するenableという設定メソッドは省略できません。

plugin :rodauth do
  enable :login, :logout
end

機能が読み込まれた後は、その機能でサポートされる設定用メソッドをすべて利用できるようになります。設定用メソッドには次の2種類があります。

    1. 認証系メソッド

1つ目は認証系メソッド(auth methods)と呼ばれます。これらのメソッドはブロックを1つ取り、Rodauthのデフォルトメソッドをオーバーライドします。ブロック内でsuperを呼ぶとデフォルトの動作を取得できますが、superには明示的に引数を渡す必要があります。なお、beforeフックやafterフックではsuperを呼ぶ必要はありません。

たとえば、ユーザーのログイン時にログ出力を追加したい場合は次のようにします。

plugin :rodauth do
  enable :login, :logout
  after_login do
    LOGGER.info "#{account[:email]} logged in!"
  end
end

ブロック内は、リクエストに関連付けられたRodauth::Authインスタンスのコンテキストになります。このオブジェクトで次のメソッドを使うと、リクエストに関連するあらゆるデータにアクセスできます。

request
RodaRequestのインスタンス
response
RodaResponseのインスタンス
scope
Rodaのインスタンス
session
セッションのハッシュ
flash
flashメッセージのハッシュ
account
アカウントモデルのインスタンス(Rodauthのメソッドで事前に設定済みの場合)

ログイン中のユーザーのIPアドレスをログ出力したい場合は次のようにします。

plugin :rodauth do
  enable :login, :logout
  after_login do
    LOGGER.info "#{account[:email]} logged in from #{request.ip}"
  end
end
    1. 認証値系メソッド

設定用メソッドの2つ目は認証値(auth value)のメソッドです。認証値系メソッドは認証系メソッドと似ていますが、単にブロックを受け取るほかに、ブロック無しで引数を1つ受け取ることもできます。受け取った引数は、その値を単に返すブロックとして扱われます。

たとえば、データベースのテーブルにアカウントを保存するaccounts_tableの場合、次のようにテーブル名をシンボルで渡すことでオーバーライドできます。

plugin :rodauth do
  enable :login, :logout
  accounts_table :users
end

認証値系メソッドはブロックを1つ受け取ることもできるので、リクエストから得られる情報を使ってすべての挙動を上書きできます。

plugin :rodauth do
  enable :login, :logout
  accounts_table do
    request.ip.start_with?("192.168.1") ? :admins : :users
  end
end

Rodauthではどの設定メソッドもブロックを受け取れるので、多くのレガシーシステムを統合するのに十分な柔軟性を備えています。

各機能のドキュメント

サポートされている各機能のオプションやメソッドについては、機能ごとに別ページを設けています。もしリンクが切れていたら、ドキュメントのディレクトリで必要なファイルを参照してください。

Rodauthをルーティングツリーで呼び出す

一般に、以下のようにrodauthをルーティングブロックの早い段階で呼び出すのが普通です。

route do |r|
  r.rodauth

  # ...
end

Rodauthはこれで実行できます。ただしこのままでは、アクセスするユーザーのログインを必須にしたり、サイトにセキュリティを追加したりできません。すべてのユーザーに対してログインを必須にするには、ログインしていないユーザーを以下のように強制的にログインページにリダイレクトします。

route do |r|
  r.rodauth
  rodauth.require_authentication

  # ...
end

ログインを必須にしたいページがサイトの一部に限られている場合は、以下のようにすると、ルーティングツリーの特定のブランチについてだけユーザーがログインしていない場合にリダイレクトできます。

route do |r|
  r.rodauth

  r.on "admin" do
    rodauth.require_authentication

    # ...
  end

  # ...
end

Rodauthをルーティングツリーのルートではなく、ルーティングのブランチ内でだけ実行したい場合があります。その場合は以下のようにRodauthの設定で:prefixを設定してから、ルーティングツリーの該当するブランチでr.rodauthを呼び出します。

plugin :rodauth do
  enable :login, :logout
  prefix "auth"
end

route do |r|
  r.on "auth" do
    r.rodauth
  end

  rodauth.require_authentication

  # ...
end

rodauthメソッド

Rodauthの機能のほとんどはr.rodauth経由で公開されています。これを使って、Rodauthで自分が有効にした機能にルーティングできます(ログイン機能の/loginなど)。しかし、上述したようにこうしたメソッドをrodauthオブジェクトで呼び出したいこともあります(現在のリクエストが認証済みであるかどうかのチェックなど)。

以下のメソッドは、r.rodauthの外でもrodauthオブジェクトで呼び出せるように設計されています。

require_login
セッションでログインを必須にし、ログインしていないリクエストをログインページにリダイレクトします。
require_authentication
require_loginと似ていますが、アカウントが2要素認証用に設定されている場合は2要素認証も必須にします。ログイン済みであっても2要素認証されていない場合は、リクエストを2要素認証ページにリダイレクトします。
logged_in?
セッションがログイン中であるかどうかを返します。
authenticated?
logged_in?と似ていますが、アカウントが2要素認証用に設定されている場合はセッションが2要素認証されているかどうかを返します。
require_two_factor_setup
(2要素認証用)セッションで2要素認証を必須にします。2要素認証されていない場合は、リクエストを2要素認証ページにリダイレクトします。
uses_two_factor_authentication?
(2要素認証用)現在のセッションのユーザーが2要素認証を使えるよう設定されているかどうかを返します。
update_last_activity
(アカウント有効期限用)現在のアカウントの最終活動時刻を更新します。最終活動時刻を基にアカウントの有効期限が切れるようにしてある場合にのみ意味があります。
require_current_password
(アカウント有効期限用)アカウントのパスワードの有効期限が切れた場合に、パスワード変更ページにリダイレクトして現在のパスワードを入力しないと継続できないようにします。
load_memory
(パスワード保存機能用)セッションが認証されていない場合に、remember cookieがあるかどうかをチェックします。有効なremember cookieがある場合はセッションに自動ログインしますが、rememberキー経由でログインしたというマークを付けます。
logged_in_via_remember_key?
(パスワード保存機能用) rememberキーを使って現在のセッションにログインしたかどうかを返します。セキュリティ上重要な操作でパスワードの再入力を必須にしたい場合は、confirm_passwordを使えます。
check_session_expiration
(セッション有効期限用) 現在のセッションの有効期限が切れているかどうかをチェックし、期限切れの場合は自動的にログアウトします。
check_single_session
(シングルセッションの有効期限) 現在のセッションがまだ有効かどうかをチェックし、無効な場合はセッションからログアウトします。
verified_account?
(許容期間の確認の延長) 現在のアカウントが(訳注: メールなどで)確認済みかどうかを返します。falseの場合、ユーザーが「許容期間」に該当しているためにログインを許されていることを示します。
locked_out?
(ロックアウト機能) 現在のセッションのユーザーがロックアウトされているかどうかを返します。

複数の設定を使う

Rodauthでは、同じアプリケーションで複数のrodauth設定の利用をサポートしています。これは、プラグインを読み込んで2度目のログインで別設定の名前を指定するだけで行なえます。

plugin :rodauth do
end
plugin :rodauth, :name=>:secondary do
end

その後は、いつでもルーティングでrodauthを呼び、使いたい設定名を引数で指定できるようになります。

route do |r|
  r.on 'secondary' do
    r.rodauth(:secondary)
  end

  r.rodauth
end

パスワードハッシュをアカウントのテーブルに保存する

Rodauthでは、パスワードハッシュをアカウントと同じテーブルに保存することもできます。これは、パスワードハッシュを保存するカラムを指定するだけで行なえます。

plugin :rodauth do
  account_password_hash_column :password_hash
end

Rodauthでこのオプションを設定すると、パスワードハッシュのチェックをRubyで行うようになります。

PostgreSQL/MySQL/Microsoft SQL Serverでデータベース関数を使わないようにする

RodauthとPostgreSQL/MySQL/Microsoft SQL Serverで、認証用のデータベース関数を使いたくないがハッシュテーブルは従来どおり別テーブルに保存したい場合は、次のようにします。

plugin :rodauth do
  use_database_authentication_functions? false
end

言い換えると、rodauth_get_salt関数とrodauth_valid_password_hash関数を独自に実装すれば、PostgreSQL/MySQL/Microsoft SQL Server以外のデータベースでもこの値をtrueにできます。

認証をカスタマイズする

Rodauthの設定用メソッドの中には、他の種類の認証方法を使えるようにできるものもあります。

認証をカスタマイズすると、ログインの変更やパスワードの変更などのRodauthの機能の使い方がわからなくなったり、カスタム設定を追加する必要が生じたりするかもしれません。ただし以下のカスタマイズ例では、ログイン機能とログアウト機能は正常に機能します。

  • LDAP認証を使う

アカウントがデータベースに保存されている状態でLDAP認証したい場合は、simple_ldap_authenticatorライブラリを利用できます。

require 'simple_ldap_authenticator'
plugin :rodauth do
  enable :login, :logout
  require_bcrypt? false
  password_match? do |password|
    SimpleLdapAuthenticator.valid?(account.username, password)
  end
end

データベースにアカウントがない状態でLDAPの有効なユーザーがログインできるようにしたい場合は、次のようにします。

require 'simple_ldap_authenticator'
plugin :rodauth do
  enable :login, :logout

  # LDAPで認証するのでbcryptライブラリをrequireしない
  require_bcrypt? false

  # セッションの値を:loginキーに保存する
  # (デフォルトの:account_idキーだとわかりにくいため)
  session_key :login

  # セッションの値で与えられたログインを使う
  account_session_value{account}

  # このログインそのものをアカウントとして使う
  account_from_login{|l| l.to_s}

  password_match? do |password|
    SimpleLdapAuthenticator.valid?(account, password)
  end
end
  • Facebook認証を使う

JSON APIでのFacebook認証の例を以下に示します。この設定では、クライアント側にJSONでPOSTリクエストを送信するコードがあることが前提です。このPOSTリクエストは/loginに送信され、FacebookでユーザーのOAuthアクセストークンを設定するaccess_tokenパラメータを含むとします。

 require 'koala'
 plugin :rodauth do
  enable :login, :logout, :jwt

  require_bcrypt? false
  session_key :facebook_email
  account_session_value{account}

  login_param 'access_token'

  account_from_login do |access_token|
    fb = Koala::Facebook::API.new(access_token)
    if me = fb.get_object('me', :fields=>[:email])
      me['email']
    end
  end

  # there is no password!
  password_match? do |pass|
    true
  end
end
  • その他のWebフレームワーク

Rodauthは、アプリケーションでRoda Webフレームワークが使われていなくても利用できます。これは、Rodauthを使うRodaミドルウェアを追加することで行なえます。

require 'roda'

class RodauthApp < Roda
  plugin :middleware
  plugin :rodauth do
    enable :login
  end

  route do |r|
    r.rodauth
    rodauth.require_authentication
    env['rodauth'] = rodauth
  end
end

use RodauthApp

RodauthはRodaアプリに対し、Rodaがレイアウト提供の目的で使われることを期待します。そのため、Rodauthを他のアプリ用のミドルウェアとして使う場合、Rodauthから使えるviews/layout.erbファイルがないのであれば、おそらくRodaのrenderプラグインの追加も必要になります。その場合、Rodauthがアプリと同じレイアウトを使えるようプラグインを適切に設定する必要もあるでしょう

ミドルウェア内部のルーティングブロックでenv['rodauth'] = rodauthを設定すると、Rodauthメソッドを簡単に呼び出せる方法をアプリに導入できるようになります。

Rodaを使わないアプリでのRodauth導入例をいくつか示します。

Rodauthでは、TOTP(Time-Based One-Time Passwords: RFC 6238)経由での2要素認証を使えます。Rodauthで2要素認証をアプリに統合する方法は、アプリでの必要に応じてさまざまなものがあります。

2要素認証のサポートはOTP機能の一部なので、ログイン機能に加えてOTP機能も有効にする必要があります。一般に、2要素認証を実装する場合は2要素認証を2種類用意し、プライマリの2要素認証が利用できない場合にセカンダリの2要素認証を提供するべきです。RodauthではSMSコードとリカバリーコードをセカンダリ2要素認証としてサポートします。

アプリで2要素認証をサポートし、かつ2要素認証を必須にしたくない場合は次のようにします。

plugin :rodauth do
  enable :login, :logout, :otp, :recovery_codes, :sms_codes
end
route do |r|
  r.rodauth
  rodauth.require_authentication

  # ...
end

OTP認証を全ユーザーで必須にし、アカウントを持っていないユーザーに対してOTP認証の設定を要求する場合の設定です。

route do |r|
  r.rodauth
  rodauth.require_authentication
  rodauth.require_two_factor_authentication_setup

  # ...
end

認証を必須にする場合の一般的な方法と同様に、特定のブランチでのみ2要素認証を必須にし、サイトの他の場所ではログイン認証を必須することもできます。

route do |r|
  r.rodauth
  rodauth.require_login

  r.on "admin" do
    rodauth.require_two_factor_authenticated
  end

  # ...
end

JSON APIサポート

プラグインに:jsonオプションを渡してJWT機能を有効にすると、JSONレスポンス取り扱いのサポートを追加できます。

plugin :rodauth, :json=>true do
  enable :login, :logout, :jwt
end

JSON APIをビルドするのであれば、:json => :onlyを渡すことでRodauthで通常読み込まれるHTML関連のプラグイン(render、csrf、flash、h)を読み込まないようにできます。

plugin :rodauth, :json=>:only do
  enable :login, :logout, :jwt
end

ただし、メール送信機能はデフォルトでrenderプラグインに依存していることにご注意ください。:json=>:onlyを使う場合は、renderプラグインを手動で読み込むか、*_email_body設定オプションでメールの本文を指定する必要があります。

JWT機能を導入すると、Rodauthに含まれるその他のJSON APIサポートもすべて利用できるようになります。

rodauthオブジェクトにカスタムメソッドを追加する

設定のブロック内でauth_class_evalを使うと、rodauth`オブジェクトから呼び出せるカスタムメソッドを追加できます。

plugin :rodauth do
  enable :login

  auth_class_eval do
    def require_admin
      request.redirect("/") unless account[:admin]
    end
  end
end

route do |r|
  r.rodauth

  r.on "admin" do
    rodauth.require_admin
  end
end

外部の機能を使う

有効にする設定メソッドは、Rodauthの外部にある機能を読み込めます。この外部機能のファイルは、rodauth/features/feature_nameからrequireできるディレクトリに置く必要があります。このファイルは以下の基本構造をとる必要があります。

module Rodauth
  # :feature_nameは、有効にしたい機能を指定する引数
  # :FeatureNameはオプションで、inspect出力を読みやすくする定数名を設定するのに使う
  Feature.define(:feature_name, :FeatureName) do
    # 認証値系メソッドを固定値で定義するショートカット
    auth_value_method :method_name, 1 # method_value

    auth_value_methods # 認証値メソッドごとに1つの引数

    auth_methods       # 認証メソッドごとに1つの引数

    route do |r|
      # この機能のルーティングへのリクエストをこのブロックで受ける
      # ブロックはRodauth::Authインスタンスのスコープで評価される
      # rはリクエストのRoda::RodaRequestインスタンス

      r.get do
      end

      r.post do
      end
    end

    configuration_eval do
      # メソッド固有の追加設定を必要に応じてここで定義する
    end

    # auth_methodsとauth_value_methodsのデフォルトの挙動を定義する
    # ...
  end
end

機能の構成方法の例については、Rodauthの機能のコードを参照してください。

ルーティングレベルの挙動をオーバーライドする

Rodauthのすべての設定メソッドは、Rodauth::Authインスタンスの挙動を変更します。しかし場合によってはルーティング層の扱いをオーバーライドしたくなることもあります。これは、r.rodauthを呼び出す前に以下のように適切なルーティングを追加するだけで簡単に行なえます。

route do |r|
  r.post 'login' do
    # ここにカスタム POST /login ハンドリングを記述する
  end

  r.rodauth
end

Rodauthテンプレートをプリコンパイルする

Rodauthは自分自身のgemフォルダにあるテンプレートを使います。fork型のWebサーバーを使っていて、コンパイル済みテンプレートを事前に読み込んでメモリを節約したい場合や、アプリをchrootしたい場合は、Rodauthのテンプレートをプリコンパイルすることでメリットを得られます。

plugin :rodauth do
  # ...
end
precompile_rodauth_templates

0.9.xからのアップグレード

Rodauthを0.9.xから現在のバージョンにアップグレードする場合の注意点です。

account_valid_passwordデータベース関数を使っていた場合はこれを削除し、上のマイグレーションに記載されている2つのデータベース関数を追加する必要があります。以下のコードをマイグレーションに追加することでこの作業を行えます。

require 'rodauth/migrations'
run "DROP FUNCTION account_valid_password(int8, text);"
Rodauth.create_database_authentication_functions(self)
run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO ${DATABASE_NAME}"
run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO ${DATABASE_NAME}"

類似のプロジェクト

以下はすべてRailsに特化しています。

  • Devise
  • Authlogic
  • Sorcery

著者

Jeremy Evans (code@jeremyevans.net

関連記事

Railsアプリの認証システムをセキュアにする4つの方法(翻訳)

[Rails] Devise Wiki日本語もくじ1「ワークフローのカスタマイズ」(概要・用途付き)

週刊Railsウォッチ(20171117)Rails開発3年分のコツ集大成、PostgreSQL 10.1でセキュリティ問題修正ほか

$
0
0

こんにちは、hachi8833です。Firefox Quantumで泣きましたか?私は拡張使ってませんでした。

11月中旬のウォッチ、いってみましょう。

Rails: 今週の改修

今週の公式更新情報はありません。

タイムゾーンがあいまいになることがある問題を修正

# 修正前
"2014-10-26 01:00:00".in_time_zone("Moscow")
#=> TZInfo::AmbiguousTime: 26/10/2014 01:00 is an ambiguous local time.

# 修正後
"2014-10-26 01:00:00".in_time_zone("Moscow")
#=> Sun, 26 Oct 2014 01:00:00 MSK +03:00
# activesupport/lib/active_support/values/time_zone.rb
     def period_for_local(time, dst = true)
-      tzinfo.period_for_local(time, dst)
+      tzinfo.period_for_local(time, dst) { |periods| periods.last }
     end

ほとんど何も説明がありません。普段はしないのですが、y-yagiさんブログをこっそりチェックすると以下のように書かれています。Rubyの挙動に合わせてActiveSupportが修正されたとのことです。

Europe/Moscowのようにタイムゾーンが複数ある値を指定した場合に、TZInfo::AmbiguousTimeが発生していたのを、発生しないよう修正しています。

「同じ地域に複数のタイムゾーンがある」というのがよくわからなかったので、#31128で修正されたという#17395を見ると、「最近(2014年)ロシアのタイムゾーン変更がtzinfoに反映された」とあります。さらに調べると、Time-j.netで以下の記述を見つけました。

ロシアのタイムゾーンは2014年10月26日(日)から以下の図のようにUTC+2 ~ UTC+12 の11個のタイムゾーンになりました。世界一広い国だけあって国内の最大の時差が10時間あります。
サマータイムについては、実施していません。2010年までは実施していましたが、2011年にサマータイムを標準時間としてサマータイムを廃止し、2014年10月26日(日)に本来の標準時間に戻りました。
2016年には、以下の地域で1時間時計の針を進めるタイムゾーンの変更がありました。

www.time-j.netより

そしてtimeanddate.comで、上のソースと同じ日付の2014/10/26 2:00に変更が行われていることがわかりました。このエッジケースに対応したということのようです。

ロシアのタイムゾーンが近年こんなにガッツンガッツン変更されているとは知りませんでした。ロシア国民とライブラリメンテナの苦労が伺えます。
プーチン、無茶しよるのう…

rails newでできる.gitignoreにmaster keyが含まれていなかったのを修正

# railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb

+      def ignore_key_file_silently(key_path, ignore: key_ignore(key_path))
+        append_to_file ".gitignore", ignore if File.exist?(".gitignore")
+      end

つっつきボイス: 「おっとっと、気をつけないとmaster keyをリポジトリに突っ込んじゃうやつだ」

Rails

3年かけて培ったRails開発のコツ


blog.kollegorna.seより

たくさん載ってます。

  • トップレベルのコントローラではrescue_fromを使う
  • コントローラではload_resourceを使う
  • ほか続々

つっつきボイス: 「よく使うテクニックもいろいろありそうなので、ざっと見ておいて損はなさそう」「これ翻訳しますね」

Rack DeflateをオンにしてRailsのページサイズを80%削減(Ruby Weeklyより)


schneems.comより

Railsの設定にRack::Deflateを追加することで100msほど高速化したそうです。

# 同記事より
config.middleware.insert_after ActionDispatch::Static, Rack::Deflater

つっつきボイス: 「要するにzipしてるってことね」

なぜService Objectが思ったほど普及しないのか(Ruby Weeklyより)


aaronlasseigne.comより

この記事は、先週のウォッチでご紹介したEnough With the Service Objects Already(Service Objectにちょっとうんざり)に答える内容です。


つっつきボイス: 「確かにこのコード先週も見たなー↓」「『彼の主張には一理あるかもしれない』だそうです」「Service Objectは使いみちが広い分、増えたときに設計がぐらつかないようにしないといけないし、気をつけないとYAGNIになる可能性もあるかも」

# 同記事より
class IpnProcessor
  def process_ipn(...)
    # controller code here
  end
end

SprocketからWebpackに移行する方法(Ruby Weeklyより)


rossta.netより

手順が具体的でよさそうです。


つっつきボイス: 「Webpack使う機会がまだなかった…」「Webpackいいですよー」「そうそう、Webpack使うとjavascript_pack_tagが挿入される↓」

<!-- application.html.erb -->

<html>
    <body>
        <!-- ... -->
        <%= javascript_pack_tag 'vendor' %>
        <%= javascript_include_tag 'vendor' %>

        <%= javascript_pack_tag 'application' %>
        <%= javascript_include_tag 'application' %>
    </body>
</html>

:before_validateコールバックの変わった使い方(Ruby Weeklyより)


karolgalanciak.comより

# 同記事より
class MyModel
  before_validate :strip_url

  private

  def strip_url
    self.url = url.to_s.strip
  end
end

つっつきボイス::before_validateは一見バッドプラクティスに見えたりするけど、データのフォーマットはバリデーション前にやらないと意味がないこともある」

この間翻訳させていただいた記事「RailsのObject#tryがダメな理由と効果的な代替手段」の著者でもあります。

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

Ruby/Railsプログラミングでよくある5つの間違い


business2community.comより

  • method_missingの使いすぎ
  • gemに頼りすぎ
  • アプリのロジックがビューに漏出する
  • 「コントローラを薄くする」ことにこだわりすぎる
  • SQLインジェクションを放置する

つっつきボイス:method_missingって、Railsアプリではまず書かないかな: gem作るとかフレームワーク作るならともかく」「アプリのロジックがビューに漏れるというのも、このぐらいだったら許容範囲なんじゃないかなー↓: 程度問題だけど」

# 同記事より
<h2>
Congratulations
<% if winning_player %>
<%= winning_player.name %>
<% else %>
Contestant
<% end %>
</h2>

「DHHが薄いコントローラを推してるんですよね」「ファットコントローラは確かによくないけど」

あなたが知らないかもしれないRailsの5つのコツ

# 同記事より
params[:sort].presence_in(sort_options) || :by_date

つっつきボイス:Object#presence_inは知らなかったナー」「後はおなじみといえばおなじみかな」

テストは簡潔に書くべきか言葉をつくすべきか

# 元記事より
# アプローチ1
describe '#[]' do
  subject { [1, 2, 3].method(:[]) }

  its_call(0) { is_expected.to ret 1 }
  its_call(1..-1) { is_expected.to ret [2, 3] }
  its_call(:b) { is_expected.to raise_error TypeError }
end

# アプローチ2
describe '#[]' do
  subject { [1, 2, 3] }

  it 'returns the value at a given index' do
    expect(subject[0]).to eq 1
  end
  it 'returns a list of values between a range on indexes' do
    expect(subject[1..-1]).to eq [2, 3]
  end
  it 'raises TypeError when passed a symbol' do
    expect { subject[:b] }.to raise_error TypeError
  end
end

つっつきボイス: 「う、実はアプローチ1って個人的には好き」「テストでエラーになったときに、テストを修正すべきなのかコードを修正すべきなのかわからないと困っちゃう」「普通にアプローチ2かな」
「これと直接関係ないけど、過去の経緯でプロジェクトによってテストの書き方が大きく違ってたりすると大変」「its_callなんてのがあるのか」「自分はlet好きなのでlet使う派」「私はletキライ」「お、派閥が分かれてるんですね」

Rubyで「Interactor」パターン

# 同記事より

# DeleteAccount interactor
class DeleteAccount
  include Interactor
  def call
  end
end

# Controller
def destroy
  account = Account.find(params[:id])
  DeleteAccount.call(account: account) # pass whatever you want as a hash
end

つっつきボイス: 「InteractorパターンってFacadeみたいなものなのかな?」「#call使いまくるあたりがそんな感じですね」

以下の記事でもInteractor gemが取り上げられています。

Railsコードを改善する7つの素敵なGem(翻訳)

parallel_tests: テストを並列化するgem

RSpec、Test::Unit、Cucumberのテストを並列化できるそうです。

# grosser/parallel_testsより
rake parallel:test          # Test::Unit
rake parallel:spec          # RSpec
rake parallel:features      # Cucumber
rake parallel:features-spinach       # Spinach

rake parallel:test[1] --> force 1 CPU --> 86 seconds
rake parallel:test    --> got 2 CPUs? --> 47 seconds
rake parallel:test    --> got 4 CPUs? --> 26 seconds
...

つっつきボイス: 「これ使った方います?」「一応データベースもスレッド化してくれるようです: ただ、案件で追加してみたときはテスト動かなくなったんで外しました」「もしかしてテストの方に問題があったのかもw」「プロジェクトの初期段階から使うのがよさそう」

multiverse: Railsで複数のデータベースを扱うgem(Ruby Weeklyより)

ankaneさんの作です。半月足らずで★200超えてます。


つっつきボイス: 「データベース間でJOINできます?」「使ってみないとわかりませんが、さすがに無理かなー」

data-migrate: スキーマの他にデータもマイグレーションするgem

これも半月足らずで★300近くあります。

$> rake -T data
rake data:forward                 # Pushes the schema to the next version (specify steps w/ STEP=n)
rake data:migrate                 # Migrate data migrations (options: VERSION=x, VERBOSE=false)
rake data:migrate:down            # Runs the "down" for a given migration VERSION
rake data:migrate:redo            # Rollbacks the database one migration and re migrate up (options: STEP=x, VERSIO...
rake data:migrate:status          # Display status of data migrations
rake data:migrate:up              # Runs the "up" for a given migration VERSION
rake data:rollback                # Rolls the schema back to the previous version (specify steps w/ STEP=n)
rake data:version                 # Retrieves the current schema version number for data migrations
rake db:forward:with_data         # Pushes the schema to the next version (specify steps w/ STEP=n)
rake db:migrate:down:with_data    # Runs the "down" for a given migration VERSION
rake db:migrate:redo:with_data    # Rollbacks the database one migration and re migrate up (options: STEP=x, VERSIO...
rake db:migrate:status:with_data  # Display status of data and schema migrations
rake db:migrate:up:with_data      # Runs the "up" for a given migration VERSION
rake db:migrate:with_data         # Migrate the database data and schema (options: VERSION=x, VERBOSE=false)
rake db:rollback:with_data        # Rolls the schema back to the previous version (specify steps w/ STEP=n)
rake db:version:with_data         # Retrieves the current schema version numbers for data and schema migrations

つっつきボイス:rake db:migrate:down:with_dataのような感じで使うのね」

webmock: HTTPリクエストのモック/expectation gem

リクエストのheaderやbodyを細かに指定できます。★2600超えです。

require 'webmock/rspec'

expect(WebMock).to have_requested(:get, "www.example.com").
  with(body: "abc", headers: {'Content-Length' => 3}).twice

expect(WebMock).not_to have_requested(:get, "www.something.com")

expect(WebMock).to have_requested(:post, "www.example.com").
  with { |req| req.body == "abc" }
# Note that the block with `do ... end` instead of curly brackets won't work!
# Why? See this comment https://github.com/bblimke/webmock/issues/174#issuecomment-34908908

expect(WebMock).to have_requested(:get, "www.example.com").
  with(query: {"a" => ["b", "c"]})

expect(WebMock).to have_requested(:get, "www.example.com").
  with(query: hash_including({"a" => ["b", "c"]}))

expect(WebMock).to have_requested(:get, "www.example.com").
  with(body: {"a" => ["b", "c"]},
    headers: {'Content-Type' => 'application/json'})

Grill.rb: バーベキューしながらRubyカンファレンス

今年の7月にポーランドで開催されたカンファレンスです。

蚊に食われないかなと心配になってしまいます。

grill rbさん(@grill.rb)がシェアした投稿

ReactやRailsで作った28のアプリリスト(ソース付き)

作者はさまざまです。アプリ開発のヒントにしたり、作りたいアプリが思いつかない学生さんとかにもよいかもしれません。

github-awesome-autocompleteはちょっと便利そう。


github.algolia.comより

Ruby trunkより

提案: ArgumentErrorにメソッドのプロトタイプを表示

[4] pry(main)> Kerk.new.foo1
ArgumentError: wrong number of arguments (0 for 1)
Method prototype:
    def foo1(a)
from /home/esjee/src/printprototype/spec/kerk_class.rb:2:in `foo1'

つっつきボイス: 「ノンジャパニーズの方が日本語で書いているところにほだされてしまいました」「エラーにプロトタイプって必要かしらん」
「sentry-ravenって何だろ?」「raven-rubysentry.ioというエラーレポート集約サイトがありますね」


sentry.ioより

Ruby

ベテランRubyistならPythonコードを5倍速くできることもある


schneems.comより

Rubyに精通していればPythonでも同じ考えがいろいろ通用するという主旨です。Richard Schneemanさんは怒涛のように濃い記事を書いてますね。

Total time: 1.17439 s
File: perf.py
Function: get_legal_moves_fast at line 53

Total time: 5.80368 s
File: perf.py
Function: get_legal_moves_slow at line 69

ガイジン向けRubyKaigiガイド(翻訳)

requireの仕組み(Ruby Weeklyより)


ryanbigg.comより

とても短い記事です。active_support/allというファイルがないのにrequireできる理由を解説しています。
作者のRyan Biggさんは2011年にRuby Heroを受賞しています。

Railsの`CurrentAttributes`は有害である(翻訳)

一味違う方法でRubyのパフォーマンスをプロファイリング(Ruby Weeklyより)


kollegorna.seより

ruby-prof-flamegraphというgemを援用して次のようなflame graphを生成する記事です。flame graphという呼び名を初めて知りました。関係ありませんが、最近medium.comでよい記事を見かけることが多い気がします。


kollegorna.seより


つっつきボイス: 「このグラフどうやって読むの?」「一番下がThreadだから、呼び出しが上に進んでいる感じですね」

wsdirector: WebSocketをCLIとyamlで操作するgem(Ruby Weeklyより)

yamlでWebSocketのやり取りを書いて実行できます。

  # script.yml
  - client: # first clients group
      name: "publisher" # optional group name
      multiplier: ":scale" # :scale take number from -s param, and run :scale number of clients in this group
      actions: #
        - receive:
            data: "Welcome"
        - wait_all # makes all clients in all groups wait untill every client get this point (global barrier)
        - send:
            data: "test message"
  - client:
      name: "listeners"
      multiplier: ":scale * 2"
      actions:
        - receive:
            data: "Welcome"
        - wait_all
        - receive:
            multiplier: ":scale" # you can use multiplier with any action
            data: "test message"
wsdirector script.yml ws://websocket.server:9876 -s 10

#=> Group publisher: 10 clients, 0 failures
#=> Group listeners: 20 clients, 0 failures

つっつきボイス: 「テスト用なのかな」「そういえばWebSocketまだ使ったことなかったナ」

なおスポンサーはEvil Martiansです。同社ブログの以下の記事を翻訳させていただきました。

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

ぼっち演算子&.の落とし穴


antulik.comより

これもとても短い記事です。

# 同記事より
# 落とし穴を踏んだコード
if start_date &.< start_of_month && end_date.nil?
  # …
end

つっつきボイス: 「演算子結合の優先順位の罠か」「(メソッドチェーンかと思ったら違った…)」

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

PB MEMO: Rubyコミットを追いかけるブログ

Railsコミットをひたすら追うy-yagiさんのなるようになるブログのように、このブログではRubyのコミットをひたすら追いかけています。頭が下がります。

Rubyのインスタンス変数とアクセス制御

Rubyのインスタンス変数とアクセサの関係がちょっとモヤモヤしてたので貼ってみました。


つっつきボイス: 「dispだとdisplayしか思いつかないw」「attr_accessorでアクセサを作ると、代入時に同じ名前のインスタンス変数が作成されるという理解」
protectedはJavaと同じに考えるとハマるやつですな」「access controlとして作ってないんだとすると、どんな意図で作られたんだろう…」


追記: Junichi Itoさんの英語ブログ「Matz answers why Ruby lets sub classes to access private methods in super class」がわかりやすいです。

(動画)MatzとRuby 3.0について語る

30分の動画です。駆け足で聞いてみたところ、最初と最後はよもやま話で、12:02あたりからがRuby 3.0の話でした。「パフォーマンス」「concurrency」「型」の3つのコンセプトについて語っています。


つっつきボイス: 「ここ駐車場かな?」「いや、運転してるし」

SQL

PostgreSQL 10の記事が続々出ています。

PostgreSQL 10.1などリリース: セキュリティ問題修正(Postgres Weeklyより)


postgresql.orgより

バージョン9以前に影響するものもあります。

  • CVE-2017-12172: 初期化スクリプトに権限昇格の脆弱性
  • CVE-2017-15098: JSON関数でサーバーのメモリの一部が露出
  • CVE-2017-15099: INSERT ... ON CONFLICT DO UPDATEすると権限無しでSELECTできる

pglogical拡張でPostgreSQL 9.6から10にダウンタイム最小限で移行する(Postgres Weeklyより)


rosenfeld.herokuapp.comより

PostgreSQL 10のテーブル継承と宣言的パーティショニングでスケールする(Postgres Weeklyより)


timescale.comより

PostgreSQLのパーティショニングされたテーブルを10のネイティブパーティショニングに移行する(Postgres Weeklyより)


openscg.comより

PostgreSQL 11の機能を先行紹介するブログ


depesz.comより


つっつきボイス: 「何と気が早い」

JavaScript

webpack-bundle-size-analyzer: Webpackのインストール内訳を分析

Webpackでインストールされたライブラリをこんな感じで表示できます。


github.com/robertknight/webpack-bundle-size-analyzerより

CSS/HTML/フロントエンド

Unicodeについて知っておくべき5つのこと


gojko.netより

  • 画面に表示されないUnicodeポイントはたくさんある
  • 見た目が互いにそっくりなUnicodeポイントはたくさんある
  • 正規化(normalization)はそんなに簡単な話じゃない
  • 表示の長さとメモリのサイズは同じとは限らない
  • Unicodeは単なる静的なデータではない

つっつきボイス: 「右の4つから左の合字を作れる話を思い出しました↓」


http://unicode.org/emoji/charts/emoji-zwj-sequences.htmlより

Source Mapとは何か(Ruby Weeklyより)

// 同記事より
{
  "version":3,
  "file":"application.js",
  "mappings": "AAAA;AACA;AACA;#...",
    "sources": [
      "jquery.source-56e843a66b2bf7188ac2f4c81df61608843ce144bd5aa66c2df4783fba85e8ef.js",
      "jquery_ujs.source-e87806d0cf4489aeb1bb7288016024e8de67fd18db693fe026fe3907581e53cd.js",
      "local-time.source-b04c907dd31a0e26964f63c82418cbee05740c63015392ea4eb7a071a86866ab.js"
    ],
    "names":[]
}

Firefox Quantum(57)から拡張機能はWebExtensionのみになる

Firefox 57リリース後にBPS社内でも小さな悲鳴がいくつか上がっていました。


つっつきボイス: 「私も拡張全滅しましたorz」

WebAssemblyが主要なブラウザでサポート(Frontend Weeklyより)


blog.mozilla.orgより

FirefoxとChromeに続き、SafariとEdgeでもWebAssemblyがサポートされたとのことです。


つっつきボイス: 「Aaron PattersonさんがWebAssemblyに興味持ってると言ってたのを思い出しました: Rubyで動くかしら」「機械語になるから難しそうですね」

dev.toの激速が話題

記事そのものもよさそうです。


つっつきボイス:CDNの効果が大きいのかな」

その他

スレッドとは何か

10年間見逃されていたmanコマンドの脆弱性


sudosatirical.comより

バージョンアップできないAndroid端末の台数をグラフ化

danluu.comより

番外

無電力パワードスーツで工場の事故や怪我を大きく削減


今週は以上です。

バックナンバー(2017年度)

週刊Railsウォッチ(20171110)dry-rbでFormObjectを作る、RailsのSQLインジェクション手法サイト、年に1度だけ起きるバグほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

3年以上かけて培ったRails開発のコツ集大成(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

3年以上かけて培ったRails開発のコツ集大成(翻訳)

順序は特に決まっていません。

1. トップレベルにrescue_fromを書く(permalink

ルートのコントローラにrescue_fromを書くと、その下で発生したすべての例外をキャッチできるので非常に便利です。Webアプリにこれを追加すると、リクエスト/レスポンスのサイクルで実行されるほとんどのコードが一気に便利になります。シンプルなAPIを例に考えます。rescue_fromを使えば、レコードが見つからない(ActiveRecordがActiveRecord::RecordNotFoundをスローする)場合のアプリの振る舞いを明示的に指定できます。

rescue_from ActiveRecord::RecordNotFound do
  api_error(status: 404, errors: 'Resource not found!')
end

2. コントローラにload_resourceを書く(permalink

もうひとつのパターンは、以前同僚が使っていたのを見て以来採用しているものです。必要なリソースのフェッチをコントローラのメソッド内で行う代わりに、共通のコントローラのフィルタを使い、アクションの実行に応じてフェッチするというものです。

class UsersController
  before_action :load_resource

  def index
    # @usersで何かする
  end

  def show
    # @userで何かする
  end

  def create
    # @userで何かする
  end

  def update
    # @userで何かする
  end

  def destroy
    # @userで何かする
  end

  private
    def load_resource
      case params[:action].to_sym
      when :index
        @users = paginate(apply_filters(User.all, params))
      when :create
        @user = User.new(create_params)
      when :show, :update, :destroy
        @user = User.find(params[:id])
      end
    end
end

これの発展版がdecent_exposureです。私自身はまだ使う機会がありませんが。

ところで、私は主に2つの理由から「よいコードは常にそれ自身が語る」(訳注: コメントを避けてコードに語らせる)という言説にあまり賛成できません。

  • ある開発者にとってよいコードであっても、別の開発者にとっては悪いコードになることもあります(スタイルは人それぞれなので、それ自体が悪いのではありません)。
  • 時間や予算の制約から、手早く修正してissueをクローズするしかないという状況はいくらでもありえます。最善の(そして最も自明な)ソリューションは「10倍努力する」ということがわかっていてもですになるかもしれないためです。

というわけで、「コードが匂ってるな」と思ったら、恥ずかしがらずにどしどしコメントしましょう😃

3. decoratorやpresenterを使う(permalink

しばらく前から、「モデルをファットにして、その分コントローラを薄くせよ」という言説をRailsコミュニティで見かけます。「コントローラを薄くせよ」については同意しますが、ファットモデルについては同意できません😃。モデルもできるだけ薄くするべきであり、特殊の場合にしか使わないようなプロパティをモデルで自動生成しないことです。そのような場合はラッパークラスを使って(皆さん、これがdecoratorですよ!)必要なメソッドだけを公開しましょう。

presenterはdecoratorと似ていますが、複数のモデルを扱う点だけが異なります。

4. モデル配下のワーカーを名前空間化してafter_commitで呼び出す(permalink

Userというモデルがあるとしましょう。あるモデルに関連するバックグラウンドジョブの90%は、モデルの作成/更新/削除で発生します。ここでデータが変更されるからです。ここから、User::CreateWorkerUser::UpdateWorkerUser::DestroyWorkerという3つの一般的なワーカーを導き出せます。利用可能な場合にはこれらのワーカーをActiveRecordコールバックやprevious_changesと組み合わせて使ってみましょう。ワーカーの呼び出しはafter_commitで行います。理由についてはこちらをご覧ください。

5. PostgreSQLのarrayは、よほどシンプルでない限り使わないこと(permalink

PostgreSQLのarrayもクールなのですが、私の経験では、時間を節約するより問題を生み出す方が多くなります。PostgreSQLのarrayを使うと(何らかのIDを保存するなど)、後でそのテーブルを見たときに必ず私の頭が爆発しました。データベースにはJOINというものがあるのですから、テーブルを追加するコストは高くありません。

PostgreSQLのarrayは、うんと小規模な場合にしか使わないことにします。

  • テーブルに保存する要素が少数にとどまり、かつ要素の平均個数が将来増加しないことがわかっている場合(わずかな変動ならありです)
  • テーブルがIDや関連付けと一切関わりを持たないことがわかっている場合

6. Postgres JSONBはいいヤツ(permalink

PostgreSQLのarrayとは対照的に、PostgreSQLのJSONBは大好きです。データベースにスキーマがあることのメリットは明らかなので、私たちは皆スキーマを持つデータベースが大好きです。しかしながら、スキーマを事前に予測できない場合、スキーマレスデータベースのシンプルさがどうしても必要になることがあります。私は次のような場合にJSONBを使うことがよくあります。

  • 小さな属性を多数使うことがあり、しかも親属性で名前空間される可能性もある場合。普通のテーブルでこれをやると、カラムだらけになってしまいます。
  • 保存する内容が正確にわからない場合や、プロトタイプを急いで作る場合
  • オブジェクトのhydrationを作る場合: オブジェクトをJSON形式でデータベースに保存し、同じJSONからオブジェクトを再構成する

訳注: hydrationは化学用語の「水和/水和物」の借用で、シリアライズに少し似た概念です。

7. aasm gemはいいヤツ、ただしステートを変えて初期化しないこと(permalink

私はaasm gemが大好きです。ステートマシンで状態や操作を強制することができ、専用のきわめてシンプルなDSLを使えます。ただし、オブジェクトを初期状態と異なるステートで作成するとフックが動作しないという問題が生じます。aasmの内部状態とにらめっこして頑張るか、あきらめてオブジェクトの特定のステートを手動でスキャンすることになります(それ用のサービスを作ったりとか)。

8 .メールアドレスのバリデーションを頑張るよりgemを使う(permalink

メールアドレスのバリデーションに使う正規表現をググると毎回違う正規表現が見つかるのは、もう笑うしかありません。完璧な正規表現を探すのはとっととあきらめて、おとなしくgemを使うに限ります。

9. decoratorやpresenterを使って、ビューに渡すインスタンス変数をなるべく1つだけにする(permalink

私がRailsで残念に思っている部分です。コントローラからビューにコンテキストを渡すのにインスタンス変数をいくつも使うのは、バッドプラクティスだと思います。Sandi Metzの言うとおり、インスタンス化して渡すオブジェクトは常に1つだけにすべきです。

10. モデルに保存するインスタンスメソッドに!を付ける(permalink

モデルのメソッドがオブジェクトを変更してデータベースに保存する場合、メソッド名の末尾に必ず!を付けてそのことを示しましょう。簡単なことです。

クラスレベルで厳密なAPIを書くことがコードの品質を高めることにつながりますが、開発者はそのことを忘れがちです(私もですね!)。

11. 単に認証したい場合はDevise gemを使わないこと(permalink

Deviseはマジックが多すぎます。

12. Virtusを使って、ActiveRecordでないモデルの属性をより厳密に定義する(permalink

私はVirtus gemを多用していましたし、今も使っています。シンプルなPORO(素のRuby: Pure Old Ruby Object)でモデルのように振る舞うオブジェクトを構成でき、属性をある程度厳密に保つこともできます。私は、属性が増えすぎたときに次のようなVirtus向けの独自DSLを書いて属性を操作できるようにすることがよくあります。

# シリアライザなどに定義した属性を再利用できるシンプルなモジュール
module VirtusModel
  extend ActiveSupport::Concern

  included do
    include Virtus.model

    if defined?(self::ATTRIBUTES)
      self::ATTRIBUTES.each do |group|
        group[:attrs].each do |attr|
          attribute(attr, group[:type])
        end
      end
    end
  end

  class_methods do
    def all_attributes
      self::ATTRIBUTES.map{|i| i[:attrs]}.flatten
    end
  end
end
# モデルの例
class Model < ActiveModelSerializers::Model
  ATTRIBUTES = [
    {
      attrs: [
        :id, :name, :header_text, :is_visible, :filtering_control,
        :data_type, :description, :caregory, :calculation
      ],
      type: String
    },
    {
      attrs: [
        :display_index, :min_value, :max_value, :value_type,
        :number_of_forcast_years
    ],
      type: Integer
    },
    {
      attrs: [:category], type: Array
    },
    {
      attrs: [:is_multi_select],
      type: Virtus::Attribute::Boolean
    }
  ].freeze

  include VirtusModel
end

さまざまな属性の種類を列挙することも、属性のグループにある種のタグを追加することもできます。おかげで私はニッコニコです😃

なお、Railsのattributes APIができたので、これで同じか似たようなことができるのではないかと考えています😃

13. 外部API参照などの重たい処理にはメモ化(memoization)を使う(permalink

もうおわかりですよね😃

14. PostgreSQL全文検索は単純な用途に向いている(permalink

pg_searchは驚くほど簡単にセットアップできます。tvectorsなどでPostgreSQL全文検索を最適化しなければならない場合は、素直にElasticSearchを使いましょう。PostgreSQLでそれ以上時間をかけるのは無駄です。

15. 2017年にもなって未だにService Objectとは何かがちゃんと定義されていない(permalink

多くの人が同意してくれるService Objectのもっと明確な定義と、どのように実装すべきかを今も探し続けています。

私たちが最近手がけた案件では、あるパターンに従うことで再利用が楽になりました。最初に、モジュールを1つ作成します。これをincludeすると、performという名前のクラスメソッドを作成します。

次に、作成するすべてのサービスで、コンストラクタ(initialize)をprivateにします。つまり、このperformパブリッククラスメソッドだけを呼ぶということです(もちろんRubyのような動的言語ではprivateメソッドも呼ぼうと思えば呼べますが、単に呼びにくくするだけの処置です)。

module PerformerService
  def self.included(base)
    base.send(:define_singleton_method, :perform) do |url|
      begin
        return self.send(:new, url).send(:perform)
      rescue Exception => e
        Rails.logger.error("#{self.class}: Exception raised: #{e}")
      end

      return nil
    end
  end
end
class UrlParser
  include PerformerService
  private
    def initialize(url)
      @url = url
    end
    def perform
      # ここですごいことをやる
    end
end

UrlParser.perform('https://kollegorna.se')

16. ActiveRecordのエラーメッセージを好みの形に変換する(permalink

RailsでAPIを書くと、エラーメッセージはたいていJSONAPI形式に従います。つまり、メッセージ(can't be blank)とメッセージが失敗した属性(user_id)が出力されます。

この例ではJSONポインタを使っていませんが、これにも同じアイデアを適用できます。

クライアント側では好みに応じて次の2つの方法でこれらを扱います。フォームに移動してuser_id inputを赤で表示するか、メッセージを連結して「User id can’t be blank」などのように読みやすい形に変換するかです。

しかしメッセージに関連する属性がユーザーにとって意味のないものである場合はどうなるでしょうか。

このアプリで、各ユーザーは新しい投稿(post)を1つ作成できるとします。ただし投稿は1日1回までだとします。モデルで次のようにして一意性を強制します。

validates :user_id, {
  uniqueness: {
    scope: :post_id,
    conditions: -> { where('created_at >= ?', 1.days.ago) },
  }
}

(はい、DBレベルでも同じように一意性制約をかけるべきですよね、わかっております。しかしここでは仮に、ユーザーが2つの異なるサーバー(しかも同じアプリが動き、同じDBを使っている)にアクセスして、運よく(運悪く)2つのリクエストを完全に同時に受け取れないと困るので、このエラーは扱いません)

このときのメッセージは次のようになります。

{
  "title": "リクエストを処理できませんでした",
  "message": "(エラーの詳しい説明)",
  "errors": [
    {
      "attribute": "user_id",
      "message": "は既に使われています"
    }
  ]
}

ユーザーはこれを渡されても困ってしまいます。1つの方法は、messageオプションを使うことです。

validates :user_id, {
  uniqueness: {
    scope: :post_id,
    conditions: -> { where('created_at >= ?', 1.days.ago) },
  },
  message: 'さんの投稿は1日1回までです'
}

これで、メッセージは['user_id', 'さんの投稿は1日1回までです']のように多少読みやすくなりましたが、両方の属性を使う場合にあまり便利ではありません。

{
  "title": "リクエストを処理できませんでした",
  "message": "(エラーの詳しい説明)",
  "errors": [
    {
      "attribute": "user_id",
      "message": "さんの投稿は1日1回までです"
    }
  ]
}

理想は、このメッセージをbaseに移動することです。このメッセージは特定のモデル属性に依存しない、より一般的なカスタム制約だからです。これは、メッセージにカスタムDSLを追加すればできるようになります。

validates :user_id, {
  uniqueness: {
    scope: :post_id,
    conditions: -> { where('created_at >= ?', 1.days.ago) },
  },
  message: {
    replace: "user_id",
    with: {
      attribute: "base",
      message: "ユーザーの投稿は1日1回までです"
    }
  }
}
def replace_errors(errors)
  errors_array = []
  errors.messages.each do |attribute, error|
    error.each do |e|
      if e.is_a?(Hash) && e[:replace]
        errors_array << {
          attribute: e[:with][:attribute],
          message: e[:with][:message]
        }
      else
        array_hash << {attribute: attribute, message: e}
      end
    end
  end

  return errors_array
end

これで、使いたい属性に合うエラーが出力されます。

{
  "title": "リクエストを処理できませんでした",
  "message": "(エラーの詳しい説明)",
  "errors": [
    {
      "attribute": "base",
      "message": "ユーザーの投稿は1日1回までです"
    }
  ]
}

17. 値を返すメソッドでは明示的にreturnを書く(ワンライナーであっても)(permalink

Rubyコミュニティはreturn文を書かないことにこだわっていると思いますが、私はそこにこだわる理由はない気がしています。実際私は、たとえワンライナーであっても、副作用が目的ではなく戻り値を目的とすべき場合はreturn文を追加しています。

Rubyのクールさと表現力を云々することよりも、生産性と(ある種の)安全性の方が勝ります。

18. なるべくかっこ()を使う(ある種のDSLを使う場合を除く)(permalink

これも同様です。かっこを追加して困ることはありませんし、普段他の言語も使っている同僚が幸せになれます。

19. env変数に厳密な論理値型を追加する(permalink

私はconfig/sercrets.ymlで次のようなスニペットを使うのが好きです。

<%
booly_env = ->(value) {
  return false if value.blank?

  return false if (['0', 'f', 'false'].include?(value.to_s.downcase))
  return true if (['0', 't', 'true'].include?(value.to_s.downcase))
  return true
}
%>

こうすることで、論理値型のenv変数がtruefalseのどちらかだけを取るようになるので、コードで使いやすくなります。

development:
  enable_http_caching:  <%= booly_env[ENV["ENABLE_HTTP_CACHING"] || false] %>

20. PostgreSQL以外のデータベースをメインで使うのであれば十分な理由付けが必要(permalink

MongoDBはひと頃もてはやされていましたが、ほどなくしてMongoDBの欠点が知られるようになりました。

  • スキーマレスである: スキーマレスは機能の1つだと思うかもしれませんが、実際には大きな欠点です。データベースにスキーマがあることで、スキーマを必要に応じて少しずつ変更できますし、ツールや保証も得られます。たとえば、SQLにinteger型のカラムが1つあるとすると、これをstring型やtext型に変更することも、デフォルト値の設定やNULL禁止の設定もできます。これはスキーマレスなデータベースでは不可能であり、プログラミング言語を用いて高度なレベルで自作する必要があります。スキーマレスなデータベースでは、属性の追加や削除も不可能です。基本的に最初のスキーマに縛られてしまうので、一から作り直して正しく移行できることを自力で確認するか、アプリケーションレベルで扱うことになります。
  • トランザクションが使えない
  • ACIDでない
  • クエリが少し大きくなったときの速度も大したことはないと感じる

メインで使っているデータベースでこんな目に遭っても構いませんか?私はイヤです。個人的にMongoDBの唯一の目玉機能と思えるのは、親ドキュメントに多数のドキュメントを埋め込めることぐらいです。それ以外の機能はおそらくPostgreSQLで用が足ります(それにセキュリティアップデートの面倒を見なければならないデータベースシステムが1つで済みます)。

21. 動的スコープは、他に打つ手がない場合にはよいパターン(permalink

Rubyでクロージャ(proclambda)を定義すると、レキシカルなスコープや環境がクロージャにカプセル化されます。

これは、コードのAという場所でprocを定義したとしても、コードのBという場所でそれを渡して呼び出したときに、procが定義されたAのレキシカルスコープ内で定義されているものであれば変数でも何でも参照できるということです。言い方を変えると「環境について閉じている」ということです。

これを逆にしたらどうなるでしょうか。たとえばコードのAという場所でprocを1つ定義し、そこでprocを呼んでもまったく意味がないが、コードのBという場所でprocを呼びたい場合にクロージャのレキシカルスコープを変更することで、実行結果にBの環境が反映されるようにするとします。

次の例をご覧ください。

CLOSURE = proc{puts internal_name}

class Foo
  def internal_name
    'foo'
  end

  def closure
    proc{puts internal_name}
  end

  def name1
    closure.call
  end

  def name2
    CLOSURE.call
  end

end

puts Foo.new.name1 #=> foo
puts Foo.new.name2 #=> undefined local variable or method `internal_name' for main:Object (NameError)

クロージャの定義時点ではinternal_nameが定義されていないので、当然name2メソッドは失敗します。

しかし、instance_execを使うとprocのバインディング(レキシカルスコープ)を再定義できます。

CLOSURE = proc{puts internal_name}

class Foo
  def internal_name
    'foo'
  end

  def closure
    proc{puts internal_name}
  end

  def name1
    closure.call
  end

  def name2
    instance_exec(&(CLOSURE))
  end

end

puts Foo.new.name1 #=> foo
puts Foo.new.name2 #=> foo

成功です。これは、アプリのある部分に書いたコードを、まったく異なるコンテキストで実行できるということです。しかしこれはどんなときに便利なのでしょうか?このあたりをいろいろハックしてみましたが、非常に有用な使いみちの1つはRailsのルーティングでした。

次のようなルーティングがあるとします。

  namespace :api do
    namespace :v1 do
      resources :company_users, only: [:show] do
        resources :posts, only: [:index] do
          resource :stats, only: [:show]
        end
      end
    end
  end

上から以下のルーティングが生成されます。

/api/v1/company_users/:id
/api/v1/company_users/:company_user_id/posts
/api/v1/company_users/:company_user_id/posts/:post_id/stats

:company_user_idはどうやら不要なので、次のようにしてクライアント側での柔軟性を高めたいと思います。

/api/v1/stats?user_id=:company_user_id&post_id=:post_id

しかしAPIは既に本番で稼働していて変更は困難です。

  namespace :api do
    namespace :v1 do
      resources :company_users, only: [:show] do
        resources :posts, only: [:index] do
          resource :stats, only: [:show]
        end
      end

      resource :stats, only: [:show], defaults: {company_user_id: proc{params[:company_id]}}
    end
  end

ルーティングの中にparamsがある?そのとおり!理由は、次のスニペットを使って、procのコンテキストをコントローラのコンテキストに再バインドしているからです。

def reshape_hash!
    self.params = HashWithIndifferentAccess.new(params.to_unsafe_h.reshape(self))
end

これで、このルートにuser_idを送信すると、このメソッドをbefore_filterとして追加することで、company_user_idとして追加されます。

class Api::V1::StatsController < ApplicationController
  before_action :authenticate_user!
  before_action :reshape_hash!

  def index
    stats = Stats.new(current_user).all(
      user_id: params[:company_user_id], post_id: params[:post_id]
    )

    render json: stats, serializer: StatsSerializer
  end
...

このテクニックをルーティング以外で使ったこともありますが、ほとんどは最後の手段としてです。ご利用は計画的に。

関連記事

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

Railsで重要なパターンpart 1: Service Object(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

Railsのdefault_scopeは使うな、絶対(翻訳)

Rails: dry-rbでForm Objectを作る(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: dry-rbでForm Objectを作る(翻訳)

現代のRailsでは、Form Objectを作るのは珍しくありません。多くのRuby開発者はVirtusActiveModel::ValidationsをincludeしてForm Objectを作成することに慣れています。本記事では、dry-typedry-validationを使ってForm Objectを作成する方法をご紹介したいと思います。

絵ハガキ(postcard)を作成する簡単なForm Objectを作ってみましょう。このアプリには次の3つのモデルがあります。

  • Country: フィールドはnameis_state_required。2つ目のフィールドは正しいアドレスの作成に使われ、米国などのユーザーは州名の入力が必要です。
  • Country::State: フィールドはnamecountry_id
  • Postcard: フィールドはstate_idcountry_idcontentaddress

完了までの作業を定義する

  • フォームで新しい絵ハガキを作成する(だいたいおわかりですね)
  • 住所、市町村、郵便番号、コンテンツ、国のバリデーションを行う
  • 郵便番号フォーマットのバリデーションを行う
  • コンテンツの長さのバリデーションを行う(ツィートやテキストメッセージ並に短くしたい場合)
  • 選択した国で州名が必要な場合、州名の存在のバリデーションも必要

属性と型

まずは属性の定義から行います。Form ObjectはDry::Types::Structから継承する必要があります。必要なゲッターやコンストラクタはDryで定義されます。

class Postcard
  module Types
    include Dry::Types.module
  end

  class CreateForm < Dry::Types::Struct
    attribute :address, Types::Coercible::String
    attribute :city, Types::Coercible::String
    attribute :zip_code, Types::Coercible::String
    attribute :content, Types::Coercible::String
  end
end

Dry::Types.moduleincludeするだけでDry-typesの型を使えるようになります。Dry-typesでは変更に応じた多くのプリミティブ型を選択できます

Railsモデルを使う場合はもう少し複雑です。これらの型で属性を作成するには、型を登録する必要があり、TypeName = Dry::Types::Definition.new(::MyRubyClass)のように行います。.constructorをブロック付きで呼び出すと、dry-typesで構成される型を指定できます。

定義は以下のような感じになります。

module Types
  include Dry::Types.module
  Country = Dry::Types::Definition.new(::Country)
  CountryState = Dry::Types::Definition.new(::Country::State)
end

これで、CountryCountryStateを型として使えるようになりました。最終的なフォームの定義は次のようになります。

class CreateForm < Dry::Types::Struct
  attribute :address, Types::Coercible::String
  attribute :city, Types::Coercible::String
  attribute :zip_code, Types::Coercible::String
  attribute :content, Types::Coercible::String
  attribute :country, Types::Country
  attribute :state, Types::CountryState
end

これでやっと、シンプルなstructを作成できました。

メモ: dry-typesのstructコンストラクタについて

コンストラクタの種類を指定しないと、strictコンストラクタが生成されます。この場合、属性が見つからないとArgumentErrorをスローします。存在のバリデーションはdry-validationで行うので、より多くの情報を含むコンストラクタであるschemaコンストラクタやsymbolizedコンストラクタを使うことになります。schemaコンストラクタを使うには、クラス本体の中でconstructor_type(:schema)を呼ぶ必要があります。

バリデーション

Form Object内部でバリデーションを実行するには、dry-validation gemを使います。これにはさまざまな述語(メソッド)が含まれており、使い方も簡単です。まずは存在のバリデーションを行ってみましょう。

PostcardSchema = Dry::Validation.Schema do
  required(:address).filled
  required(:city).filled
  required(:zip_code).filled
  required(:content).filled
  required(:country).filled
end

先ほど定義したモデルの属性を渡すスキーマを次のように定義します。

errors = PostcardSchema.call(to_hash).messages(full: true)

それではこの動作を見てみましょう。

  • to_hash(またはto_h): 属性をハッシュベースで生成する
  • .messages(full: true): 完全なエラーメッセージを返す。

フォーマットや長さなど、渡すバリデーションの要件を増やすには、単に.filledメソッドにパラメータを渡します。contentを例に取ると、存在バリデーションの他に、20文字より長いこともバリデーションされます。

required(:content).filled(min_size?: 20)

利用できる述語の全リストはこちらをご覧ください。

バリデーションロジックがさらに複雑な場合

存在や長さのバリデーション機能はdry-validationによって提供されます。残念なことに(?)、実際に動くアプリではこれだけでは不十分です。そのため、dry-validationで独自の述語を書けるようになっています。

まずは簡単なものから。バリデーションに渡されたcountrystateが必要な場合は以下のように書きます。

PostcardSchema = Dry::Validation.Schema do
  configure do
    config.messages_file = Rails.root.join('config/locales/errors.yml')
    def state_required?(country)
      country.is_state_required
    end
  end
# (...)
end

このとおり簡単です。errors.ymlに正しいエラーメッセージを書いておくのをお忘れなく。エラーファイルについて詳しくはこちらをどうぞ。

次はいよいよ、countryで必要になった場合にのみstateの存在をチェックしましょう。stateが存在するかどうかをバリデーションに伝える必要があります。これは、スキーマに以下の行を書くだけでできます。

required(:state).maybe

ルール自体を定義する

ルール自体は次のように定義します。

rule(country_requires_state: [:country, :state]) do |country, state|
  country.state_required? > state.filled?
end

これも見てのとおり簡単です。

  • ルール内で必要となるフィールドに沿ったルール名を渡します。ここではcountryとstateを使います。
  • これらの変数はブロックにyieldされます。
  • 「stateが必要な場合は、stateの存在をチェックする」というようにルールが変換されます。

より高度なルールについて詳しくはこちらをどうぞ。

完成したForm Object

class Postcard
  module Types
    include Dry::Types.module
    Country = Dry::Types::Definition
                .new(::Country)
    CountryState = Dry::Types::Definition
                     .new(::Country::State)
  end

  class CreateForm < Dry::Types::Struct
    constructor_type(:schema)

    ZIP_CODE_FORMAT = /\d{5}/
    MINIMAL_CONTENT_LENGTH = 20

    attribute :address, Types::Coercible::String
    attribute :city, Types::Coercible::String
    attribute :zip_code, Types::Coercible::String
    attribute :content, Types::Coercible::String
    attribute :country, Types::Country
    attribute :state, Types::CountryState


    def save!
      errors = PostcardSchema.call(to_hash).messages(full: true)
      raise CommandValidationFailed, errors if errors.present?
      Postcard.create!(to_hash)
    end

    private

    PostcardSchema = Dry::Validation.Schema do
      configure do
        config.messages_file = Rails.root.join('config/locales/errors.yml')
        def state_required?(country)
          country.is_state_required
        end
      end
      required(:address).filled
      required(:city).filled
      required(:zip_code).filled(format?: ZIP_CODE_FORMAT)
      required(:content).filled(min_size?: MINIMAL_CONTENT_LENGTH)
      required(:state).maybe
      required(:country).filled

      rule(country_requires_state: [:country, :state]) do |country, state|
        country.state_required? > state.filled?
      end
    end
  end
end

モデルやspecを含む完全なプロジェクトは私のGitHubに公開してあります。

適用できるリファクタリング

記事を読みやすくするため、私はオブジェクト自身に関連するものをすべてひとつのファイルに書きました。このような書き方は、おそらく実際のアプリにおけるコードベースの編成法として最適ではありません。次のリファクタリングが考えられます。

  • Typesモジュールを別のモジュールに配置する(場合によってはグローバルスコープに)
  • PostcardSchemaはForm Objectの外部に配置し、UpdateFormなどでも使う
  • ZIP_CODE_FORMATMINIMAL_CONTENT_LENGTHについても同様

まとめ

dry-typesを使うと、アプリで型安全なコンポーネントを書けるようになります。このライブラリでは多数の型が利用可能で、独自定義も簡単です。

私にとって、dry-validationによるアプローチはActiveModelを使ったものよりも明快に感じられます。バリデーションロジックをすべて明確に区切られた場所に集められます。これらのバリデーションは他のフォーム(UpdateForなど)での再利用も簡単です。

dry-rbシリーズの最大の問題は(ROMRodaにも同種の問題があるのですが)、初めてのユーザーが簡単に使えるようなドキュメントがないことです。信じていただけるかどうかはともかく、私はこのForm Objectの作成に2時間かかりました。原因のほとんどは、ドキュメントの問題と、ブログ記事がないことです。本記事が皆さまの2時間を節約するのに役立てばと願っています。

関連記事

Ruby: Dry-rb gemシリーズのラインナップと概要

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

RubyのModule Builderパターン #2 Module Builderパターンとは何か(翻訳)

Rails5: ActiveSupport::Durationでの数値へのパッチ

$
0
0

こんにちは、hachi8833です。

小ネタですが、RailsのActiveSupport::Durationで数値にどうやってパッチを当てているのかが気になったので見てみました。

ActiveSupport::Durationでの挙動

1.month2.daysなどでDurationになります。

require 'active_support/all'
a = 1.month
#=> 1 month
a.class
#=> ActiveSupport::Duration

ついでに1のクラス階層も見てみます。

1.class
#=> Integer
1.class.ancestors
#=> [ActiveSupport::ToJsonWithActiveSupportEncoder,
 ActiveSupport::NumericWithFormat,
 Integer,
 JSON::Ext::Generator::GeneratorMethods::Integer,
 Numeric,
 Comparable,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 Object,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 PP::ObjectMixin,
 Kernel,
 BasicObject]

どうやらNumericWithFormatでやっているようです。

numeric/conversions.rbだった

あっさり見つかりました。

module ActiveSupport::NumericWithFormat
  ...
end

# Ruby 2.4+ unifies Fixnum & Bignum into Integer.
if 0.class == Integer
  Integer.prepend ActiveSupport::NumericWithFormat
else
  Fixnum.prepend ActiveSupport::NumericWithFormat
  Bignum.prepend ActiveSupport::NumericWithFormat
end
Float.prepend ActiveSupport::NumericWithFormat
BigDecimal.prepend ActiveSupport::NumericWithFormat

Module#prependを使ってIntegerにパッチを当てていました。
Ruby 2.4以前の場合はFixnumBignumにパッチを当てています。

FloatBigDecimalにも当たっているので、1.1.hoursもできます。

関連記事

[Rails5] Active Supportの概要をつかむ

Rails 5.2ベータがリリース!内容をざっくりチェックしました

$
0
0

こんにちは、hachi8833です。

日本時間の早朝、DHHが自らRails 5.2ベータの公開をアナウンスしましたので、駆け足で内容を追っかけてみました。

過去の週刊Railsウォッチで扱った内容が多かったので、扱ったことのあるPRにはバックナンバーも貼りました。

Rails 5.2ベータの概要

プレスリリースの末尾には、Railsの5.xシリーズはこれが最後になるかもしれないとあります。既にRails 6.0を視野に入れているそうです。

Active Storageの改善

ファイルをクラウドにそのままアップロードできるようになりました。Amazon S3、Google Cloud Storage、Microsoft Azure Cloud File Storageをすぐ使うことができます。動画やPDFのプレビューも作りやすくなったそうです。

Active StorageはBasecamp 3で既に本番運用されているとのことです。

Active Storage READMEより:

  • ファイルが1つの場合も複数の場合も対応

モデルでhas_one_attachedhas_many_attachedで指定できます。

# ファイルが1つの場合
class User < ApplicationRecord
  # 添付ファイルとblobが関連付けられる。ユーザーが削除されるとデフォルトで削除される
  # (モデルが削除され、リソースファイルが削除される)
  has_one_attached :avatar
end

# ファイルが複数の場合
class Message < ApplicationRecord
  has_many_attached :images
end
  • Active StorageとJavaScriptライブラリでクラウドへのダイレクトアップロードをサポート
// asset pipelineに以下を追加
//= require activestorage
// npmパッケージを利用
import * as ActiveStorage from "activestorage"
ActiveStorage.start()
<!-- フォームでアップロードを指定 -->
<%= form.file_field :attachments, multiple: true, direct_upload: true %>

なお現時点では、ActiveStorageのドキュメントはedgeguides.rubyonrails.orgにも上がっていません。

週刊Railsウォッチ(20170707)Railsの新機能ActiveStorage、高速Rubyフォーマッタrufo gemが超便利、Railscasts全コンテンツが無料公開ほか

Redisキャッシュストアが標準で使える

純粋なRedis、hiredis、Redis::Distributedをサポートし、複数Redisでのシャーディング(sharding: 複数サーバーへのデータ分散)やMGETも利用可能です。Redisサーバーにアクセスできない場合にも例外をraiseせず、ローカルキャッシュも利用できます。

      # デフォルトは `redis://localhost:6379/0`
      config.cache_store = :redis_cache_store
      # Redis::Distributedで複数ホストをサポート
      config.cache_store = :redis_cache_store, driver: :hiredis
        namespace: 'myapp-cache', compress: true,
        url: %w[
          redis://myapp-cache-1:6379/0
          redis://myapp-cache-1:6380/0
          redis://myapp-cache-2:6379/0
          redis://myapp-cache-2:6380/0
          redis://myapp-cache-3:6379/0
          redis://myapp-cache-3:6380/0
        ]

キャッシュの改善

キーベースのキャッシュの不要な生成を抑制して再利用を促進し、1KB以上のキャッシュをデフォルトで圧縮するようになりました。イニシャライザでcompress: falseを指定すると圧縮がオフになります。

週刊Railsウォッチ(20170526)増えすぎたマイグレーションを圧縮するsquasher gem、書籍「Complete Guide to Rails Performance」ほか

HTTP/2 early hintsを導入

HTTP/2のearly hintsに対応しました。

# actionpack/lib/action_dispatch/http/request.rb
+    def send_early_hints(links)
+      return unless env["rack.early_hints"]
+
+      env["rack.early_hints"].call(links)
+    end

参考: blog.jxck.io HTTP の新しいステータスコード 103 Early Hints

週刊Railsウォッチ(20171013)Ruby 2.5.0-preview1リリース、RubyGems 2.6.14でセキュリティバグ修正、Bootstrap 4.0がついにBetaほか

Bootsnap gemが標準に

Shopify作のbootsnap gemはRails/Rubyアプリを高速で起動できます。既存のRailsアプリのGemfileへの追加も容易です。

週刊Railsウォッチ(20170728)bootsnapがRailsで正式採用、Ruby Prizeの推薦開始、PostgreSQL配列の重複を除去ほか

Content-Security-PolicyヘッダーをDSLで設定可能に

これはRailsウォッチでは扱っていなかったものでした。

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do
  p.default_src :self, :https
  p.font_src    :self, :https, :data
  p.img_src     :self, :https, :data
  p.object_src  :none
  p.script_src  :self, :https
  p.style_src   :self, :https, :unsafe_inline

  # Specify URI for violation reports
  # p.report_uri  "/csp-violation-report-endpoint"
end

参考: MDN Content-Security-Policy

config/secrets.yml.encで秘密情報をリポジトリで一元管理可能に

EncryptedConfigurationクラスも導入されました。

週刊Railsウォッチ(20170929)特集: RubyKaigi 2017セッションを振り返る(2)Ruby 2.3.5リリースほか

Webpacker 3.0が利用可能に

そういえばWebpacker 2.0が6月頃、3.0が8月末にリリースされていました。asset pipelineの出番が減りそうです。

週刊Railsウォッチ(20170602)チームが喜ぶ19のgem、Bundler 1.15が高速化&機能追加、Deviseに挑戦する新認証gem「Rodauth」ほか

番外

関連は不明ですが、早くもRails 5.1.2がらみのバグとおぼしきissueがRubyの方に上がりました。

Rails: RSpecをもっとDRYに書くテクニック(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: RSpecをもっとDRYに書くテクニック(翻訳)

これは何?

RSpec APIは常に可能な限りDRYで読みやすいDSLへと進化し続けています。しかし、specをさらにDRYにできる方法(トリックや追加メソッドなど)はまだまだあります。

警告: 本記事を読んで「これこれの方法を使うより先に、テストしやすいコードを書けるように設計を見直す方がよくね?」と言いたくてたまらなくなるかもしれませんが、それについてはどうか周知のこととお考えください。

更新情報(2017/08/15): saharspecはその後正式にリリースされました。テストもドキュメントもあります。ぜひお試しください。

DRYに書く方法

subjectを活用

subject文は現代的なRSpecと非常によく馴染みます。subjectを使うことで、テストの対象を明確に記述してからテストを実行できます。

# DRYかつ良い
subject { 'foo' }
it { is_expected.to eq 'foo' }

rspec-its(以前はRSpecのコアにありましたが、v3からは別gemに切り出されました)を使うと、さらに創意に満ちたチェックを行えます。

# これもDRYかつ良い
its(:size) { is_expected.to eq(3) }

しかしこのアプローチには少々限りがあります。

subjectにarrayを書く

(subjectで)値のarrayをチェックしたい場合、itsチェックでは(英語的に)うまくはまりません。

# `subject`を繰り返して長い`expect`引数を使う必要がある :(
subject { %w[foo test shenanigans] }
it { expect(subject.map(&:size)).to include(3) }

次の方法はいかがでしょうか。

subject { %w[foo test shenanigans] }
its_map(:size) { is_expected.to include(3) }

実装は次のとおりです(既にrspec/itsをrequireして名前空間を再利用している前提です)。

module RSpec
  module Its
    def its_map(attribute, *options, &block)
      describe("map(&:#{attribute})") do
        let(:__its_map_subject) do
          attribute_chain = attribute.to_s.split('.').map(&:to_sym)
          attribute_chain.inject(subject) do |inner_subject, attr|
            inner_subject.map(&attr)
          end
        end

        def is_expected
          expect(__its_map_subject)
        end

        alias_method :are_expected, :is_expected

        options << {} unless options.last.is_a?(Hash)

        example(nil, *options, &block)
      end
    end
  end
end

次のようにチェインすることもできます。

its_map(:'chars.first') { is_expected.to include('s') }
subjectにブロックを書く

次のコードで考えてみます。

describe 'addition' do
  subject { 'foo' + other }

  context 'when compatible' do
    let(:other) { 'bar' }
    # DRYかつ良い
    it { is_expected.to eq 'foobar' }
  end

  context 'when incompatible' do
    let(:other) { 5 }
    # subjectをまた書かないといけない:(
    it { expect { subject }.to raise_error(TypError) }
  end
end

上より以下の方が良いとは思いませんか?(私は思います)

subject { 'foo' + other }
# ...
context 'when incompatible' do
  let(:other) { 5 }
  its_call { is_expected.to raise_error(TypeError) } # よし、これもDRYになった
end

実装は次のとおりです(既にrspec/itsをrequireして名前空間を再利用している前提です)。

module RSpec
  module Its
    def its_call(*options, &block)
      describe("call") do
        let(:__call_subject) do
          -> { subject }
        end

        def is_expected
          expect(__call_subject)
        end

        example(nil, *options, &block)
      end
    end
  end
end

its_callは多くの便利マッチャと併用できます。

subject { hunter.shoot_at(:lion) }
its_call { is_expected.to change(Lion, :count).by(-1) }
subjectにメソッドを書く

ある比較的シンプルなメソッドについて、条件をさまざまに変えると戻り値がどのように変わるかを多数テストしなければならないとします。こんなとき、どう書きますか?

it { expect(fetch(:age)).to eq 30 }
it { expect(fetch(:weight)).to eq 50 }
it { expect(fetch(:name)).to eq 'June' }
# .... パターンがあることがわかりますか?...

以下の書き方はいかがでしょうか(これはRSpecとrspec/itsだけで追加コードなしで書けます)。

subject { ->(field) { fetch(field) } }

its([:age]) { is_expected.to eq 30 }
its([:weight]) { is_expected.to eq 50 }
its([:name]) { is_expected.to eq 'June' }

これだけで動きます。its([arg])とするとsubjectの[]が呼び出され、Ruby’のProcでは[]の定義が.callと同義になっているからです。

: 同じsubjectをテストするもうひとつの方法は、上のits_callを書き換えてits_call(:age) { is_expected.to eq 30 }のように引数を取れるようにすることです。

マッチャで楽しむ

上でご紹介したアイデアは、どちらもRSpecメンテナーたちによって検討の末rejectされました。だからといってこのアイデアが完全に役に立たないということにはなりません(正直、私はメンテナーの好みよりこちらの方がずっと明確だと思います)。

否定テスト

更新(2017年夏): この方法は非常によくないので使わないでください。演算子の優先順位が原因で、and_notを2つ連続で使うと思いもよらぬ結果が生じます。

訳注: 取り消し線の部分は訳出しませんでした。

メソッドのexpectation

#934で、「あるオブジェクトのあるメソッドを、あるコードから呼び出せるexpectation」をRSpecで1文で書けない理由がずっと議論されています(長すぎて私もあまりフォローできていません)。

訳注: 現在#934はcloseしています。

現時点では、次のどちらかの書き方が使えます。

# その1
expect(obj).to receive(:method)
some_code

# その2
obj = spy("Class")
some_code(obj)
expect(obj).to have_received(:method)

私にはどちらもあまり「アトミック」には見えません。次のソリューションはいかがでしょうか。

RSpec::Matchers.define :send_message do |object, message|
  match do |block|
    allow(object).to receive(message)
      .tap { |m| m.with(*@with) if @with }
      .tap { |m| m.and_return(@return) if @return }
      .tap { |m| m.and_call_original if @call_original }

    block.call

    expect(object).to have_received(message)
      .tap { |m| m.with(*@with) if @with }
  end

  chain :with do |*with|
    @with = with
  end

  chain :returning do |returning|
    @return = returning
  end

  chain :calling_original do
    @call_original = true
  end

  supports_block_expectations
end

これで以下のように1文で書けます。

# 引数の順序はchange()マッチャと似ている
expect { some_code }.to send_message(obj, :method).with(1, 2, 3).returning(5)

# 前述のits_callとの相性もよい
subject { some_code }
it { is_expected.to send_message(obj, :method).with(1, 2, 3).returning(5) }

クールだと思いませんか?

軽くまとめ

更新情報(2017/08/15): saharspecはその後正式にリリースされました。テストもドキュメントもあります。ぜひお試しください。

ご紹介したスニペットはいずれも私が日常的に業務で便利に使っており、ちゃんと動いています。もちろん別の方法がよいこともあるでしょう。しかしいずれにしろ、どの実装も雑なりにちゃんと動きます(「動くけど雑」と思う人もいるかもしれませんが)。私はこれらのスニペットをsaharaspecというちょっと気の利いた名前のリポジトリに置きましたが、本記事執筆時点ではまだ正式なgemになっておらず、テストやドキュメントもありません。しかし正式なgemspec付きでGitHubに置かれているので既に利用可能な状態になっていますので、ぜひ皆様のご感想をお寄せください。

Gemfileに以下を追記します(おそらくdevelopmentグループ)。

gem 'saharspec', git: 'https://github.com/zverok/saharspec.git'

なお、Redditでは本記事のワンライナーDRY specについて議論がかなり白熱しています(良し悪しはともかく)。

関連記事

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

テストを不安定にする5つの残念な書き方(翻訳)

Ruby: 「マジック」と呼ぶのをやめよう(翻訳)

Rails: ActiveRecord関連付けのpreload/eager-loadをテストする2つの方法(翻訳)


あまり知られてないRuby/Railsの便利メソッド5つ(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

あまり知られてないRuby/Railsの便利メソッド5つ(翻訳)

私がRuby on Railsで開発するようになってかれこれ数年が経過しましたが、Ruby世界で何か新しい発見があるたびに感動があります。それこそがRubyであり、Rubyは開発者を幸せにするのが目的です(他にもいろいろ法則はありますが、とりわけ)。Rubyを使っていると、毎年何か新しいものを見つけるたびにそのことを感じます。

本記事では最近私が発見したものをいろいろご紹介いたします。これらはめったに使われていませんし、使わなければいけないものでもありません。ほとんどは「シンタックスシュガー」ですが、いずれにしろコードがとても明確になります。一部は最近のRubyやRailsの新機能です。

1. Hash#dig

Rails開発を7年続けていますが、つい最近初めてこれを見かけたときに当然「これは何?」と思いました。私がRubyで開発を始めた頃は1.8だったのですが、この機能は2.3で導入されました。

次のようなコードを何回書いたか覚えていますか。

... if params[:user] && params[:user][:address] && params[:user][:address][:somewhere_deep]

digはsafe navigation演算子(ぼっち演算子)&.の一種ですが、Hashオブジェクトで使えます。これを使えば上のコードは次のように書き直せます。

... if params.dig(:user, :address, :somewhere_deep)

2. Object#presence_in

これは、Query Objects in Ruby on Railsというお気に入りの記事を書いたときに見つけました。存在チェックの結果(論理値)が欲しいのではなく、チェックされたオブジェクトそのものが欲しい場合に、条件(多くは三項演算子)を1つのメソッドで書けます。次のコードをご覧ください。

sort_options = [:by_date, :by_title, :by_author]
...

sort = sort_options.include?(params[:sort])
  ? params[:sort]
  : :by_date

# もうひとつの方法
sort = (sort_options.include?(params[:sort]) && params[:sort]) || :by_date

上のコードを下のように書き直せまず。ずっとよくなりましたよね?

params[:sort].presence_in(sort_options) || :by_date

3. Module#alias_attribute

この便利さに気づいたのは、ある案件でレガシーなデータベースを扱っているときでした。そのデータベースのあるテーブルでは、SERNUM_0ITMDES1_0といった気持ち悪いカラム名が大量に使われていました。このテーブルをActiveRecordモデルにマップするときに、WeirdTable.where(SERNUM_0: '123')のようなクエリやスコープを書く代わりに、#alias_attributeを使うことにしました。このメソッドのよさは、述語メソッドやゲッターやセッターを生成するだけではなく、クエリで次のように使える点です。

alias_attribute :name, :ITMDES1_0
...
scope :by_name, -> (name) { where(name: name) }

4. Object#presence

このメソッドは他のものよりは知られています。ApiDockにわかりやすい説明があります。object.presenceは次と同等です。

object.present? ? object : nil

5. Module#delegate

理由はわかりませんが、このメソッドを使っている開発者をめったに見かけません。このメソッドの主な目的は、結合を弱めてデメテルの法則に沿うようにすることです。デメテルの法則についての良記事はいろいろありますが、その中でもAvdi Grimm氏の「デメテルは単なるよいアイデアではない: 法則である」が思い出されます。Rails Best Practicesの記事でも、デメテルの法則を適用するときに#delegateを使っているものがありますのでご覧ください。以下のスニペットでもおわかりいただけます。

class Profile < ApplicationRecord
  belongs_to :user

  delegate :email, to: :user
end

...
profile.email # profile.user.emailと同等

本記事でご紹介したヒントがお役に立てば幸いです。楽しくコーディングしましょう!

本記事を気に入っていただいた方は、ぜひ元記事の[✋]をクリックして応援してください。

関連記事

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

Rails: dry-rbでForm Objectを作る(翻訳)

3年以上かけて培ったRails開発のコツ集大成(翻訳)

週刊Railsウォッチ(20171201)JSON PatchでRails高速化、Knapsack Proでテスト高速化、Decorator/Presenter gem比較ほか

$
0
0

こんにちは、hachi8833です。マカーな皆さまはHigh Sierraアップデートお済みでしょうか。

続報: macOS High Sierra脆弱性パッチに別の不具合、ファイル共有できなくなる恐れ。ただし修正は簡単

12月最初のウォッチ、いってみましょう。

Rails: 今週の改修

MemCacheStoreでexpiringカウンタに機能追加

expires_in: [seconds]#increment#decrementオプションが追加されました。
Rails.cache.increment("my_counter", 1, expires_in: 2.minutes)のように使えます。

# activesupport/lib/active_support/cache/mem_cache_store.rb#125
        options = merged_options(options)
         instrument(:increment, name, amount: amount) do
           rescue_error_with nil do
-            @data.incr(normalize_key(name, options), amount)
+            @data.incr(normalize_key(name, options), amount, options[:expires_in])
           end
         end
       end

アイドリング中のDB接続を解除

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#959
+      # Disconnects all currently idle connections.
+      #
+      # See ConnectionPool#flush! for details.
+      def flush_idle_connections!
+        connection_pool_list.each(&:flush!)
+      end

つっつきボイス: 「これまではとりあえずconfigで設定したpool数分だけ常にDBのconnction poolを確保していたのが、idle_timeoutの間使われていないconnectionは切断するようになったということかな」「大規模なシステムだと影響ありそう: abstractの中で定義されているので、ほぼすべてのDBMSに影響するのかも」

ActiveRecordのDB接続のfork周りを改善

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb より
+      # Discards all connections in the pool (even if they're currently
+      # leased!), along with the pool itself. Any further interaction with the
+      # pool (except #spec and #schema_cache) is undefined.
+      #
+      # See AbstractAdapter#discard!
+      def discard! # :nodoc:
+        synchronize do
+          return if @connections.nil? # already discarded
+          @connections.each do |conn|
+            conn.discard!
+          end
+          @connections = @available = @thread_cached_conns = nil
+        end
+      end

つっつきボイス: 「これは上の#31221に関連する修正のようだ」「最近DBアダプタ周りの改修が目につきますね」

form_withヘルパーでデフォルトでidを生成するよう変更

configでid生成を無効にすることもできるそうです。

# actionview/lib/action_view/helpers/form_helper.rb#1676
      def initialize(object_name, object, template, options)
         @nested_child_index = {}
         @object_name, @object, @template, @options = object_name, object, template, options
-        @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {}
+        @default_options = @options ? @options.slice(:index, :namespace, :allow_method_names_outside_object) : {}

         convert_to_legacy_options(@options)

つっつきボイス: 「あ…これform_forform_withに代えたときにはまった気がする」「今度からはデフォルトでidが有効ですね」

ActiveRecord::RecordNotFoundに引数を追加

# activerecord/lib/active_record/associations/collection_association.rb#79
       def find(*args)
         if options[:inverse_of] && loaded?
           args_flatten = args.flatten
-          raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
+          model = scope.klass
+
+          if args_flatten.blank?
+            error_message = "Couldn't find #{model.name} without an ID"
+            raise RecordNotFound.new(error_message, model.name, model.primary_key, args)
+          end
+

つっつきボイス: 「これはデバッグでありがたい: どのidで失敗したかがわかるのは親切」
「その代わり今後はこういう雑なコード↓を書くとリクエストidがお漏らししてしまいますけどね」

raise => e
  render :index, alert: e.inspect

variable_size_secure_compareをpublicに変更

publicに変更してもSHA256ダイジェストの長さは暴露されないからという理由です。長さを手がかりにした攻撃方法があった気がします。

# actionpack/lib/action_controller/metal/http_authentication.rb#70
             before_action(options.except(:name, :password, :realm)) do
               authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
                 # This comparison uses & so that it doesn't short circuit and
-                # uses `variable_size_secure_compare` so that length information
+                # uses `secure_compare` so that length information
                 # isn't leaked.
-                ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) &
-                  ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password])
+                ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) &
+                  ActiveSupport::SecurityUtils.secure_compare(password, options[:password])
               end
             end
           end

つっつきボイス: 「長さを手がかりにした攻撃って何て名前だったかなー」「んーと」「そうだ、timing attack

以下の記事によると、アプリの応答時間がパスワード比較の前方一致に比例してしまうとパスワードが推測されやすくなってしまうので、それを防ぐためにRails 4.2.5.1でvariable_size_secure_compareが導入されたという経緯でした。

参考: Rails CVE-2015-7576 で見る タイミングアタック(Timing Attack)

既存の認証情報をデフォルトで上書きしないよう修正

昨日mergeされていました。

# railties/lib/rails/generators/rails/credentials/credentials_generator.rb#8
   module Generators
     class CredentialsGenerator < Base
       def add_credentials_file
-        unless credentials.exist?
+        unless credentials.content_path.exist?
           template = credentials_template

           say "Adding #{credentials.content_path} to store encrypted credentials."
...
       def add_credentials_file_silently(template = nil)
-        credentials.write(credentials_template)
+        unless credentials.content_path.exist?
+          credentials.write(credentials_template)
+        end
       end

つっつきボイス: 「うっぷ、これは普通にバグ」「y-yagiさんが秒殺で修正」

Rails

Decorator/Presenter gem 6種を比較(Awesome Rubyより)

以下の6つを比較していますが、他にもあるそうです。Lulalalaは手作りなようです。

  • ActiveDecorator
  • Draper
  • Oprah
  • Display-case
  • Lulalala Presenter
  • RailsCasts

つっつきボイス: 「RailsCast?と思ったらgemじゃなくて本当にRailsCastsだった」「↓この図、DecoratorとPresenterの違いがよくわかってとてもいい」「Presenterは機能を絞り込んでる感じ」「Decoratorは強力な分ビューで無茶なことできちゃったりする」


lulalala.logdown.comより

Rails 5.2を待たずにActiveStorageを使ってみる(Ruby Weeklyより)

すぐ使える手順です。

# 同記事より
Rails.application.routes.draw do
  resources :posts
end

Rails 5.2ベータがリリース!内容をざっくりチェックしました

JSON-PatchでRailsのパフォーマンスを向上(Ruby Weeklyより)

見出しから単にJSONにパッチを当てるのかと思ったら、RFC6902: JSON Patchを元に実装したfast-JSON-Patchというnpmパッケージでした。hanaというgemで導入しています。

// 同記事より
import { compare as jsonPatchCompare } from 'fast-json-patch'

if (!Immutable.is(template.fields, previousTemplate.fields)) {
  data.fields_patch = jsonPatchCompare(
    previousTemplate.fields.toJS(), template.fields.toJS())
}

つっつきボイス: 「おー、JSONを毎回まるごと投げる代わりに差分だけを投げるのか」

ブラウザごとのsession cookie上限を調べてみた(Ruby Weeklyより)

Rack::Protection::MaximumCookieというRackミドルウェアの利用を勧めています。

# mwpastore/rack-protection-maximum_cookie より
use Rack::Protection::MaximumCookie

つっつきボイス: 「巨大なsession cookie食わせると普通にぶっ壊れますね」「クライアント側で対処しないといけなくなるとつらいです」

Amazon API GatewayでRuby SDK生成をサポート(Ruby Weeklyより)

aws.amazon.comより

# 同記事より
gem install pet-sdk-1.0.0.gem

つっつきボイス: 「おー、gemとして生成してくれるのがよさそう」「そういえば昔SOAPとかでこういうSDK生成的なのが流行った気がする: Java界隈とかで特に」

RailsEventStore.orgで監査ログを無料で取得


railseventstore.orgより

RailsEventStore.orgはオープンソースをベースにしています。


つっつきボイス: 「これ使いみちあります?」「悪くなさそう: 某社案件で使いたかったなこれ」「pub/sub好きな人にはいいかも」「Auditingは大体以下の2つをやりたいケースが多い:」

  • データに対する参照や変更ログを自動で網羅的に取りたい
  • 特定の操作に対して手動で特別なログを出したい

「前者はARのModelに対してであればpaper_trailとかがメジャーで、良い」「後者は正直あんまり決め手がないというかLoggerでよくね?という話になったりする」


後でRailsのPublish/Subscribeについて検索したところ、The Publish-Subscribe Pattern on Rails: An Implementation Tutorialという記事でwisperというgemがあるのを知りました。RailsのActionCableにもPub/Subがありますね。

Knapsack Pro: 複数のCIノードにテストを分散するサービス


knapsackpro.comより


つっつきボイス: 「Knapsack Pro、以下がとりあえずわかりやすかった」

参考: CircleCI + KnapsackProでRailsのテストを高速化させる

comfortable-mexican-sofa: Rails 5.1対応のマルチリンガルCMSエンジン(RubyFlowより)

以前からあったようです。★2100超え。


github.com/comfy/comfortable-mexican-sofaより


つっつきボイス: 「名前が凄いな: comfyという略し方も」「mexican sofaってこういう形なのか」「Railsエンジンのようだけどルーティングは自分で足すのかな↓」「マルチリンガル対応みたいだし、使いどころがありそうならチェックしてもいいかも」

# 同記事より
comfy_route :cms_admin, path: "/admin"
comfy_route :cms, path: "/"

derailed_benchmarks: Railsアプリ全体のベンチマークgem

Railsのベンチマークをさっと取れます。作者はRichard Schneemanさんです。

$ bundle exec derailed bundle:mem
TOP: 54.1836 MiB
  mail: 18.9688 MiB
    mime/types: 17.4453 MiB
    mail/field: 0.4023 MiB
    mail/message: 0.3906 MiB
  action_view/view_paths: 0.4453 MiB
    action_view/base: 0.4336 MiB

つっつきボイス: 「gem名はちょっとひねりすぎかなー」「『脱線』w」「自分でもちょっとだけ動かしてみました」

duckrails: Rails APIのモックを急いで作りたいときに(Ruby Weeklyより)

GUIでAPIモック作れます。docker pull iridakos/duckrailsでお試しできます。


github.com/iridakos/duckrailsより


つっつきボイス: 「これ教育用にならいいかもしれない」

enumerize: ActiveRecordなどの属性をi18n化

class User < ActiveRecord::Base
  extend Enumerize
  enumerize :sex, :in => [:male, :female], scope: true
  enumerize :status, :in => { active: 1, blocked: 2 }, scope: :having_status
end

User.with_sex(:female)
# SELECT "users".* FROM "users" WHERE "users"."sex" IN ('female')

User.without_sex(:male)
# SELECT "users".* FROM "users" WHERE "users"."sex" NOT IN ('male')

User.having_status(:blocked).with_sex(:male, :female)
# SELECT "users".* FROM "users" WHERE "users"."status" IN (2) AND "users"."sex" IN ('male', 'female')

つっつきボイス: 「名前からenumの拡張かと思ったら、enumerizeのkeyはstringとしてDBに保存されるということかな」
「なお、自分はこのkeyをstringのままenumとして扱う方が好き: DBでSELECTしたときにわかるので」「keyが0とか1とかだと見たときにわからないですしね」「そういえば今のRailsはenumで_prefix_suffixが使えるのでとても助かってます↓」「enumってRails 4.1からだったのか」

参考: Rails Enum with prefix/suffix

social-share-button: Railsに各種SNSボタンを追加するgem(Awesome Rubyより)


github.com/huacnlee/social-share-buttonより


つっつきボイス: 「今ならgemよりWebpackでインストールしたいですね」

本当にあったRailsの怖い話

Ruby trunkより

提案: attrattr_readerattr_writerをpublicに変えよう

2.5で採用されるようです。

Here are 15k+ examples of send :attr_accessor in the wild:
https://github.com/search?utf8=%E2%9C%93&q=language%3Aruby+%22send+%3Aattr_accessor%22&type=Code
15k+ examples of send :attr_writer in the wild:
https://github.com/search?utf8=%E2%9C%93&q=language%3Aruby+%22send+%3Aattr_writer%22&type=Code
15k+ examples of send :attr_reader in the wild:
https://github.com/search?utf8=%E2%9C%93&q=language%3Aruby+%22send+%3Aattr_reader%22&type=Code

つっつきボイス:send :attr_accessorとかでやっている事例がこんなにたくさんあるとは↑」「それならpublicにしてもよさそうですね」

提案: Exception#displayのエラー出力をフォーマットしたい(継続)

def the_program
  # ...
  raise "failure!"
  # ...
rescue RuntimeError => e
  $stderr.puts "#{e.message} (#{e.class})\n\t#{e.backtrace.join("\n\t")}"
  retry
end

# こう書けば済むようにしたい

rescue RuntimeError => e
  e.display
  #

つっつきボイス:sorahさんからの提案だ」「『そらは』って読むのか」

今年3月の大江戸Ruby会議でキーノートスピーチを務めたのがsorahさんでした。

大江戸Ruby会議 06 に行ってまいりました

Ruby

クラスメソッドをclass << selfで定義する理由(Awesome Rubyより)

RubocopのRubyスタイルガイドではdef self.methodが推奨されていることを踏まえた記事です。いかにも議論になりそうです。


つっつきボイス: 「わかるー: 自分もclass << selfにしたい派」「self.だとクラスのどこにでもクラスメソッドを書けてしまうから散らかりそう」「self.使うこともあるかな」「個数にもよるかも: 1つのクラス内でself.が3つ以上になると耐えられなくなりそう」

【保存版】Rubyスタイルガイド(日本語・解説付き)総もくじ

Ruby 2.5の新機能: Dir.childrenDir.each_childRuby Weeklyより)

# 同記事より
> Dir.each_child("/Users/mohitnatoo/Desktop/test") { |child| puts child }
.config
program.rb
group.txt
test2

つっつきボイス:Dir.childrenDir.each_child、なぜ今までなかったんだと思っちゃいますね」

Rubyでfreezefrozen?を使うタイミング

# 同記事より
MY_CONSTANT = "foo".freeze
MY_CONSTANT << "bar" # => RuntimeError: can't modify frozen string

平易で読みやすい内容です。


つっつきボイス: 「Ruby最近始めた人やPHPとかから来た人には読んでおいて欲しいですね」

chewy: Ruby製Elasticsearchフレームワーク(Awesome Rubyより)

Elasticsearch公式のelasticsearch-rubyODMでありラッパーだそうです。

# toptal/chewyより
class UsersIndex < Chewy::Index
  define_type User.active.includes(:country, :badges, :projects) do
    field :first_name, :last_name # multiple fields without additional options
    field :email, analyzer: 'email' # Elasticsearch-related options
    field :country, value: ->(user) { user.country.name } # custom value proc
    field :badges, value: ->(user) { user.badges.map(&:name) } # passing array values to index
    field :projects do # the same block syntax for multi_field, if `:type` is specified
      field :title
      field :description # default data type is `string`
      # additional top-level objects passed to value proc:
      field :categories, value: ->(project, user) { project.categories.map(&:name) if user.active? }
    end
    field :rating, type: 'integer' # custom data type
    field :created, type: 'date', include_in_all: false,
      value: ->{ created_at } # value proc for source object context
  end
end

つっつきボイス: 「インターフェースが素直で普通っぽく書けるのがよさそう」

bunny: RabbitMQのRuby製クライアント


rubybunny.infoより

ドキュメントがかなり充実しているようです。

# ドキュメントより
require "bunny"

conn = Bunny.new
conn.start

ch   = conn.create_channel
q = ch.queue("", :exclusive => true)
x = ch.fanout("logging.events")

q.bind(x)

つっつきボイス:RabbitMQはメッセージングミドルウェアとして有名」「そういえばIBMのWebSphere MQというのもありますね」


www.rabbitmq.comより

参考: RabbitMQを導入すると… “依存しないカンケイ”でもっと幸せになれる!?

RubyHack.com: 米ソルトレイクシティで開催されるRubyカンファレンス


rubyhack.comより

昨年に続き、来年5月中旬に第2回が開催されます。詳細は未定のようです。


つっつきボイス: 「ソルトレイクシティというとロケットカーのイメージ」「ユタ州なのか」

@a_matsudaさんの福岡Ruby会議02スライド


つっつきボイス: 「a_matsudaさんの作ったライブラリこんなにある↓」

RubyConf 2017に参加しての雑感(Ruby Weeklyより)

ざっとしか見ていませんが、進むに連れてだんだん落ち込み気味になってきて、ちょっとどきどきしてしまいました。コメント欄の励ましが泣けます。

mruby向けIDE

ちょっとだけ動かしてみました。


つっつきボイス: 「組み込み分野でIDE欲しい人はそれなりにいると思うので、そういう人向けかも」

データベース

PostgreSQLの設定を1箇所変えたら速度が50倍になった(Postgres Weeklyより)

これだけ速くなったそうです。


amplitude.engineeringより


つっつきボイス: 「まさしくexplainが役に立つ例」

[Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)

PostgreSQLのAutovacuumのビジュアル表示とチューニング(Postgres Weeklyより)


つっつきボイス: 「Vacuumって何だろうと思って」「↓: うかつにVACUUM FULLすると終わるまでテーブル全部ロックされるから注意」「こ、怖」

参考: PostgreSQL: VACUUM

PostgreSQLのトランザクション分離レベル(Postgres Weeklyより)

repmgr 4.0がリリース(Postgres Weeklyより)

PostgreSQL標準のツールです。dry-runできる操作が増えたようです。


つっつきボイス: 「dry-runマジありがたい」「レプリケーション方向逆にして自爆するのを防げる」

PgBouncerとAWS RDSでデータベーストラフィックのセキュリティを向上(Postgres Weeklyより)

参考: PgBouncer とは何ですか

スライド: 人間のためのPostgreSQL設定(Postgres Weeklyより)


つっつきボイス: 「やっぱりPostgreSQLはいい情報あるなー」

JavaScript

JSのletconst解説

ブラウザからBluetooth機器につないでみた


blog.vertica.dkより


つっつきボイス: 「こんなことできるのか!」

CSS/HTML/フロントエンド

Webサイトを簡単にPWA(Progressive Web App)に変える方法

Googleが推進しているProgressive Web Appの記事です。


pwa.rocksより


つっつきボイス: 「Progressiveといえばプログレ」「私もろその世代」「ピンク・フロイドとかキング・クリムゾンとかでしたっけ」「今はJoJoの影響でキング・クリムゾンの意味が全然違っちゃいましたね: 本家クリムゾンもそれに反応してたり」

参考: いまさら聞けないPWAとAMP

HTTP/2 pushは思ったより手強い(Frontend Weeklyより)

その他

正規表現の背後を深掘り


つっつきボイス: 「この間翻訳したJSの正規表現記事より深い内容っぽいので、これも翻訳してみます」

VSCodeの表示をかっこよくするCSS

VSCodeをアップデートするとCSSが元に戻っちゃうそうです。

コーダーがスランプを理解して克服する方法

最初何のblockかと思ってしまいました。

  • パソコンの電源を切ってみる
  • あえて紙と鉛筆でやってみる
  • などなど

つっつきボイス: 「紙と鉛筆はスランプ以外でも有効ですねー」

番外

どうぶつの森

100ドルで買えるミューオン検出器

12月


今週は以上です。

バックナンバー(2017年度)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Rails 3.2を4.0にアップグレードする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails 3.2を4.0にアップグレードする(翻訳)

本記事はUpgrade Railsシリーズの1つです。シリーズの他の記事についてはこちらをクリックしてください

前回の記事では、マイグレーションを考慮する際の一般的なヒントについて触れましたので、本記事ではもう少し先に進みます。最初にRailsを3.2から4.0にアップグレードし、次に4.1、最終的に4.2にアップグレードします。所要時間はアプリの複雑さに応じて、開発者1人で1週間かかることもあれば、開発者2人で数か月かかることもあります。

  1. Rubyバージョン
  2. Gem
  3. Configファイル(config/)
  4. アプリケーションコード
    • a. モデル(app/models/)
    • b. コントローラ(app/controllers/)
  5. テスト
  6. その他
  7. 次のステップ

1. Rubyバージョン

Ruby 3.2.xは、Ruby 1.8.7をサポートする最後のバージョンです。アプリでRuby 1.8.7を使っている場合、Rubyを1.9.3以上にアップグレードする必要があります。本ガイドではRubyのアップグレードについて扱いませんので、詳しくはこちらのガイドをご覧ください。

2. Gem

rails4_upgradeというgemをRails 3プロジェクトのGemfileに追加して、アップデートの必要なgemを確認します。

$  myproject git:(develop) ✗ bundle exec rake rails4:check

** GEM COMPATIBILITY CHECK **
+------------------------------------------+----------------------------+
| Dependency Path                          | Rails Requirement          |
+------------------------------------------+----------------------------+
| devise 2.1.4                             | railties ~> 3.1            |
| devise-encryptable 0.2.0 -> devise 2.1.4 | railties ~> 3.1            |
| friendly_id 4.0.10.1                     | activerecord < 4.0, >= 3.0 |
| strong_parameters 0.2.3                  | actionpack ~> 3.0          |
| strong_parameters 0.2.3                  | activemodel ~> 3.0         |
| strong_parameters 0.2.3                  | activesupport ~> 3.0       |
| strong_parameters 0.2.3                  | railties ~> 3.0            |
+------------------------------------------+----------------------------+

現在バンドルされているgemやGemfile.lockを手動で調べなくても、アップグレードの必要なgemのレポートを生成できます。

3. Configファイル

Railsにはrails:updateというタスクが含まれています。これを、こちらの記事で説明されているガイドとして使うことができます。configファイルやinitializerから不要なコードやモンキーパッチを取り除くときに役立ちます。特に、Rails 2で動いていたRails 3アプリので有用です。

他の方法として、RailsDiffというサイトで基本的なRailsアプリの3.2から4.0への変更点をチェックすることもできます。このサイトでは他のバージョン間の変更点もチェックできます。

冒険してみたい方は、このスクリプトをお試しください。これは(RailsDiffで表示されているパッチと同じような感じで)Railsアプリにこのgitパッチ の適用を試行し、3.2から4.0に移行します。ただし複雑なアプリや成熟したアプリに適用するとコンフリクトが発生するためおすすめしません。

4. アプリケーションコード

a. モデル

  • .find_by_...を除くすべての動的finderメソッドが非推奨になります。
# 移行前
Authentication.find_all_by_provider_and_uid(provider, uid)

# 移行後
Authentication.where(provider: provider, uid: uid)

これらのfinderメソッドは、activerecord-deprecated_finders gemを追加することで使えるようになります。

  • ActiveRecordのスコープではlambdaが必要になりました。
# 移行前
default_scope where(deleted_at: nil)

# 移行後
default_scope { where(deleted_at: nil) }

# 移行前
has_many :posts, order: 'position'

# 移行後
has_many :posts, -> { order('position') }

(なお、default_scopeはくれぐれも慎重にお使いください)

  • 保護された属性は非推奨になりましたが、protected_attributes gemをインストールして引き続き使うことは可能です。ただし、RailsコアチームはRails 5.0からこのgemをサポート対象外にしているため、いずれにしろそのモデルはStrong Parametersに移行するべきですではありません

モデルを移行するには、attr_accessibleへの呼び出しをモデルから削除し、モデルに対応するコントローラにuser_paramsモデル名_paramsといった名前で新しいメソッドを追加する必要があります。

class UsersController < ApplicationController
  def user_params
    params.require(:user).permit(:name, :email)
  end
end

最後に、params[:user]への(ほとんどの)参照を、コントローラのアクション内のuser_paramsに変更します。この参照が更新や作成に使われる場合は(user.update_attributes(params[:user])など)、user.update_attributes(user_params)に変更します。この新しいメソッドは、Userモデルのname属性やemail属性の利用を許可し、Userモデルのその他の属性(idなど)への書き込みをすべて禁止します。

  • ActiveRecord ObserversはRails 4.0のコードベースから削除され、gemに切り出されました。Gemfileに以下を追加することで使えるようになります。
gem 'rails-observers' # https://github.com/rails/rails-observers

別の方法として、wisper gemか、もう少し違うアプローチとしてRailsのconcern(Rails4.0以降で追加)をチェックしてみてもよいでしょう。

  • ActiveResourceは削除され、独自のgemに切り出されました。
gem 'active_resource' # https://github.com/rails/activeresource

b. コントローラ

  • ActionController Sweeperはrails-observers gemに切り出されました。Gemファイルに以下を追加することで使えるようになります。
gem 'rails-observers' # https://github.com/rails/rails-observers

  • Action cachingは独自のgemに切り出されました。この機能をもう一度使いたい場合は、以下のいずれかの方法を使います。
caches_page   :public

または

caches_action :index, :show

その場合次のgemを追加する必要があります。

gem 'actionpack-action_caching' # https://github.com/rails/actionpack-action_caching

5. テスト

Ruby 1.9からはtest-unit gemをGemfileに追記する必要があります(標準ライブラリから削除されたため)。または、MinitestRSpecなど好みのテストフレームワークに移行します。

6. その他

  • ルーティングでリクエストメソッドの指定が必須になったため、デフォルトの「catch-all」動作に依存できなくなりました。
# これは以下のいずれかに変更する
match '/home' => 'home#index'

# こちら
match '/home' => 'home#index', via: :get

# またはこちら
get '/home' => 'home#index'
  • Rails 4.0からはプラグインのサポートが廃止されたため、プラグインをgemに置き換える必要があります。RubyGemsGithubでgemを探すか、プラグインをlibディレクトリに移動してRailsアプリで必要な箇所でrequireします。

7. 次のステップ

以上の手順が無事終了すれば、Rails 4.0を実行できるはずです。

アプリを微調整したい場合は、ぜひFastRuby.ioであなたのアップグレード結果をお気軽にお知らせください。

関連記事

Railsのdefault_scopeは使うな、絶対(翻訳)

Rails+PostgreSQLのパーティショニングを制覇する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

原文タイトルはおそらくCommand&Conquerのもじりと思われます。

Rails+PostgreSQLのパーティショニングを制覇する(翻訳)

前書き

本記事は実際の出来事をヒントにしたデータベースパーティショニングについて書いたものです。productionアプリ、すなわちRuby on RailsとPostgreSQLの速度を低下させる巨大なテーブルを分割する方法について手順を追って学びます。

これ以上大きくなっては困るとき

データベースは肥大化する傾向があります。データベースのサイズはある時点から負債と化しますが、主キー数が上限に達するような極端な状況をなかなか想定できません(そしてこれは実際に起きます)。本記事は、私達の一顧客であるGettでの経験を元にしています。このときはデータベーステーブルが危険水域に達するほど肥大化し続けていました。

行数が数百万行に達すると、クエリによっては完了に数時間を要することもあります。これによって生じた技術的な困難をデータベースパーティショニングによって解決しました。

1個の巨大なテーブルを多数の小さなテーブルに分割するというのは標準的な技法ですが、特に本番稼働中のデータが危機にひんしている場合は注意深く行う必要があります。本記事では、よくある落とし穴を回避してデータロスなしで移行できるようにする方法を解説します。何らかのハンズオンをやってみるのが学習法として最善なので、最初にフェイクデータで巨大なテーブルを作成して問題を作り出します。続いて、PostgreSQLのマジックを武器としてこの問題を皆さんと一緒に解決します。

実際のフェイクテーブル

まずデータが、それも大量のデータが必要です。改善前のテーブルが含むordersには、通常のビジネスロジックを模したカラムがあります。

訳注: 上の一文目は、おそらく映画「マトリックス」のセリフ(Guns, Lots of Guns)のもじりです。

CREATE TABLE orders (
  id SERIAL,
  country VARCHAR(2) NOT NULL,                        -- 国コード
  type VARCHAR NOT NULL DEFAULT 'delivery',           -- orderの種類
  scheduled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL,  -- orderの作成時刻
  cost NUMERIC(10,2) NOT NULL DEFAULT 0,              -- orderのコスト
  data JSONB NOT NULL DEFAULT '{}'                    -- 追加データ
);

: リファクタリングでは主にPostgreSQLを考慮するため、以後クエリは純粋なSQLで、関数はPL/pgSQLでそれぞれ表記します。作業が終わった後は、ActiveRecord経由でデータをRailsで扱えるようになります。

最初に、実行頻度が最も高いクエリを次のように決めます。

  • idorder_byしてorderを1件取得
  • ある期間(精度は分単位)の特定の国についてのordersをすべて取得する
  • orderのcostや関連付けられたデータを変更する

ordersテーブルを検討すれば、最も多いクエリの速度を向上させるにはcountryscheduled_atをインデックス化するのが妥当であることが即座にわかります。

CREATE INDEX index_orders_on_country_and_scheduled_at ON orders (country, scheduled_at);

準備が整ったので、以下のようにランダムな値を用いてgenerate_seriesでテーブルの値を埋めます。

INSERT INTO orders (country, type, scheduled_at, cost)
SELECT
  ('{RU,RU,RU,RU,US,GB,GB,IL}'::text[])[trunc(random() * 8) + 1],
  ('{delivery,taxi}'::text[])[trunc(random() * 2) + 1],
  CURRENT_DATE - (interval '1 day' * (n / 100000)) + (interval '1 second' * (random() * 86400)),
  round((100 + random() * 200)::numeric, 2)
FROM
  generate_series(1,30 * 1000000) s(n);

分割

目標はストレートに設定しなければなりません。ここではordersを分割して次のようにしたいと考えています。

  • 生成されるテーブルには特定の月の特定の国のordersがすべて含まれること
  • アプリのロジックがほぼ変わらないようにすること

最も達成しやすいのは、子テーブルを作成し、対応するトリガを作成し、テーブル全体にレコードを分散させるトリガ関数を作成する方法です。

しかしこの方法でActiveRecordでデータベースにクエリをかけたい場合、ひとつ面倒な点があります。純粋なSQLでは、同じレコードを2回INSERTする(マスターテーブルで1回、子テーブルで1回)のを避けるため、トリガプロシージャはNULLを返す必要があります。しかしこれはActiveRecordとの相性がよくありません。ActiveRecordはINSERT文でRETURNING文を使った場合に新規レコードの主キーを1つ返すことを期待するからです。解決方法はいくつか考えられます。

レガシーなスキーマで(データベース)ビューを使う方法の例はRailsガイド(英語)をご覧ください。

  1. ActiveRecordにおまかせする: NULLの代わりに新規レコードを返します。新規レコードをマスターテーブル子テーブルにそれぞれ配置したら、マスターテーブルから即座に削除します。つまり1つの操作を3つに分けて行うことになります。この方法を選んでもパフォーマンスが必然的に著しく低下するため、ほとんどのRails開発者はパーティショニング自体を諦めざるを得なくなるでしょう。
  2. ActiveRecord PostgreSQLアダプタで切り抜ける: Rails 4.0.2以降なら設定ファイルでinsert_returningfalseに設定すればよいので、これは難しくありません。これはうまくいきますが、その代わりアプリの全テーブルの振る舞いが変わってしまいます。また、(主キーの現在の値を取得するため)INSERT操作ごとにリクエストを1つ余分に受け取ることになります。

  3. (データベース)ビューを使う: これならデータベースレベルのリファクタリングだけでできるようになります。しかも、ビューは「普通の」テーブルであるかのように扱えるため、ActiveRecordはビューと自然に協調動作でき、既存アプリのロジック変更は最小限で済みます。

第3の方法を使うことにします。最初に、テーブルを複製する必要があります。テーブルを複製するメリットは次のとおりです。

  • 既存データの完全性を保ち、他のモデルからの参照が安全に保たれる
  • パーティショニングのデプロイ中(マイグレーション後からリスタートまでの間)に既存アプリを生かしておくことができる
  • 作業中に問題が発生しても元のテーブルにフォールバックできる

複製は次の方法で行います。

CREATE TABLE orders_partitioned (LIKE orders INCLUDING ALL);

クローンされたテーブルの主キーは、元のテーブルと同じorders_id_seqシーケンスを参照します。これにより、古いテーブルから新しいテーブルにデータを移動するときに衝突を回避できます。

データベースビューはマジでいいやつ

今度は新しいテーブルでビューを作成する必要があります。新しいテーブルはまだ空ですが、変更をデプロイするとすべての新規レコードがそこに配置され、対応する複数の子テーブルにも直ちに同じレコードが配置されます。

CREATE OR REPLACE VIEW orders_partitioned_view AS SELECT * FROM orders_partitioned;

まだ何か足りないようです。デフォルト値はどうすればよいでしょうか。明らかに主キーのデフォルト値が必要ですし、デフォルト値がないとActiveRecordでINSERTが効かなくなってしまいます(orders_id_seqにご注目ください)。

ALTER VIEW orders_partitioned_view
ALTER COLUMN id
SET DEFAULT nextval('orders_id_seq'::regclass);

理論上は、他のカラムはデフォルト値なしでActiveRecordによって扱われますが、アプリのコードではまだ必要になる可能性があります。
また、先のトリガ関数はデフォルト値がないと動作しません(フィールド値がNULLのままになるため、INSERTNot Null Violationで失敗します)

ALTER VIEW orders_partitioned_view
ALTER COLUMN cost
SET DEFAULT 0;

ALTER VIEW orders_partitioned_view
ALTER COLUMN type
SET DEFAULT 'delivery';

ALTER VIEW orders_partitioned_view
ALTER COLUMN data
SET DEFAULT '{}';

次は、以下を行うトリガプロシージャが必要です。

  • 子テーブルをどのレコードを配置できるかを決定する
  • テーブルが存在しない場合は作成する: これがないと、思いつく限りすべてのテーブルを手動で作成しなければならなくなります。もちろんアプリで自動化もできますが、余分なコードは書かずに済ませたいものです。

データベースの(ストアド)プロシージャのコードを書くときが来ました。PL/pgSQLを使います。

CREATE OR REPLACE FUNCTION orders_partitioned_view_insert_trigger_procedure() RETURNS TRIGGER AS $BODY$
  DECLARE
    partition TEXT;
    partition_country TEXT;
    partition_date TIMESTAMP;
  BEGIN

    /* 新しいテーブルの名前を作成する*/

    partition_date     := date_trunc('month', NEW.scheduled_at);
    partition_country  := lower(NEW.country);
    partition          := TG_TABLE_NAME || '_' || partition_country || '_' || to_char(partition_date, 'YYYY_MM');

    /*
    必要な場合に子テーブルを作成する。関連するすべての部分に通知する。
    */

    IF NOT EXISTS(SELECT relname FROM pg_class WHERE relname = partition) THEN

      RAISE NOTICE 'ordersの新しいパーティションを作成します: %', partition;

      /*
      ここでは以下を行います:
      * マスターテーブルからテーブルを1つ継承
      * 生成されるテーブルにCHECK制約を作成
        (条件を満たさないレコードが挿入されないようにするため)
      * 必要なインデックスを作成
      * 引用符が3つあるのは機能であり、バグではありません
      */

    EXECUTE 'CREATE TABLE IF NOT EXISTS ' || partition || ' (CHECK (
      country = ''' || NEW.country || ''' AND
      date_trunc(''minute'', scheduled_at) >= ''' || partition_date || ''' AND
      date_trunc(''minute'', scheduled_at)  < ''' || partition_date + interval '1 month' || '''))
      INHERITS (orders_partitioned);';

    EXECUTE 'CREATE INDEX IF NOT EXISTS ' || partition || '_scheduled_at_idx ON ' || partition || ' (scheduled_at);';

  END IF;

  /* And, finally, insert. */

  EXECUTE 'INSERT INTO ' || partition || ' SELECT(orders  ' || quote_literal(NEW) || ').*';

  /*
  注意: NULLではなく、新規レコードが返されます。
  これによりActiveRecordとの相性がよくなります。
  */

  RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;

これで以下ができました。

  • テーブルとして使えるビュー
  • 新規レコードを配置するトリガ関数

それではここまでの成果をまとめてみましょう。

CREATE TRIGGER orders_partitioned_view_insert_trigger
INSTEAD OF INSERT ON orders_partitioned_view
FOR EACH ROW EXECUTE PROCEDURE orders_partitioned_view_insert_trigger_procedure();

ところで、Rubyらしく行うには、元のテーブルの代わりにorders_partitioned_viewを使うようOrderモデルに指示するだけでできます。Railsはこうした操作を自然に行なえますので、モデルのtable_nameにビュー名を与えるだけでおしまいです。
この機能はレガシーなスキーマを用いるときによく使われます。

ところで元のデータは?

そうそう、既存のデータを忘れてはいけません。まだ元のテーブルにそのまま残っているので、注意深く新しいテーブルに移動しなければなりません。問題はデータ量が非常に大きい(数百万行でしたよね)ことで、対処方法はいくつも考えられます。ありがたいことに、すべてのデータを移行しなければならないことはめったにありません。通常、直近のいくつかの月に対応する子テーブルにデータを入れる必要があります。先月分のデータでやってみましょう。

INSERT INTO orders_partitioned_view
       SELECT * FROM orders
       WHERE scheduled_at >= date_trunc('month', now());

後悔しないデプロイ方法

手順はできあがりましたが、本番環境で行うのはまったく別の話です。予想もつかないような問題がいくつも起きるでしょう。

私たちが本番でこの手法を使ったとき、まさにそれが起きたのです。パーティショニングが必要なサービスは「High Availability(高可用性)」を謳っていました。つまりダウンタイム15分以上は許されないということです。

本番サービスへの全リクエストはMessage Queueを経由しますので、少し待てば作業完了後にリクエストを実行できます。しかし実際には長時間待つしかありませんでした。

アプリはActiveRecordに全面的に依存していたのではなく、生のデータベースクエリも使われていたため、ダウンタイムは避けられませんでした(生SQLを避けることができれば、多くのトラブルに遭わずに済みます)。そういうわけで、Railsでテーブル名を単に切り替えるという選択肢は使えませんでした。また、テーブルの複製も避けたかったため、いくつかの手順を組み合わせました。

ALTER TABLE orders RENAME TO orders_partitioned;
CREATE OR REPLACE VIEW orders AS SELECT * FROM orders_partitioned;

/* ...省略 */

常に操作が繰り返されている稼働中のビジネスデータを扱うので、変更のリリースは「リスキー」とマーキングすべきです。理想的なデプロイチェックリストを以下に示します。

  • 運用エンジニアは今度のリリース内容を把握し、かつ立ち会うこと
  • データベース管理者も同様にリリース内容を把握し、かつ立ち会うこと
  • 監視可能なものはすべて監視対象に含めること: ディスクアクセス操作、RAMやCPUの使用量、ネットワークリクエスト、データベースクエリ、アプリリクエスト、バックグラウンドタスク、ログ
  • 別の開発者かチームリーダーに精査してもらった「リリースプラン」を用意すること
  • マイグレーションコードを書いた開発者に連絡を取れること
  • チームリーダーはこれらを把握し、立ち会うこと
  • あらゆる手順を手動で再現できるようにしておくこと
  • ロールバックプランを用意すること

大げさに見えるかもしれませんが、自信過剰は禁物です。十分時間をかけて正しいデプロイ手順を策定しましょう。

パーティショニングすべきかどうかの決定

テーブルのサイズを検査するのはもちろんですが、最も重要なのは時間の経過に伴うデータ増加を見積もることです。どんなクエリが最も多いかを検討します。主キーや外部キーによるSELECT文しか実行しないのであれば、おそらくパーティショニングは不要です。データを何らかの形でグループ化し(上の例のような期間ごとのグループ化など)、グループに関連するクエリを常用するのであれば、パーティショニングすることでかなり楽になるでしょう。

「これ使っちゃダメですか?」

より複雑で粒度の高いパーティショニングが必要になることがあります。しかし一度に1つのカラムしか作成しないのであれば使ってもよいでしょう。

  • PostgreSQL 10(とお遊びでpg_partyも)

宣言的パーティショニング(declarative partitioning)は本当に便利です。使える状況であれば遠慮なくどうぞ。ただし、PostgreSQLにかぎらず、最新バージョンを無条件に使える幸せな状況はめったにありません。(PostgreSQL 10へ)アップグレードできるのであれば、上述のデプロイプランを用意しておきましょう。


パーティショニングを試す準備は整いましたか?マイグレーションコードの作成は仕事の半分でしかありません。残り半分はチームが正しく作業を実行できるよう準備することです。

もっと詳しく知りたい方へ


スタートアップをワープ速度で成長させられる地球外エンジニアよ!Evil Martiansのフォームにて待つ。

関連記事

PostgreSQL 10の使って嬉しい5つの機能(翻訳)

Rails: PostgreSQLのマイグレーション速度を改善する(翻訳)

[Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)

Rails開発者のためのPostgreSQLの便利技(翻訳)

Rails 5.2を待たずに今すぐActiveStorageを使ってみた(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails 5.2を待たずに今すぐActiveStorageを使ってみた(翻訳)

DHHは今年ActiveStorageという新しいコンポーネントの導入をアナウンスしました. ActiveStorageは、写真などのアップロードをRailsで直接管理します。

以来、ActiveStorageをRailsに統合するため多くの改良が加えられ、ActiveStorageは事実上利用可能になっています。本記事では、ActiveStorageを使うためにRailsをアップデートする方法を調べてみました。

警告: bleeding edgeバージョンのRailsを使うため、見たこともないような問題が引き起こされる可能性があります。

Rails向けにActiveStorageをセットアップする

  • 5.1.14より前のRailsを使っている場合は、Gemfileを変更して5.1.14にアップデートします。
gem 'rails', '~> 5.1', '>= 5.1.4'
  • $ bundle update railsを実行します。
  • $ rails app:updateを実行してコードの差分をすべて解決します。
  • 5.1.14へのアップグレードで問題ないことを確認します。
  • bleeding edgeバージョンのRailsにアップデートします。

Gemfileを以下のように変更します。

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
  "https://github.com/#{repo_name}.git"
end
...
gem 'rails', github: 'rails/rails'
gem 'arel', git: 'https://github.com/rails/arel.git'
gem 'bootsnap', '~> 1.1', '>= 1.1.5', require: false
  • $ bundle update railsを実行します。
  • $ bundle exec rails -vでbleeding edgeバージョンRails 5.2.0.alphaの表示を確認します。
  • アプリケーションのconfigを更新し、$ bundle exec rails app:updateを実行します。
  • アプリ起動前に$ ./bin/rails --tasksrails active_storage:installが実行可能タスクに表示されることを確認します。
  • $ ./bin/rails active_storage:installを実行し、マイグレーションファイルを生成します。
  • $ ./bin/rails db:migrateを実行します。これでSQliteデータベースでActiveStorageがサポートされます。

ActiveStorageでシンプルな画像をアップロードしてみる

ここまではRailsアプリでActiveStorageをサポートするための準備でした。アプリでActiveSupportが使えるようになったので、ActiveStorageを使って画像のpostを作成できるようにする簡単な機能を作ってみましょう。

  • $ ./bin/rails g model postでPostモデルを作成します。
  • 次のマイグレーションファイルでPostのテーブルにtitlebodyの2つのカラムを追加します。
# db/migrate/20171114063756_create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :body

     t.timestamps
    end
  end
end
  • $ ./bin/rails g controller postsでPostsリソースのコントローラを作成します。
  • Postsリソースへのルーティングをconfig/routes.rbに追加します。

Rails.application.routes.draw do
  resources :posts
end
  • 画像とpostの関連付けが必要です。
class Post < ApplicationRecord
  has_many_attached :images
end
  • indexshowcreateアクションのコードを追加します。
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # postのフォームをここで表示する
  def index
    @post = Post.new
  end
  # ここでpostを作成する
  def create
    post = Post.create! params.require(:post).permit(:title, :body)
    post.images.attach(params[:post][:images])
    redirect_to post
  end
  # 写真付きのpostをここで表示する
  def show
    @post = Post.find(params[:id])
  end
end
  • 以下のコードを持つindexのビューをpostに追加します。アップロード用フォームはlocalhost:3000/postsに表示されます。
# app/views/posts/index.html.erb
<%= form_with model: @post, local: true do |form| %>
  <%= form.text_field :title, placeholder: "Title" %><br>
  <%= form.text_area :body %><br><br>
  <%= form.file_field :images, multiple: true %><br>
  <%= form.submit %>
<% end %>
  • postを表示するビューを追加します。
# app/views/posts/show.html.erb
<%= image_tag @post.images.first %>
  • 写真を1枚送信してみると、ビューに画像が表示されます。

この画像は、アプリのルートレベルのstorageというディレクトリにローカル保存されますが、このファイルをAWS S3やGoogle Cloud、Azureなどのクラウドファイルストレージシステムにプッシュするよう設定することもできます。

ご覧のとおり、ActiveStorageのおかげでRailsのActiveRecordコンポーネントにうまく統合されたシンプルなファイル管理システムを使えました。ActiveStorageが成熟すれば、ファイル管理で主要なユースケースをカバーできるようになるでしょう。Rails向けのファイル管理システムは他にもいろいろありますが、最初にActiveStorageを検討することをおすすめします。

ActiveStorageのドキュメントもご覧ください。

本記事の最終的なコードはGitHubリポジトリでご覧いただけます。

自身のプロジェクトでActiveStorageを使った経験を共有してくださった@jeffreyguentherに感謝いたします。

関連記事

Rails 5.2ベータがリリース!内容をざっくりチェックしました

週刊Railsウォッチ(20170721)ActiveStorageは5.2で正式導入、Onigmoの脆弱性が修正、この夏読みたい名作Ruby本ほか

Rails: Service Objectはもっと使われてもいい(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

なお、Aaron Lasseigne氏の言うService ObjectとAvdi Grimm氏の言うService Objectは違うものを指しているのではないかという意見がBPS社内でありました。

Rails: Service Objectはもっと使われてもいい(翻訳)

Avdi Grimm氏は最近のブログ記事で、Service Objectの盛り上がりに苦言を呈していました。Service Objectを擁護しようと思い、Service Objectにどんな欠点があるのか読んでみたくなりました。私の使うこのツールの長所と短所を知りたかったのです。しかし記事の内容は私の期待とは異なり、私の経験と真っ向からぶつかるものでした。

Avdiは冒頭で、PayPalのIPNデータを処理する「重すぎるコントローラ」を取り上げていました。皆さんもこの巨大過ぎるコントローラアクションを記事でご覧になったでしょう。これはよくある問題であり、修正したくなる問題です。彼はService Objectを1つ作成してコントローラのコードのほとんどをそこに移動しました。移動後の構造は次のような感じです。

class IpnProcessor
  def process_ipn(...)
    # ここにコントローラのコードを書く
  end
end

続いてIpnProcessor.new.process_ipnには「コードの匂い」があることを指摘しています。この命名は冗長であり、同義反復的な悪いコードに見えるとのことです。

ipn processor new process ipn

まさしく彼の言うとおりです。この命名はよくないものであり、Service Objectで通常使われている命名法からも外れています。チームでService Objectを使う場合、callまたはrunといったメソッド名で統一するのが普通です。

class IpnProcessor
  def call(...)
    ...
  end
end

ポイントは、publicなメソッドを1つに限定し、クラス名を見ればそのメソッドの動作がわかるようにすることです。IpnProcessorオブジェクトは、アプリで行う操作の1つを表現します。モデル名は名詞で表すべきであるのと同様に、Service Object名は動詞で表されるべきです。

Avdiは続いて、彼がよりよいソリューションと考えるアイデアを提唱しています。

ところで私のアプリでは、アプリのコードに総合的な名前空間を与えるモジュールを1つ作ることがよくあります。このアプリは「perkolator」(訳注: percolator: 濾過器のもじり)と呼ばれていたので、このモジュール名はPerkolatorとしました。

こうして作成されるモジュールはアプリ名を踏襲し、IPN処理のメソッドを1つ持ちます。

module Perkolator
  def self.process_ipn(...)
    # ...
  end
end

このアプローチのメリットは、「責務の増加」や「Service間の結合」をうまく避けられることです。

彼は、定義に不備のあるオブジェクトは危険であると主張しています。そうしたオブジェクトは本質的にあいまいになるため、本当に追加してよいかどうか疑問の残る機能が追加されやすくなってしまいます。その点には私も賛成です。しかしアプリケーション名のモジュールに全てを入れてしまえば、それこそそのモジュールが乱雑なコード置き場になってしまうのではないでしょうか。多くのRails開発者は、そこにふさわしくない機能が山積みされる「夢の島」と化したapp/controllers/application_controller.rbのことを思い出すでしょう。

Service Objectであれば、IpnProcessorオブジェクトは1つしかなく、publicに呼び出せるメソッドも1つしかありません。これなら、そこにメソッドをさらに追加しようとするときに違和感を覚えてコードレビューで相談を持ちかけるでしょう。(メソッドを増やさずに)機能を追加するには、1個のpublicメソッドの機能を増やすしかありませんが、直ちにそれもおかしいと思えてくるでしょう。1つのメソッドにオプション引数を大量に追加する方法は正しくないと感じられるものです。

2番目の大きな懸念は「Service間の結合」です。Serviceにはアプリのさまざまな操作がカプセル化され、最終的にそれらの操作が組み合わせられて動作します。2つの操作がデータベーステーブルを共有したり、2つのステップとしてさらに大きな処理に組み込まれるかもしれません。コントローラで起きているのはこれではないでしょうか?Service Objectをモジュールに置き換えたところで、この点が変わるでしょうか?何も変わりません。

私の経験では、Service Objectはアプリを介したパスを定義するのに有用です。Service Objectを使って次のようにわかりやすい操作のリストを作成できます。

CreateUserCreateGroupAddUserToGroupBanUserFromGroup

ある処理の手順が正確に定まっており、それらの手順をいつもとは異なる方法で互いに結合したい場合は、それらの手順をservicesディレクトリ内のフォルダに配置して、次のように名前空間を与えます。

Purchase::MadePurchase::Redeemed

Service Objectを利用しても、モジュールが利用できなくなるわけではありません。購入で使う承認トークンを理解して扱えるモジュールならおそらく意味があるでしょう。この場合、Service Objectはこのモジュールを承認に活用して他の処理を行えます。もちろん、これは何がしたいかによってまったく変わってきます。ハンマーしか入っていない道具箱は、本当の道具箱ではありません。

私の経験から

わぉ(いい意味の「わぉ」ではありません)。私はとあるスタートアップ企業でずっと働き続けています。開発者は4人で、従業員は20人そこそこでしょうか。

その企業では、あるRailsアプリを何年も前から使っていました。そのアプリは、コンピュータサイエンス専攻の学生たちが学位を終了する前に無償で作ってリリースしたものでした。学生たちは頭脳明晰でしたが、経験についてははなはだ不十分でした。もし私がその場にいたとしても、もっとうまくやれたとは思えません。実際、彼らは動く製品を作ったのであり、収益にもつながっていました。DHHの言う「期待を遥かに上回る」というやつです。

しかし機能の作り込みはだんだん困難になり、バグ修正もどんどんトリッキーになっていきました。サイトの実行速度も以前より落ちてしまい、仕切り直しが必要になりました。

作業は大変でしたが、それでも進捗はありました。会社は成長し、チームも拡大しました。私たちはコードをモジュールに移動し、巨大なオブジェクトのいくつかを分割しました。そしてある日のこと、ある開発者が「Service Objectを使ってみてはどうか」と持ちかけてきました。やがてチームは、その決定が勝利を導いたことに気づきました。

あるグループにユーザーを1人追加する方法が必要だとしましょう。追加するのはUser#join_groupGroup#add_memberでしょうか?違います。追加するのは、グループとユーザーを1つずつ受け取るAddGroupMemberというService Objectです。今度は、送信の必要なメールアドレスについてはどうでしょう。新しいメンバーにはグループから紹介メールを送信すべきであり、グループは新メンバー追加のメールを受け取るべきです。これも問題ありません。グループに新メンバーを追加する処理に含めることができます。

私たちは、このような状況をある程度避ける方法を見つけました。操作を無様な方法でモデルにアタッチするのではなく、それにふさわしい住み家を与えました。私たちはここから最終的にActiveInteractionというgemを作り上げました。このgemは私たちにとって非常にうまく動きましたし、他の人にとっても有用です。ActiveModelが名詞を扱うように、ActiveInteractionでは動詞を扱います。グループで利用できるインターフェイスも統一されました。私たちはこれらの扱い方、エラーハンドリング、呼び方を理解して、作成のための枠組みを手に入れました。

訳注: ActiveInteraction gemは「Commandパターン」を実装したものだそうです。

クラスからの操作の切り出しも素直に行えました。こうしたコードは簡単にテストできます。私たちは巨大なクジラを一口ずつ、おいしく味わいました。

これを手続きとして実現することもできましたが、もしそうしていたら枠組みは失われていたでしょう。ある開発者はRubyエラーをraiseし、別の開発者はエラーをreturnオブジェクトにアタッチし、また別の開発者はタプルの一部として送信するというようにバラバラになっていたことでしょう。チーム開発において、コードの標準化は成功に欠かせない重要な部分のひとつです。

彼が間違っているとも限らない

私はAvdiをとても尊敬していますが、この件に関して彼と私の意見は明らかに異なっています。もしかすると、彼が気づいていることで私がまだ見落としていることがあるのかもしれません。私はこれまでにもいろんなことに気づきましたし、今後も気づくことがあるでしょう。しかし今のところ、彼の主張から学べる点がまだ見つかりません。

私にとって確かなのは、Service Objectは業務を改善してくれたということです。Service Objectを使っている他の人に尋ねてみた結果も、肯定的な経験が圧倒多数でした。私はフリーランス開発者として、コードをきれいにするためにチームにService Objectを導入する価値はおそらく今後数年変わらないと思います。本記事への肯定的な評価が多い方に賭けます。

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

Ruby: Chain of Responsibilityパターンの解説(翻訳)

週刊Railsウォッチ(20171208)最近のRailsフロントエンド事情、国際化gem、mallocでRubyのメモリが倍増、るびま記事募集ほか

$
0
0

こんにちは、hachi8833です。

Railsウォッチ公開前に行われているつっつき会の後、軽呑みになだれこみました。「Service ObjectはパターンじゃないんではGoFデザインパターンほど定義のコンセンサスが取れていないのでデザインパターンの一種のように扱わない方がいいのでは」「名前変えて欲しい」みたいな話が飛び交ったような気がします。

12月のRailsウォッチ2回目、いってみましょう。年の瀬の足音が聞こえる…

Rails: 今週の改修

add_indexでPostgreSQLの演算子クラスをサポート

# activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb#249
       def test_index_with_opclass
         with_example_table do
-          @connection.add_index "ex", "data varchar_pattern_ops"
-          index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
-          assert_equal "data varchar_pattern_ops", index.columns
+          @connection.add_index "ex", "data", opclass: "varchar_pattern_ops"
+          index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
+          assert_equal ["data"], index.columns

-          @connection.remove_index "ex", "data varchar_pattern_ops"
-          assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
+          @connection.remove_index "ex", "data"
+          assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
         end
       end

つっつきボイス: 「PostgreSQLのoperator classってこれか↓」

演算子クラスにより、その列のインデックスで使用される演算子が特定されます。 例えば、int4型に対するB-treeインデックスには、int4_opsクラスを使用します。 この演算子クラスには、int4型の値用の比較関数が含まれています。 実際には、通常、列のデータ型のデフォルト演算子クラスで十分です。 演算子クラスを持つ主な理由は、いくつかのデータ型では、複数の有意義なインデックスの振舞いがあり得るということです。
https://www.postgresql.jp/document/9.6/html/indexes-opclass.htmlより

「それにしてもこのissue、随分スレが伸びてますね」

PostgreSQLの外部キーをバリデーションなしでも作成できる機能

外部キー追加によるパフォーマンス低下回避のためだそうです。

  • valid: falseオプションを指定すると無効な外部キーを作成できる機能
  • 外部キーバリデーション用のvalidate_foreign_keyメソッドを追加
# activerecord/test/cases/migration/foreign_key_test.rb#230
+        if ActiveRecord::Base.connection.supports_validate_constraints?
+          def test_add_invalid_foreign_key
+            @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+
+            foreign_keys = @connection.foreign_keys("astronauts")
+            assert_equal 1, foreign_keys.size
+
+            fk = foreign_keys.first
+            refute fk.validated?
+          end

つっつきボイス: 「PostgreSQL寄りの改修ほんとうに増えましたね」

preload_link_tagヘルパーを追加

HTTP/2 Early hintsがプロキシでサポートされている場合に対応するそうです。

preload_link_tag("custom_theme.css")
# => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />

preload_link_tag("/videos/video.webm")
# => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />

preload_link_tag(post_path(format: :json), as: "fetch")
# => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
...
# actionview/lib/action_view/helpers/asset_tag_helper.rb#260
+        early_hints_link = "<#{href}>; rel=preload; as=#{as_type}"
+        early_hints_link += "; type=#{mime_type}" if mime_type
+        early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin
+        early_hints_link += "; nopush" if nopush
+
+        request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request

つっつきボイス: 「PreloadってW3Cの仕様にあるんですね↓: すごく新しい」

Preload: W3C Editor’s Draft 30 August 2017

ActiveRecordのスコープ名に予約名を使えないよう修正

#
+  def test_scopes_name_is_relation_method
+    conflicts = [
+      :records,
+      :to_ary,
+      :to_sql,
+      :explain
+    ]
+
+    conflicts.each do |name|
+      e = assert_raises ArgumentError do
+        Class.new(Post).class_eval { scope name, -> { where(approved: true) } }
+      end
+      assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
+    end
+  end

つっつきボイス: 「Rails開発普通にやっていれば、使ってはいけない名前ってだいたい見当は付きますけどね」「それでもありがたいです」

Rails

Railsの現代的なフロントエンド事情を理解する

TechRacho翻訳記事でお世話になっているEvil Martiansの記事です。Asset PipelineからWebpackに移行した事情などを解説しています。


「Sprocketsの改修(左)が盛り下がる一方、Webpackの改修頻度は高まっている」
同記事より


つっつきボイス: 「これはいいまとめ記事」「PostCSSって初めて知った」「そういえばSassとかCompassってRubyで書かれているけど、そういうのもJSでやりたいってことか: 気持わかる」「Compassって一見便利だけどバッドノウハウの塊ですね」「(Compassに入れ込んだ日々を返してくれ…)」

参考: PostCSS まとめ


postcss.orgより

ニューオリンズRubyカンファレンス動画47本(Ruby Weeklyより)

他にRubyConf 2017を振り返る記事も紹介されていました。


つっつきボイス: 「時間をかけて動画を見る余裕はなかなかないなー」「ところで最近YouTubeの自動字幕の日本語機械翻訳、前より良くなったっぽいですね」「それでも英語の自動字幕の方が精度高いですね: 大文字小文字を区別してたりして驚異」

RailsのモデルidにPostgreSQLのUUIDを使う(Ruby Weeklyより)

短い記事です。新規プロジェクト以外はやめておくほうがよいかもだそうです。


つっつきボイス: 「そりゃもう、IDを後から変えるとか自殺行為w」「ところでID生成については以下の記事↓がとてもよくまとまってますね」

参考: Qiita ID生成大全

RailsでHTTP OPTIONSをうまく扱う方法(Ruby Weeklyより)

# route.rb
  match '*path', {
    controller: 'application',
    action: 'options',
    constraints: { method: 'OPTIONS' },
    via: [:options]
  }

# コントローラ
  class Api::V1::UsersController < ApplicationController
    options do
      {
        schemas: {
          accepts: Company.json_schema,
          returns: Company.json_schema
        },
        meta: { max_per_page: 100 }
      }
    end
  end

つっつきボイス: 「あー、確かにRailsでHTTP OPTIONS扱うの割りと面倒ではある」

参考: MDN HTTP OPTIONS

ActionCableを単独で使ってみた(RubyFlowより)

ActionCableそのものの解説も充実しています。

# 同記事より
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :uuid

    def connect
      self.uuid = SecureRandom.urlsafe_base64
    end
  end
end

つっつきボイス: 「ストリーミングだけやりたいときなんかはRailsなくてもいいのかも: それこそmrubyでやれればよさそう」

Rails 5.2の新機能: HashWithIndifferentAccessfetch_values

とても短い記事です。


つっつきボイス:fetch_values今までなかったのかー」「記事からリンクされている今年4月の#28316の方がわかりやすいですね↓」

# 更新前
hash = ActiveSupport::HashWithIndifferentAccess.new
hash[:a] = 'x'
hash[:b] = 'y'
hash.fetch_values('a', 'b') # => KeyError: key not found: "a"

# 更新後
hash = ActiveSupport::HashWithIndifferentAccess.new
hash[:a] = 'x'
hash[:b] = 'y'
hash.fetch_values('a', 'b') # => ["x", "y"]
hash.fetch_values('a', 'c') { |key| 'z' } # => ["x", "z"]
hash.fetch_values('a', 'c') # => KeyError: key not found: "c"

asset_sync: RailsとS3の間でアセットを同期するgem

  #config/environments/production.rb
  config.action_controller.asset_host = "//#{ENV['FOG_DIRECTORY']}.s3.amazonaws.com" # S3の場合
  config.action_controller.asset_host = "//#{ENV['FOG_DIRECTORY']}.storage.googleapis.com" # Google Cloud Storageの場合

つっつきボイス: 「これなかなかよさそう: たとえばアセットをS3に置いてRails側でアセットのプリコンパイルをしなくて済むようにすればデプロイも速くなるし」「yarnただでさえ重いし」「★も1600超えてるし使って大丈夫そう」

多言語化gem「Mobility」が0.3にバージョンアップ

Module Builderパターン」のshioyamaさんがメンテしている多言語化gem: Mobilityがバージョンアップされたとのことです。

  • Rails 5.2とSequel 5をサポート
  • ActiveRecordのdirty系メソッド
  • ロケールのフォールバック
  • モデルをdupすると翻訳も複製
  • etc

つっつきボイス: 「gemがメンテされてるのはいいこと」

RubyのModule Builderパターン #1 モジュールはどのように使われてきたか(翻訳)

i18n-tasks: 国際化/多言語化を支援する静的分析gem


github.com/glebm/i18n-tasksより


つっつきボイス: 「このgem今まで知らなかったんですが、前から結構使われてるみたい: 訳ぬけチェックはありがたいです」「さすがに翻訳ミスまではチェックできないだろうけど」「画像に埋まってる文字の翻訳も手に余るでしょうね」

今見ると、Google翻訳で雑に訳を埋める機能までありました。くれぐれもそのまま本番で使わないようご注意ください。

$ i18n-tasks translate-missing
# accepts from and locales options:
$ i18n-tasks translate-missing --from base es fr

activerecord-cause: ActiveRecordのSQL発行タイミングをログ出力するgem

morimorihogeさんが見つけました。

# spec/spec_helper.rb
User.all

# output to log file.
# D, [2015-04-15T22:13:46.928908 #66812] DEBUG -- :   User Load (0.1ms)  SELECT "users".* FROM "users"
# D, [2015-04-15T22:13:46.929038 #66812] DEBUG -- :   User Load (ActiveRecord::Cause)  SELECT "users".* FROM "users" caused by /Users/joker/srcs/activerecord-cause/spec/activerecord/cause_spec.rb:16:in `block (3 levels) in <top (required)>'

つっつきボイス: 「欲しい機能をさっと作る、さすがjoker1007さん」「ビューが重いと思ったら実は背後のSQLが遅いとかざらにあるけど、そういう問題の解明で便利そう」「bulletでカバーしきれないときとか」

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)

Rails Developers Meetup 2017明日12/09開催

錚々たるメンバーが登壇します。キャンセル待ち223人と大盛況です。


techplay.jp

Ruby trunkより

やっぱりwarn_past_scope: trueしたい

3年前にボツになった同様の#10661を引用し、デフォルトオフでいいので変数名の衝突をチェックできるようにしたいという提案です。


つっつきボイス: 「parse.yだ」「parse.y…」「むかーしparse.yを読んでみたことあるけど手強い」「10000行超えてますしね」

Ruby

mallocでマルチスレッドのRubyプログラムのメモリ使用量が倍増することがある

Puma/Unicorn/Passengerの効率を最大化する設定」のNate Berkopecさんの記事です。


つっつきボイス: 「なかなかヘビーかつ濃厚な内容だけどためになりそう」

Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)

Ruby 3×3の進捗ってどうよ

Ruby 3×3の動きに注目しつつ、昨年提案されたGuildという手法(参考: 「Concurrency in Ruby 3 with Guilds」)による並列化がRuby trunkに見当たらないのを残念に思っているそうです。


olivierlacan.comより

参考: A proposal of new concurrency model for Ruby 3(RubyKaigi 2016資料: PDF)

RubyでDSLを書く

# 同記事より
ConstructionGirl.create_structure(:owner) do
  name { "Michael number: #{Time.now.to_i}" }
  age { [20, 18, 30].sample }
end
# => #<Owner:0x000000040d3258 @name=”Michael number: 1511421963", @age=30>

つっつきボイス: 「オレオレDSL、Rubyistなら一度は通る道ですね」

Embulk: Javaで書かれた一括読み込みツール


embulk.orgより

最初気が付かなかったのですが、Java製です。joker1007さんやmgi166さんなどがRubyでプラグインを書いています。


つっつきボイス: 「↓この図が一目瞭然ですね: Fluentdみたいに常に流し込むのでなく、バッチでアップロード&変換までやってくれるやつです」「うまくはまれば某案件で使ってみようかと思っているところ」


www.embulk.orgより

参考: Fluentdのバッチ版Embulk(エンバルク)のまとめ

るびま執筆者募集


つっつきボイス: 「TechRachoからも記事出してみようか」「Railsネタでもいいのかしら」

そういえば今のるびまはMarkdownで書けるようになったのでした↓。

ダウンロードの多いgemオールタイムトップテン(Ruby Weeklyより)


つっつきボイス: 「このダウンロード数、ほとんどはCIが回しているやつでしょうねー」「人気の指標としては当てにできない感じ」

海外のRubyアドベントカレンダー


つっつきボイス: 「雑にしか探していませんが、こんなに少ないと思わなかった:技術向けアドベントカレンダーはほぼ日本だけのような印象でした」「後は韓国に1つあったぐらい」「もともと西洋のアドベントカレンダーはこういう実物↓ですしね: お菓子やおもちゃ入れたりとか」


businessinsider.comより

BPSアドベントカレンダー2017もどうぞよろしく。

データベース

PostgreSQL: NOT NULL制約を追加して高速化(Postgres Weeklyより)

フランス企業のブログ記事はちょっと珍しい気がしました。


medium.com/doctolib-engineeringより


つっつきボイス: 「NOT NULLしないとたいてい遅くなりますね」

Pgexercise.com: PostgreSQLの出題サイト(Postgres Weeklyより)



pgexercises.comより


つっつきボイス: 「おー、これいいじゃない! 採用面接で目の前でやってもらうとか」「インターフェイスもいいですね」

一同でとりあえずいくつか解いてみたりしました。

pg_hexedit: PostgreSQLのリレーションファイル向け16進エディタ(Postgres Weeklyより)

wxHexEditorという16進エディタを元にしているようです。

pgeoghegan.blogspot.jpより


つっつきボイス: 「こういうのを持ち出すときは最後の手段ですねw」

check_pgactivity: NagiosのPostgreSQLプラグイン(Postgres Weeklyより)

# OPMDG/check_pgactivityより
check_pgactivity -p 5433 -h slave --service hit_ratio --dbexclude idelone --dbexclude "(?i:sleep)" -w 90% -c 80%

JavaScript

JavaScriptのthisって結局何?

割と短い記事です。詳しくはMDN: thisを見て欲しいとのことでした。

CSS/HTML/フロントエンド

カスタムプロパティ(CSS変数)入門

/* 同記事より */
    --width: 80%
    @media screen and (min-width: 768px) and (max-width: 1020px) {
        --width: 60%;
    }
    @media screen and (min-width: 1020px) {
        --width: 40%
    }

つっつきボイス: 「最初CSS変数という言葉を見ていつの間に?と思ったら、babaさんに『普通カスタムプロパティって言いますね』とツッコまれました」「記事にも書いてありますね」「ただしまだ使うには早い」「IE11で動かないんですね」「それにしても--で書くのかー」「Sassみたいに$にして欲しかった」

Sonarwhal: Webサイトをチェックするサービス(Frontend Focusより)


sonarwhalのマスコットnellieちゃん
24ways.orgより

オンライン版の他にコマンドライン版もあるのが特徴です。


24ways.orgより


つっつきボイス: 「CLIで動くならCIと連携できるということだから、どっかに出力しておくのは悪くない気がする: 全部の項目に対応することもないとは思うけど」

(遠い)未来のCSS(Frontend Focusより)


つっつきボイス: 「Houdiniというタスクフォースをこれで知りました: 仕様書いてる人のスライドだそうです」「Houdiniは結構有名ですね」「それにしてもこのスライド…めくりのアニメーションが見づらい」

参考: CSSのHoudiniとは何者か

Houdiniは明らかにマジシャンのハリー・フーディーニですね。

BootstrapよりCSS Gridの方がレイアウト作成に向いていると思う理由(Frontend Focusより)


hackernoon.comより

その他

Kata Container: コンテナ実装のニューフェイス


katacontainers.ioより

コンテナ間でカーネルを共有しないタイプの実装で、OCI(Open Container Initiative)に準拠しているそうです。


www.opencontainers.orgより

参考: コンテナの軽量さと仮想マシンの堅牢さを兼ね備えた新しいコンテナ実装「Kata Containers」、OpenStack Foundationが発表

deep-image-prior: 相当崩れた画像も復元するニューラルネットワークツール(Python)


つっつきボイス: 「↓このレベルで復元するのか」「しかも学習不要らしいです」「この辺は研究でもホットな分野なので、多分論文読めば何やってるかぐらいはふんわりわかる(アルゴリズムはともかく概要ぐらいは)」

Matzインタビュー

Project-modeからProduct-modeの時代へ


martinfowler.comより

かのMartin Fowler先生のサイトの記事です(筆者は別の人)。ソフトウェア開発をプロジェクトではなくProduct-modeというコンセプトで運営する方法論について解説しています。

QNNcloud: NTTが無料で公開した量子ニューラルネットワーク



qnncloud.comより

まだアカウントを作ってみたところです。

番外

日本語の学習はトップレベルに難しいらしい


www.boredpanda.comより

ヨーロッパ住民にとって。

考えるだけで楽器を演奏

ee_at_9e2_short from Thomas Deuel on Vimeo.


つっつきボイス: 「ヘッドギア付けてるとどうしても患者さんっぽく見える」「演奏していると言えるんだろうか」

年号

シンプル化とは

http://gkojax-text.tumblr.com/post/168287716115

鳥のような恐竜の化石がモンゴルで見つかる

作り物感満載。


今週は以上です。

バックナンバー(2017年度後半)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured


Rails5: ActiveRecord標準のattributes API(翻訳)

$
0
0

ActiveRecordに任意の属性を定義したり既存の属性を上書きしたりできるRails標準機能です。

概要

MITライセンスに基いて翻訳・公開いたします。

参考: Rails 5のActive Record attributes APIについて y-yagiさんの良記事です。

Rails5: ActiveRecord標準のattributes API(翻訳)

メソッド

定数

NO_DEFAULT_PROVIDED = Object.new

publicインスタンスメソッド

attribute(name, cast_type = Type::Value.new, **options)Link

型を持つ属性をこのモデルに定義します。必要な場合、既存の属性の型をオーバーライドします。これにより、モデルへの代入時に値がSQLと相互に変換される方法を制御できるようになります。また、ActiveRecord::Base.whereに渡される値の振る舞いも変更されます。これを使って、実装の詳細やモンキーパッチに依存せずに、ActiveRecordの多くに渡ってドメインオブジェクトを使えるようになります。

  • name” 属性メソッドの定義対象となるメソッド名、およびこれを適用するカラム。
  • cast_type: この属性で使われる:string:integerなどの型オブジェクト。利用例について詳しくは以下のカスタム型オブジェクトの情報をご覧ください。

オプション

以下のオプションを渡せます。

  • default: 値が渡されなかった場合のデフォルト値。このオプションを渡さなかった場合、前回のデフォルト値があればそれが使われる。前回のデフォルト値がない場合はnilになる。
  • array:(PostgreSQLのみ)array型にならなければならないことを指定する(以下の例を参照)。

  • range:(PostgreSQLのみ)range型にならなければならないことを指定する(以下の例を参照)。

ActiveRecordで検出される型はオーバーライド可能です。

# db/schema.rb
create_table :store_listings, force: true do |t|
  t.decimal :price_in_cents
end
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
end
store_listing = StoreListing.new(price_in_cents: '10.1')

# 変更前
store_listing.price_in_cents # => BigDecimal.new(10.1)

class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :integer
end

# 変更後
store_listing.price_in_cents # => 10

デフォルト値を指定することもできます。

# db/schema.rb
create_table :store_listings, force: true do |t|
  t.string :my_string, default: "original default"
end

StoreListing.new.my_string # => "original default"
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :my_string, :string, default: "new default"
end

StoreListing.new.my_string # => "new default"

class Product < ActiveRecord::Base
  attribute :my_default_proc, :datetime, default: -> { Time.now }
end

Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
sleep 1
Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600

属性の背後にデータベースのカラムがなくても構いません。

# app/models/my_model.rb
class MyModel < ActiveRecord::Base
  attribute :my_string, :string
  attribute :my_int_array, :integer, array: true
  attribute :my_float_range, :float, range: true
end
model = MyModel.new(
  my_string: "string",
  my_int_array: ["1", "2", "3"],
  my_float_range: "[1,3.5]",
)
model.attributes
# =>
  {
    my_string: "string",
    my_int_array: [1, 2, 3],
    my_float_range: 1.0..3.5
  }

カスタム型の作成

値型で定義されるメソッドと対応していれば、独自の型を定義することもできます。この型オブジェクトでは、deserializeメソッドまたはcastメソッドが呼び出され、データベースやコントローラから受け取った生の入力を取ります。前提とされるAPIについてはActiveModel::Type::Valueをご覧ください。型オブジェクトは既存の型かActiveRecord::Type::Valueのいずれかを継承することが推奨されます。

class MoneyType < ActiveRecord::Type::Integer
  def cast(value)
    if !value.kind_of?(Numeric) && value.include?('$')
      price_in_dollars = value.gsub(/\$/, '').to_f
      super(price_in_dollars * 100)
    else
      super
    end
  end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :money
end
store_listing = StoreListing.new(price_in_cents: '$10.00')
store_listing.price_in_cents # => 1000

カスタム型の作成について詳しくは、ActiveModel::Type::Valueのドキュメントをご覧ください。型をシンボルで参照できるように登録する方法については、ActiveRecord::Type.registerをご覧ください。シンボルの代わりに型オブジェクトを直接渡すこともできます。

クエリ

ActiveRecord::Base.whereが呼び出されると、そのモデルクラスで定義された型が使われ、型オブジェクトでserializeを呼んで値がSQLに変換されます。次の例をご覧ください。

class Money < Struct.new(:amount, :currency)
end
class MoneyType < Type::Value
  def initialize(currency_converter:)
    @currency_converter = currency_converter
  end

  # deserializeまたはcastの結果が値になる
  # ここではMoneyのインスタンスになることが前提
  def serialize(value)
    value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
    value_in_bitcoins.amount
  end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)
# app/models/product.rb
class Product < ActiveRecord::Base
  currency_converter = ConversionRatesFromTheInternet.new
  attribute :price_in_bitcoins, :money, currency_converter: currency_converter
end
Product.where(price_in_bitcoins: Money.new(5, "USD"))
# => SELECT * FROM products WHERE price_in_bitcoins = 0.02230

Product.where(price_in_bitcoins: Money.new(5, "GBP"))
# => SELECT * FROM products WHERE price_in_bitcoins = 0.03412

dirtyトラッキング

属性の型には、dirtyトラッキングの実行方法を変更する機会が与えられます。ActiveModel::Dirtychanged?changed_in_place?が呼び出されます。これらのメソッドについて詳しくはActiveModel::Type::Valueをご覧ください。

define_attribute( name, cast_type, default: NO_DEFAULT_PROVIDED, user_provided_default: true )Link

これはattributeの背後にある低レベルAPIです。型オブジェクトのみを受け取り、スキーマの読み込みを待たずに即座に動作します。自動スキーマ検出と#attributeはどちらもこのメソッドを背後で呼び出します。このメソッドが提供されていることでプラグイン作者によって使われる可能性もありますが、おそらくアプリのコードで#attributeを使うべきです。

  • name: 定義される属性の名前。Stringで定義します。
  • cast_type: この属性で使う型オブジェクト。

  • default: 値が渡されなかった場合のデフォルト値。このオプションを渡さなかった場合、前回のデフォルト値があればそれが使われる。前回のデフォルト値がない場合はnilになる。procを渡すことも可能であり、新しい値が必要になるたびにprocが1度ずつ呼び出される。

  • user_provided_default: デフォルト値がcastdeserializeでキャストされるべきかどうかを指定。

  • GitHubソース

関連記事

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

Rails: dry-rbでForm Objectを作る(翻訳)

Rails: JSON Patchでパフォーマンスを向上(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: JSON Patchでパフォーマンスを向上(翻訳)

FormAPIはPDFを生成するサービスです。バックエンドはRuby on Railsで書かれており、PDFテンプレートエディタはReactで作られています。PostgreSQLを用いてテンプレートのフィールドデータをjsonbカラムに保存しています。

テンプレートエディタの最初のバージョンでは、素朴なアプローチでフィールドデータを保存していました。フィールドが変更されるたびにarray全体をサーバーにpostしていたのです。立ち上げ当初のMVP(Minimum Viable Product)はこの方法で十分動きましたが、やがて顧客がPDFテンプレートに500個ものフィールドを設定するようになり、実行に5〜10秒も要するリクエストにアラートが出始めました。

明らかな解決法は、変更点だけをサーバーに送信することです。ReactアプリではReduxを使っているので、Reduxのアクションを送信することも考えましたが、Reduxのアクションには複雑なロジックが若干含まれているため、私たちのFormAPIではその手が使えませんでした。コードをRubyで書き直したくなかったのですが、仮にNode.jsを使っていたなら同じJavaScriptコードをサーバー側で再利用できたかもしれません。

他の選択肢としては、純粋なJSONオブジェクトの差分を取り出してそれをサーバーに送信する方法が考えられます。その場合JSON Patchを利用できます。

JSON Patchは、JSONドキュメントの変更点を記述するフォーマットです。JSONの一部だけが変更された場合にドキュメント全体の送信を避けるために利用できます。

こういうものは独自にこしらえる1よりも、IETF標準(RFC6902)を利用する方がほとんどの場合よい結果が得られると思います。以下のオープンソースライブラリを利用しました。

  • fast-json-patch — クライアント側でJSON Patchを生成するNPMパッケージです
  • hana — RailsサーバーでJSON Patchを適用するRuby gemです

以下のようにしてRailsモデルのjsonカラムにJSON Patchを適用しました。

class Template < ApplicationRecord
  # エラーが発生したらここでエラーを保存し、
  # バリデーション中にそれらを追加する
  attr_accessor :json_patch_errors
  validate :add_json_patch_errors
  after_save :clear_json_patches

  attr_reader :fields_patch

  # メソッドが呼び出されると即座にJSON Patchが適用される
  def fields_patch=(patch_data)
    # 後でアクセスしたい場合
    @fields_patch = patch_data
    self.json_patch_errors ||= {}
    json_patch_errors.delete :fields

    unless patch_data.is_a?(Array)
      json_patch_errors[:fields] =
        'JSON patch data was not an array.'
      return
    end

    hana_patch = Hana::Patch.new(patch_data)
    begin
      hana_patch.apply(fields)
    rescue Hana::Patch::Exception => ex
      json_patch_errors[:fields] =
        "Could not apply JSON patch to \"fields\": #{ex.message}"
    end
  end

  # データ再読み込み時にすべてのJSON Patchとエラーをクリア
  def reload
    super
    clear_json_patches
  end

  private

  def add_json_patch_errors
    return unless json_patch_errors.present?
    json_patch_errors.each do |attribute, errors|
      errors.add(attribute, errors)
    end
  end

  def clear_json_patches
    @fields_patch = nil
    self.json_patch_errors = nil
  end
end

こちらのRSpecテストをコピーして実装が正しいことを確認できます。そのうちこれをgemとしてリリースするかもしれません。

permittedパラメータとしてfields_patchをコントローラに追加しました。

params.require(:template).permit(
  fields: {},
).tap do |permitted|
  # arrayやhashのネストはややこしい
  if params[:template][:fields_patch].is_a?(Array)
    permitted[:fields_patch] = params[:template][:fields_patch].
      map(&:permit!)
  end
end

上のコードは、:fields_patchを通常の属性として扱い、update_attributesの間にJSON Patchを適用することを示しています。JSON Patchの適用に失敗すると、バリデーション中にエラーが追加されます。

フロントエンド側の実装は実に簡単です。改修前のコードは次のようになっていました。

if (!Immutable.is(template.fields, previousTemplate.fields)) {
  data.fields = template.fields.toJS()
}

新しいコードでは、JSON Patchをfields_patch属性として送信します。

import { compare as jsonPatchCompare } from 'fast-json-patch'

if (!Immutable.is(template.fields, previousTemplate.fields)) {
  data.fields_patch = jsonPatchCompare(
    previousTemplate.fields.toJS(), template.fields.toJS())
}

以下は新しいコードから送信されるAJAXリクエストの例です。

{
  "template": {
    "fields_patch": [
      {
        "op": "replace",
        "path": "/2/name",
        "value": "image_field_2"
      }
    ]
  }
}

コードの変更はわずか数行で済んだにもかかわらず、送信されるデータを大きく削減できました。

JSON Patchのもうひとつのメリットは、複数ユーザーによる同時編集をずっと楽にサポートできることです。JSON Patchは多くの場合任意の順序で適用することができます。replaceinsert操作しか含まれていない場合は特にそうです。衝突が発生した場合、最新のデータをリロードしてユーザーにやり直してもらうだけで済みます。また、WebSocketを使えばサーバーからブラウザにJSON Patchを送信して全クライアントを同期することもできます。

お読みいただきありがとうございました。コメントはHacker Newsまでどうぞ。

関連記事

Rails: ActiveModelSerializersでAPIを作る–Part 1(翻訳)

RailsでGraphQL APIをつくる: Part 1 – GraphQLとは何か(翻訳)

Ruby on Railsで使ってうれしい19のgem(翻訳)


  1. FormAPIの最初のバージョンではフィールド名記述に独自文法を使っていましたが、その後でJSON Pointersというものを発見しました。
    それまでスラッシュ文字/をエスケープするなど思いもよりませんでしたが、仕様の一部に含まれています。 

Rails: Waterfall gemでコントローラとモデルを整理(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: Waterfall gemでコントローラとモデルを整理(翻訳)

ファットモデルや巨大アクションはRails開発者にとって常に問題になっていました。Service Objectに助けてもらうようになったものの、ダメなコードをコントローラから素のRubyオブジェクトに移動しただけなのではないかと思ったりもします。しかしWaterfall gemでこの問題を解決できそうです。早速チェックしてみましょう。

Waterfallについて

Waterfallは、Service Objectの新しいアプローチのひとつです。Waterfallは関数型(functional)的なアイデアに支えられており、ブロックやその他のWaterfallオブジェクトを相互に渡すことで、簡単にチェインできるというものです。オブジェクトのどれかが失敗する(呼び出したときに文字どおり「堰き止められる(dam)」)とWaterfall全体が失敗し、トランザクションでロールバックすることも#reverseフローメソッドを用いて手動でロールバックすることもできます。

平均的なWaterfallサービスは次のような感じになります。

class MakeUserHappy < Struct.new(:user)
  include Waterfall

  def call
    with_transaction do
      chain { MakeUserHappy.new(user) }

      when_falsy { user.happy? }
        .dam { "#{user.name}はまるきり不幸です >_<" }

      chain { notify_happy_user }

      on_dam { |error_pool| puts error_pool + '恥を知れ'}
    end
  end

  private

  def reverse_flow
    make_user_unhappy(user)
  end

  def notify_happy_user
    ...
  end
end

このアプローチの最もよい点は、基本的なService Objectをいくつか作成してそれらを自由に組み合わせられることです。コントローラは次のような感じになります。

def make_user_happy
  authorize(@user)

  Wf.new
    .chain  { MakeUserHappy.new(@user) }
    .chain  { flash[:notice] = 'ユーザーは幸せです!' }
    .on_dam { |err| flash[:alert] = err }

  redirect_to user_path(@user)
end

もちろん#chainメソッドは必要に応じていくつでも追加できますが、コントローラが散らからないようシンプルを心がけています。

用例1: ネスト属性やaroundコールバック

コード例を見てみましょう。ここで行いたいのはネストした属性の除去と、予約(問い合わせ)自身とは別に予約の旅客を更新することです。また、更新の直前や直後に何らかのトラッキングを行う必要もあります。このトラッキングは最終的にオプションにする必要があります。

booking: 予約
enquiry: 問い合わせ

ネストした属性の除去が必須でなければ、around_updateコールバックあたりを使って予約を行い、ネストした属性を使えば済むのでもっと楽にできたでしょう。しかし、around_updateコールバックは属性を即座に代入するため、オブジェクトの古いステートを取得しようとすると困難が生じます。最終的に、このコールバックを状況に応じてスキップする方法を検討せざるを得なくなり、しかも現在のRailsにはきれいな解決方法がありません。結局トラッキングをシンプルにする方向で行うしかありませんでした。これでも既に相当複雑になっています。

そこで私たちは、手順ごとに個別のService Objectを作成してこれらをチェインすることに決めました。更新後のアクションは次のような感じになります。

def update
  authorize @enquiry

  Wf.new
    .chain { UpdateEnquiry.new(@enquiry, enquiry_params, partners_params) }
    .chain do
      redirect_to edit_enquiry_path(@enquiry), notice: '問い合わせの更新に成功しました。'
    end
    .on_dam do |err|
      redirect_to edit_enquiry_path(@enquiry), alert: "問い合わせ更新中のエラー: #{err}."
    end
end

UpdateEnquiryサービスは次のようになります。

class UpdateEnquiry
  include Waterfall

  attr_accessor :enquiry, :enquiry_params, :partners_params

  def initialize(enquiry, enquiry_params, partners_params)
    @enquiry = enquiry
    @enquiry_params = enquiry_params
    @partners_params = partners_params
  end

  def call
    with_transaction do
      TrackEnquiryUpdate.new(enquiry).call do
        chain { UpdatePartners.new(enquiry.partners, partners_params) }

        when_falsy { enquiry.update(enquiry_params) }
          .dam { enquiry.errors.full_messages.join('; ') }
      end
    end
  end
end

ご覧のとおり、#chainメソッドはブロックの内部でも簡単に使えます。これでトラッキングがずっと簡単になり、トラックしたい問い合わせを渡して、どこかのブロックで問い合わせか問い合わせの関連付けを更新するだけで済むようになりました。これはaround_updateコールバックと似ていますが、私たちのルールに沿って動作し、使うかどうかも自由に決められます。TrackEnquiryUpdateのコードは次のとおりです。

class TrackEnquiryUpdate < Struct.new(:enquiry)
  include Waterfall

  def call
    chain do
      enquiry.track(:remove_from_cart)
      yield
      enquiry.reload.track(:add_to_cart)
    end
  end

  private

  def reverse_flow
    ...
  end
end

私たちはService ObjectにStructを使うことにしました。Structには初期化ロジックがないので、コードがずっとスリムになります。Waterfallが堰き止められた場合のカスタムロールバックを実装できる#reverse_flowメソッドにご注目ください。

Ruby on Railsで使ってうれしい19のgem(翻訳)

次の例に進みます。

用例2: 支払い処理とロールバック

私たちの仕事に支払い処理は付きものです。APIがデータベースに支払いを1件作成した後でエラーを返すこともあれば、支払いを1件登録したのにデータベースへの保存に失敗することもあります。Waterfallはこのような問題を断ち切るときにも役立ちます。

How to Choose a Payment Platform for Your Project: PayPal, Stripe, Braintree

コントローラの内部は次のようになります。

def create
  payment_form = CardPaymentForm.new(card_payment_form_params)

  Wf.new
    .chain { EnrollPayment.new(payment_form) }
    .chain { head :ok }
    .on_dam do |errors|
      render json: { msg: errors.join(";\n ") }, status: 422
    end
end

EnrollPayment Waterfallは次のようになります。

class EnrollPayment < Struct.new(:payment_form)
  include Waterfall

  def call
    chain(charge: :charge) do
      ChargeStripe.new(charge_params)
    end

    chain(balance: :balance) do |flow|
      GetStripeBalanceTransaction.new(flow.charge.balance_transaction)
    end

    when_falsy do |flow|
      payment_form.attributes = charge_payment_params(flow.charge, flow.balance)
      payment_form.save
    end
      .dam { payment_form.errors.full_messages }

    chain { notify_after_payment }
  end

  private

  def charge_params
    payment_form.stripe_charge_params
  end

  def charge_payment_params(charge, balance)
    ...
  end

  def notify_after_payment
    ...
  end
end

API呼び出しとデータベーストランザクションの分離方法にご注目ください。分離したことで、Waterfallはこれらにカスタムロールバックを適用できるようになります。with_transactionブロックはデータベースロールバックを担当します。APIのロールバックについてはChargeStripe Waterfallの方をご覧いただく必要があります。

class ChargeStripe < Struct.new(:stripe_charge_params)
  include Waterfall

  def call
    chain(:charge) { Stripe::Charge.create(stripe_charge_params) }
  rescue Stripe::StripeError => e
    dam([e.message])
  end

  private

  def reverse_flow
    Stripe::Refund.create(charge: self.outflow.charge.id)
  end
end

#reverse_flowはカスタムロールバックで、現在のWaterfallがジョブを終了した後で親のWaterfallが堰き止められた場合にのみ実行されます。したがって、データベースでエラーが発生した場合、つまりGetStripeBalanceTransaction Waterfallが堰き止められた場合、ChargeStripe Waterfallは以前行われた支払いを返金します。

まとめ

Waterfallは、Service Objectの素晴らしい新実装であり、コントローラやモデルに属さないコードをきれいに片付けることができます。サービスは、瓦礫の山のような恐ろしいコードではなく、アクションの連続として明確に表現されます。

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Rails: Service Objectはもっと使われてもいい(翻訳)

PostgreSQL 9.6→10アップグレードのダウンタイムをpglogicalで最小化(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

PostgreSQL 9.6->10アップグレードのダウンタイムをpglogicalで最小化(翻訳)

PostgreSQL 10がリリースされ、9.6クラスタを最新バージョンにアップグレードしてみたくなりました。しかし、以前アップグレードしたときのようにメンテナンス画面を開いてマイグレーションを実行するのに膨大な段取りが必要になるのでしょうか。そのときは、アプリをメンテナンスモードに切り替え、新しいダンプを取って新しいクラスタでリストアし、メンテナンスモードをオフにしました。

この方法だと、アプリは1時間ほど、もしかするともっと長い時間使えなくなるかもしれません。pglogicalを再読した後、9.6から10への切り替えをわずか数秒で完了できるpglogicalを試す決心を固めました。

概要

pglogicalは論理レプリケーションを実装していて、バージョンの異なるデータベース同士でもレプリケーションを行えます。これはPostgreSQL自身が提供するバイナリレプリケーションでは不可能です。PostgreSQL 10では論理レプリケーション機能のサポートがいくつか追加されましたが、9.6からのレプリケーションを行いたいので、いくつかの外部拡張に頼る必要があります。

pglogicalの必要条件のひとつは、レプリケーションされるすべてのテーブルに主キーが設定されていることです。主キーは単独カラムでなくても構いませんが、主キーの存在は必須です。また、レプリケーションエージェントが動作するために、両方のデータベースにsuperuserでアクセスできる必要もあります。DDLレプリケーションはサポートされず、TRUNCATE CASCADEはレプリケーションされません。特殊な条件ではないので、ほとんどのデータベースについてレプリケーションできるはずです。

ただし、主キーの必須要件には特別な注意が必要です。特に移行前のデータベースで、ActiveRecord gemでデータベースマイグレーションを管理していた場合、以前はschema_migrationsテーブルに主キーがありませんでした。その場合は次を実行します。

alter table schema_migrations add primary key (version);

アップグレードの方針は、PostgreSQLパッケージをpglogical拡張のサポート付きでインストールしてから、新しいPostgreSQL 10クラスタを作成して、新しいクラスタだけにスキーマをリストアするというものです。現在のクラスタの停止/再起動は、pglogicalを有効にしてインストールしたPostgreSQLを使って行うべきです。このクラスタはTCP/IP経由で他方に接続可能になっている必要があります。プロバイダ側(アップグレード前の9.6データベースを指す)とサブスクライバ側(新しいPostgreSQL 10データベースを指す)には、接続先のIPアドレスとポートをそれぞれ指定する必要があります。pglogical拡張を両方のデータベースで作成し、postgresql.confとpg_hba.confで論理レプリケーションを有効にし、両方のデータベースを再起動します。最後に、プロバイダ、サブスクライバ、そしてサブスクリプションを作成するいくつかのpglogical文が発行されてレプリケーションが開始されます。レプリケーションが完了したら、新しいクラスタのポートを古いクラスタに合わせて変更して古いクラスタを停止し、新しいクラスタを再起動できます。最後にアプリも再起動するとよいでしょう。特にrow型などのカスタム型がある場合、OIDが変わる可能性が高く、そうしたrow型が登録されているとアプリを再起動するまで期待どおりに動作しないためです。たとえばSequel gemを用いたDB.register_row_typeが使われている場合、これに該当する可能性があります。

最後の切り替えは可能な限り数秒以内に行われるので、ダウンタイムを最小限にできます。

手順のハンズオン

私たちのサーバーではアプリの他にDockerでPostgreSQLを実行しているので、本記事では手順の説明にもDockerを使いますが、他のセットアップでの手順にも簡単に応用できるはずです。Dockerでデモを行う利点は、(Dockerコンテナを)そのまま簡単に複製できることと、データベースの作成や実行にも対応できる点です。

本記事では、PostgreSQLクライアントがホストにもインストールされていることが前提です。

Dockerイメージと起動スクリプトの準備

以下のDockerfileをpg96/とpg10/サブディレクトリにそれぞれ作成します(Dockerコンテナ内でPostgreSQLを実行していない場合、自分の環境で複製するにはDockerfile内の手順をご覧ください)。

# pg96/Dockerfile
FROM postgres:9.6

RUN apt-get update && apt-get install -y wget gnupg
RUN echo "deb [arch=amd64] http://packages.2ndquadrant.com/pglogical/apt/ jessie-2ndquadrant main" > /etc/apt/sources.list.d/2ndquadrant.list\
  && wget --quiet -O - http://packages.2ndquadrant.com/pglogical/apt/AA7A6805.asc | apt-key add -\
  && apt-get update\
  && apt-get install -y postgresql-9.6-pglogical

RUN echo "host    replication          postgres                172.18.0.0/16   trust" >> /usr/share/postgresql/9.6/pg_hba.conf.sample
RUN echo "host    replication          postgres                ::1/128         trust" >> /usr/share/postgresql/9.6/pg_hba.conf.sample
RUN echo "shared_preload_libraries = 'pglogical'" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "wal_level = 'logical'" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "max_wal_senders = 20" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "max_replication_slots = 20" >> /usr/share/postgresql/postgresql.conf.sample
# pg10/Dockerfile
FROM postgres:10

RUN rm /etc/apt/trusted.gpg && apt-get update && apt-get install -y wget
RUN echo "deb [arch=amd64] http://packages.2ndquadrant.com/pglogical/apt/ stretch-2ndquadrant main" > /etc/apt/sources.list.d/2ndquadrant.list\
  && wget --quiet -O - http://packages.2ndquadrant.com/pglogical/apt/AA7A6805.asc | apt-key add -\
  && apt-get update\
  && apt-get install -y postgresql-10-pglogical

RUN echo "host    replication          postgres                172.18.0.0/16   trust" >> /usr/share/postgresql/10/pg_hba.conf.sample
RUN echo "host    replication          postgres                ::1/128         trust" >> /usr/share/postgresql/10/pg_hba.conf.sample
RUN echo "shared_preload_libraries = 'pglogical'" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "wal_level = 'logical'" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "max_wal_senders = 20" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "max_replication_slots = 20" >> /usr/share/postgresql/postgresql.conf.sample

両方のサーバーはIPアドレス10.0.1.10の同じコンピュータ上で実行されることを前提とします。9.6のインスタンスはポート5432で、新しいクラスタは当初(切り替え前)ポート5433で実行されます。

cd pg96 && docker build . -t postgresql-pglogical:9.6 && cd -
cd pg10 && docker build . -t postgresql-pglogical:10 && cd -

本記事はDockerのチュートリアルではありませんが、Dockerを実際に使っている場合は、これらのDockerイメージをプライベートなレジストリにpushするとよいかもしれません。

最初の手順では、古い9.6クラスタを停止し、古いデータを持つクラスタのpglogicalを有効にして起動します(作業前には常にバックアップを取っておくようにしましょう)。クラスタデータは/var/lib/postgresql/9.6/main/に、設定ファイルは/etc/postgresql/9.6/main/にそれぞれ置かれているとします。/etc/postgresql/9.6や/var/lib/postgresql/9.6がない場合もスクリプトが新しいクラスタを作成してくれるので心配無用です(なお、最初に新しいデータベースを試してみたい場合は一時ディレクトリをいくつかマップしておくとよいでしょう)。

以下のスクリプトを/sbin/pg-scripts/start-pgに作成し、ファイルに実行可能属性を与えます。このスクリプトはコンテナのデータベースを実行します。

#!/bin/bash
version=$1
net=$2
setup_db(){
  pg_createcluster $version main -o listen_addresses='*' -o wal_level=logical\
        -o max_wal_senders=10 -o max_worker_processes=10 -o max_replication_slots=10\
        -o hot_standby=on -o max_wal_senders=10 -o shared_preload_libraries=pglogical -- -A trust
  pghba=/etc/postgresql/$version/main/pg_hba.conf
  echo -e "host\tall\tappuser\t$net\ttrust" >> $pghba
  echo -e "host\treplication\tappuser\t$net\ttrust" >> $pghba
  echo -e "host\tall\tpostgres\t172.17.0.0/24\ttrust" >> $pghba
  echo -e "host\treplication\tpostgres\t172.17.0.0/24\ttrust" >> $pghba
  pg_ctlcluster $version main start
  psql -U postgres -c '\du' postgres|grep -q appuser || createuser -U postgres -l -s appuser
  pg_ctlcluster $version main stop
}
[ -d /var/lib/postgresql/$version/main ] || setup_db
exec pg_ctlcluster --foreground $version main start

クラスタが存在しない場合は、このスクリプトによって新しいクラスタが作成されます。なお実際のレプリケーションに必要というわけではありませんが、このスクリプトは作業をシンプルにするため「trust」でsuperuser認証された新しいappuserデータベース作成もサポートします。これはテスト目的で新しいデータベースを生成したい場合に役立つことがあります。必要な場合はこのスクリプトを適宜調整してユーザー名や認証方法を変更します。

コンテナを実行する

9.6クラスタをポート5432で実行します(試すだけなら、別のポートで実行したり一時ディレクトリにマッピングしたりしても構いません)。

docker run --rm -v /sbin/pg-scripts:/pg-scripts -v /var/lib/postgresql:/var/lib/postgresql\
    -v /etc/postgresql:/etc/postgresql -p 5432:5432 postgres-pglogical:9.6\
    /pg-scripts/start-pg 9.6 10.0.1.0/24
# since we're running in the foreground with the --rm option, run this in another terminal:
docker run --rm -v /sbin/pg-scripts:/pg-scripts -v /var/lib/postgresql:/var/lib/postgresql\
    -v /etc/postgresql:/etc/postgresql -p 5433:5432 postgres-pglogical:10\
    /pg-scripts/start-pg 10 10.0.1.0/24

start-pgの第1引数はPostgreSQLのバージョン、第2引数と最後の引数はpg_hba.confが存在しない場合の作成に使うネットワークです。これは、appuserが「trust」認証方法で接続するのに使われます。

Dockerコンテナをsystemdサービスとして動かしてみたい方は、原文記事末尾のコメントでお知らせいただければ時間のあるときに補足します(実は難しくありません)。ドキュメントはネット上にいろいろありますが、私たちの独自サービスユニットファイルはこうしたチュートリアルと少し違っている部分があります。違いは、サービス起動時にポートが接続を受け付けるかどうかをチェックしているのと、(Docker)イメージが既にローカルにある場合はレジストリからpullしないようになっている点です。

PostgreSQL設定を編集する

Postgresql-pglogicalコンテナを使うファイルが古いクラスタで実行できるようになったら、postgresql.confファイルを更新してコンテナを再起動します。以下の設定は9.6クラスタと10クラスタのどちらの設定のベースとしても使えます。

wal_level = logical
max_worker_processes = 10
max_replication_slots = 10
max_wal_senders = 10
shared_preload_libraries = 'pglogical'

pg_hba.confには以下の行を含めてください(Dockerを使っていない場合や、デフォルト以外のネットワークでコンテナを実行している場合は、ネットワーク設定を変更してください)。

host    all     postgres        172.17.0.0/24   trust
host    replication     postgres        172.17.0.0/24   trust

サーバーを再起動すればレプリケーション開始の準備が整います。

データベースのレプリケーションを実行する

プロバイダのセットアップ

PostgreSQL 9.6データベースで以下を実行します。

# PostgreSQLでリストアするスキーマをダンプする
pg_dump -Fc -s -h 10.0.1.10 -p 5432 -U appuser mydb > mydb-schema.dump
psql -h 10.0.1.10 -p 5432 -c 'create extension pglogical;' -U appuser mydb
psql -h 10.0.1.10 -p 5432 -c "select pglogical.create_node(node_name := 'provider', dsn := 'host=10.0.1.10 port=5432 dbname=mydb');" -U appuser mydb
psql -h 10.0.1.10 -p 5432 -c "select pglogical.replication_set_add_all_tables('default', ARRAY['public']);" -U appuser mydb

# 私の場合シーケンスレプリケーションが動かなかったので、データベース切り替えの直前には別の方法をおすすめします
# psql -h 10.0.1.10 -p 5432 -c "select pglogical.replication_set_add_all_sequences('default', ARRAY['public']);" -U appuser mydb

これで、パブリックなスキーマのテーブルとシーケンスがレプリケーション対象としてマーキングされます。

サブスクライバとサブスクリプションのセットアップ

PostgreSQL 10データベースで以下を実行します。

# データベーススキーマの作成とリストア
createdb -U appuser -h 10.0.1.10 -p 5433 mydb
pg_restore -s -h 10.0.1.10 -p 5433 -U appuser -d mydb mydb-schema.dump
# pglogical拡張をインストールしてサブスクライバとサブスクリプションをセットアップ
psql -h 10.0.1.10 -p 5433 -c 'create extension pglogical;' -U appuser mydb
psql -h 10.0.1.10 -p 5433 -c "select pglogical.create_node(node_name := 'subscriber', dsn := 'host=10.0.1.10 port=5433 dbname=mydb');" -U appuser mydb
psql -h 10.0.1.10 -p 5433 -c "select pglogical.create_subscription(subscription_name := 'subscription', provider_dsn := 'host=10.0.1.10 port=5432 dbname=mydb');" -U appuser mydb

これで、レプリケーションのステータスを以下でチェックできるようになります。

select pglogical.show_subscription_status('subscription');

初期化が完了してデータベースが同期およびレプリケーションされると(データベースのサイズによってそれなりに時間がかかります)、切り替えを開始できるようになります。

シーケンス値のレプリケーション

この時点でデータベースのレプリケーションはほぼ完了です。シーケンス値のレプリケーション方法については見つけられませんでした。シーケンスに依存するシリアル整数主キーカラムを使っている場合は正しいシーケンス値も設定したいはずであり、さもないとシリアルシーケンスの次の値に依存する新しいレコードをINSERTできなくなってしまいます。その方法についてここで説明します。set-valueステートメントを生成した後で古いサーバーを停止する時間を確保できるよう、データベースへの大量書き込み集中に備えてギャップ値5000を挿入しています。おそらくこのギャップ値は、スクリプト実行してからサーバーが停止までの間にデータベースがどのぐらい急速に増大するかに応じて適宜調整が必要です。

psql -h 10.0.1.10 -p 5432 -U appuser -c "select string_agg('select ''select setval(''''' || relname || ''''', '' || last_value + 5000 || '')'' from ' || relname, ' union ' order by relname) from pg_class where relkind ='S';" -t -q -o set-sequences-values-generator.sql mydb
psql -h 10.0.1.10 -p 5432 -U appuser -t -q -f set-sequences-values-generator.sql -o set-sequences-values.sql mydb
# 新しいシーケンス値を新しいデータベース(例ではポート5433)に設定する
psql -h 10.0.1.10 -p 5433 -U appuser -f set-sequences-values.sql mydb

最後の切り替え手順

次は基本的に、PostgreSQL 10クラスタのポートを5432に変更します(古いクラスタのポートであれば何でも構いません)。続いて9.6クラスタを停止し(上の例ではCtrl-Cで停止)、新しいクラスタを再起動します。最後に、変換ルールでrow型のOIDに依存しているカスタム型がある場合は、データベースを使っているアプリも再起動するとよいでしょう。

ここでは、SQLステートメント発行の前に何らかの接続バリデーションを用いてプール内のコネクションを安全に切断できるアプリを仮定しています。そうでない場合は、postgresql.confやpg_hba.confをいじった後にデータベースを再起動したら、必ずアプリも再起動するのがよいでしょう。

クリーンアップ

新しいデータベースでの正常な動作を確認できたら、次のようにして片付けられます。

select pglogical.drop_subscription('subscription');
select pglogical.drop_node('subscriber');
drop extension pglogical;

この記事がダウンタイム最小限のデータベースアップグレードに役立つことを願っています。

関連記事

Rails開発者のためのPostgreSQLの便利技(翻訳)

PostgreSQL 10の使って嬉しい5つの機能(翻訳)

PostgreSQLの機能と便利技トップ10(2016年版)(翻訳)

週刊Railsウォッチ(20171215)Ruby 2.5.0-rc1リリース、Ruby 2.4.3セキュリティ修正、Ruby 3.0で変わるキーワード引数、HTML 5.2 RECリリースほか

$
0
0

こんにちは、hachi8833です。クリスマスが迫るとRuby周りが忙しくなりますね。

今週のウォッチ、いってみましょう。

臨時ニュース1: Ruby 2.5.0-rc1リリース


www.ruby-lang.orgより

ついさっき出ました。実は先ほど以下のセキュリティ修正でrbenvを更新したときにrc1の文字が見えていたのでついでにインストールしちゃいました。

臨時ニュース2: Rubyセキュリティ修正リリース

今朝発表がありました。

修正内容

以下の2点です。必要な方はお早めにアップデートしましょう。

プレスリリース(ダウンロードリンクあり)


私もローカル環境とオレオレRailsアプリをRuby 2.4.3にアップデートしました。

Rails: 今週の改修

commit差分から見繕いました。

System TestにFirefox headless driverを追加

# actionpack/lib/action_dispatch/system_testing/driver.rb#67
             browser_options.args << "--headless"
             browser_options.args << "--disable-gpu"

+            @options.merge(options: browser_options)
+          elsif @browser == :headless_firefox
+            browser_options = Selenium::WebDriver::Firefox::Options.new
+            browser_options.args << "-headless"
+
             @options.merge(options: browser_options)
           else
             @options
           end
         end

         def browser
-          @browser == :headless_chrome ? :chrome : @browser
+          if @browser == :headless_chrome
+            :chrome
+          elsif @browser == :headless_firefox
+            :firefox
+          else
+            @browser
+          end
         end

つっつきボイス: 「Firefox Quantamが速くなったらしいのと関係あるのかな」

参考: Firefox Quantumが激重になる問題が発生中

非推奨メソッドがpublicに変わらないよう修正

# activesupport/lib/active_support/deprecation/method_wrappers.rb#60
               deprecator.deprecation_warning(method_name, options[method_name])
               super(*args, &block)
             end
+
+            case
+            when target_module.protected_method_defined?(method_name)
+              protected method_name
+            when target_module.private_method_defined?(method_name)
+              private method_name
+            end
           end
         end

つっつきボイス: 「お、お、こんなバグあったとは」「非推奨メソッドが削除されるまでの間ということですね」

非推奨のBigDecimal#newを削除

# activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb#6
      module OID # :nodoc:
         class Decimal < Type::Decimal # :nodoc:
           def infinity(options = {})
-            BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
+            BigDecimal("Infinity") * (options[:negative] ? -1 : 1)
           end
         end
       end

つっつきボイス:BigDecimal#newじゃなくてBigDecimalになるのか」「RailsのActiveSupportなのかなと思ったらRubyの方だった」「newが非推奨になったのはなぜなんだろう?」「オブジェクトの同一性を担保するためなのかなと思ったけどnewあってもなくてもobject_id違うな↓」

追記: 「IntegerやFloatでnewできないのに合わせたんじゃないんすかね?」

RailsのSTIを修正

# activerecord/lib/active_record/relation.rb#55
-    def new(*args, &block)
-      scoping { @klass.new(*args, &block) }
+    def new(attributes = nil, &block)
+      scoping { klass.new(scope_for_create(attributes), &block) }
     end

つっつきボイス: 「STI: Single Table Inheritance」「この記事↓見るとSTI結構嫌われてる雰囲気ですね」「STIは有用なこともあると思うけど使ったことほとんどなかったナー: カラム増えるし」

参考: みんなRailsのSTIを誤解してないか!?

使われていないwebpack定数を削除

// activestorage/webpack.config.js#1
-const webpack = require("webpack")
 const path = require("path")

 module.exports = {

つっつきボイス: 「誰も使ってないので削除したそうです」

Rails

RailsのArelを使ってコンポジション可能なQuery Builderを書く(RubyFlowより)

# 同記事より
scene = Scene.new(scene_params)
SceneQueryBuilder.new(scene).by_season
SceneQueryBuilder.new(scene).by_episode
SceneQueryBuilder.new(scene).by_dialogue

つっつきボイス: 「Query Builderはよく使うし、いいパターンだと思う: ただ自分はArelじゃなくて生SQLで書くけど」「同じことをActiveRecordだけで書くのはつらくなりやすい」「Builderパターンを説明するときにはSQLを例に使うのが一番わかりやすいと思ってる: みんなもっと使おう」

[保存版]人間が読んで理解できるデザインパターン解説#1: 作成系(翻訳)

Railsのロガー記事2本: ActiveSupport::TaggedLoggingとベストプラクティス

# 元記事1より
#lib/vendor_payment_logger.rb

class VendorPaymentLogger < ActiveSupport::TaggedLogging
  def initialize(logger)
    super(logger)
    logger.formatter = formatter
  end

  def formatter
    Proc.new{|severity, time, progname, msg|
      formatted_severity = sprintf("%-5s",severity.to_s)
      formatted_time = time.strftime("%Y-%m-%d %H:%M:%S")
      "[#{formatted_severity} #{formatted_time} #{$$}]\n #{msg}\n"
    }
  end
end

つっつきボイス: 「最近ロガーで悩んでると聞いたので探してみました」「そうそう、某案件のロガー設計方針」「いつだったか、ロガーはオブジェクト指向的設計からはみ出しやすいって言ってましたね」「まさしく」

リレーションだけが更新されるとupdate_attributeでSQLが発行されない

Gobyの作者@st0012さんからの情報です。Rails 5のissue #25503が以下のやり取りの後未だにcloseしていないそうです。この挙動は2015年に0fcd4cで追加されたそうです。

問題点を見つけた。
changed?メソッドがリレーションの更新をチェックせず、属性の更新だけをチェックしてた。
リレーションが更新されたかどうかをチェックする何かうまい方法を見つける必要がある。個別のテストは動くのにグループになるとコケるテストがあったので、条件を少し追加する必要があった。
25503#issuecomment-250984166より大意


つっつきボイス: 「仕様なのかバグなのか?」「ActiveRecord::Persistence#update_attributeではリレーションの更新には触れてないですね」「ところで『update_attributeはバリデーションがきかないゴミだぞ』って誰か言ってた」「update_attribute(key, value) というAPIも残念」「バリデーションが効かないからゴミというわけではない(過去にやらかした経験上)」

参考: ActiveRecord の attribute 更新方法まとめ

Rubyアプリが劣化する様子をレストランになぞらえて説明する

Passengerでお馴染みのオランダPhusion社のブログ記事です。レストランに皿洗い機を導入後に処理量を増やすとどうなるかという形でサーバーのワーカーの扱いについて解説しています。


同記事より

テストにおけるstubのコスト

stubは速いしスイスイ通るし便利だけど使い所に注意という記事です。

# 同記事より
describe Order do
  let(:customer) { Customer.new }
  let(:order)    { Order.new(subtotal: 100, customer: customer) }

  describe '#total' do
    context 'a customer has fee' do
      before do
        customer.fee = 21
      end

      it 'returns the total price which includes fee' do
        expect(order.total).to eq(121)
      end
    end

    context 'a customer has no fee' do
      before do
        customer.fee = 0
      end

      it 'returns the total price without any fee' do
        expect(order.total).to eq(order.subtotal)
      end
    end
  end
end

つっつきボイス: 「単体テストだとstubを使う意味ないこと多いっすね」

Passwordless: Railsアプリでパスワードなし認証するgem

class User < ApplicationRecord
  validates :email, presence: true, uniqueness: { case_sensitive: false }

  passwordless_with :email # <-- ここ
end

つっつきボイス: 「とりあえずPasswordlessという名前はわかりやすくていいかも: Deviseに比べれば」

[Rails] Devise Wiki日本語もくじ1「ワークフローのカスタマイズ」(概要・用途付き)

ankaneさんの膨大なメモ

groupdateなどさまざまなgemを出しているAndrew Kaneことankaneさんがいろんなドキュメントをリポジトリに置いているのを見つけました。内容はRails/PostgreSQL/セキュリティ/データサイエンスなどさまざまです。


つっつきボイス: 「BPSもDocbaseにMarkdownドキュメントを社内でじゃんじゃん共有しているけど、それと同じようなノリなのかも」「publicな場所に置いているのエライ」

Markdownで書けるドキュメントコラボレーションサービスを比較する

コーディングスタイルで揉める理由

『オブジェクト指向設計実践ガイド』などでお馴染みのSandi Metz氏の記事です。

  • なぜスタイルガイドを使うのか
  • どのスタイルガイドがいいのか
  • なぜチームで反対されるのか
  • 自分流でやりたいんだけどダメ?
  • チームで合意を取り付けるには
  • 新しいスタイルガイドが性に合わないときは?

つっつきボイス: 「これはどうしてもどこかで揉めるやつ」「永遠の課題」「基本はプロジェクトの既存スタイルに沿うことにして、迷ったらスタイルガイドに沿う、と言うだけなら簡単ですけどね」

【保存版】Rubyスタイルガイド(日本語・解説付き)総もくじ

ワーカープロセスを安全に切り替える

factory_girl改めfactory_botなどでお馴染みのThoughtbot社のブログ記事です。

# 同記事より
#!/usr/bin/env ruby

STDOUT.sync = true

def info
  "PID=#{Process.pid} WORKER_VERSION=1"
end

puts "TICK: Starting process... #{info}"

trap "TERM" do
  puts "TICK: Received SIGTERM #{info}"
  sleep 5 # 重い作業を終了中ということにする
  puts "TICK: Graceful shutdown #{info}"
  exit
end

loop do
  puts "TICK #{info}"
  sleep 2 # 重い作業を実行中ということにする
end

doorkeeper: RailsでOAuth2認証するgem

OAuth2に特化した認証gemです。Railsの他Grapeフレームワークにも対応しているそうです。

# doorkeeper-gem/doorkeeperより
class Api::V1::ProductsController < Api::V1::ApiController
  before_action :doorkeeper_authorize! # 全アクションでトークンを必須にする

  # 以下アクション
end

つっつきボイス: 「doorkeeper、結構定番らしいけど初めて知ったので」「これもいい名前かな」「そうっすか?Doorkeeper.jpと紛らわしそう」

Rails Developer Meetup 2017より


railsdm.github.ioより

大盛況だったRails Developer Meetup 2017のスライドからです。参加したかったー(´・ω・`)

ふつうのRailsアプリケーション開発

とても参考になります。

yuba: Railsの抽象化支援gem

generatorで生成できます。

# willnet/yubaより
rails generate yuba:service create_artist
rails generate yuba:form artist
rails generate yuba:view_model artist_index

rancher: コンテナを継続的に管理

deppbotとtachikoma.io: セキュリティや依存関係の自動更新

Ruby trunkより

Ruby 3でキーワード引数がかなり変わる見通し

詳細は今後多少変わるのかもしれませんが、既にMatzがRubyWorld Conference 2017などでRuby 3で本物のキーワード引数を導入すると言明しているそうです。

# 以下の呼び出しは「キーワード引数」を渡す
foo(..., key: val)
foo(..., **hsh)
foo(..., key: val, **hsh)

# 以下の呼び出しは「通常の引数」を渡す
foo(..., {key: val})        # {} で囲まれている
foo(..., hsh)
foo(..., {key: val, **hsh}) # {} で囲まれている

# 以下のメソッド定義は「キーワード引数」を受け取る
def foo(..., key: val)
end
def foo(..., **hsh)
end

# 以下のメソッド定義は「通常の引数」を受け取る
def foo(..., hsh)
end

Ruby 2のキーワード引数はHashオブジェクト(キーはすべてシンボル)の通常の引数であり、最後の引数として渡されている。この設計を選んだのは互換性のためだったが、かなり複雑になっていて、動作が直感的にならない多くのエッジケースの原因になっていた。
Ruby 3ではキーワード引数は通常の引数と完全に分離される(ブロックパラメータが通常の引数と完全に分離されているのと同様に)。
この変更によって互換性が失われる。キーワード引数を渡したり受け取ったりする場合は、常に({}などで囲まない)むき出しのシンボル値かdouble-splat**が必要になる。
次のような移行パスを考えている:
* Ruby 2.6か2.7あたりで警告を出すようにする: 通常の引数がキーワード引数と解釈可能な場合(またはその逆)
* Ruby 3.0で新しい文法に完全に移行する
#14183より大意


つっつきボイス: 「最初#14176 Unclear error message when calling method with keyword argumentsにしようかと思ったんですが、その後でこのissueが流れてきたので」「Railsも順次対応不可避…ゴク」「実は@st0012さんがちょっと前からGobyのキーワード引数周りの仕様で矛盾に突き当たって悩んでいるんですが、これを見たら無理もないかも」「本当のキーワード引数

Ruby 2.0.0リリース! – キーワード引数を使ってみよう

そこからsplat演算子(*)やdouble-splat演算子(**)の話題になりました。

「splat演算子って、引数とパラメータのどっちにも置けるからまたややこしい」「正直ぱっと見て分からないw」

参考: Rubyの配列展開 *[a, b, c]

Ruby

Ruby 2.5でStructにkeyword_init: trueでキーワード引数が使える

こちらもキーワード引数にちょっと絡みます。

Foo = Struct.new(:a, :b, keyword_init: true)
Foo.new(a: 1, b: 2) #=> #<struct Foo a=1, b=2>

つっつきボイス: 「これはとてもよい」「最初ハッシュが渡せるのかと思ったらキーワード引数だった」「とはいうもののStructにするより普通にクラス書いちゃうことの方が多い: Structって規模感で悩むのと、ダメな使い方になる可能性もあるんで」「Struct#newインスタンスを継承すべからずとかですね」

Rubyスタイルガイドを読む: クラスとモジュール(2)クラス設計・アクセサ・ダックタイピングなど

シングルトンクラスの本当の使われ所

私たちが普通クラスメソッドと呼ぶものは、技術的には、それぞれのシングルトンクラスで定義されたクラスオブジェクトの(シングルトン)インスタンスメソッドです。
同記事より


www.puzzle.chより


つっつきボイス: 「この図↑を見た限りでは、シングルトンクラスも加味すると継承パスはこんなふうに二本立てともみなせるということかな」

RubyConf 2017で発表したお: Rubyで2Dゲーム作った

記事のスライドの見せ方が見事です。


www.blacktm.comより


つっつきボイス: 「↑Apple IIがつい懐かしくって」

最近のYARV-MJIT


つっつきボイス: 「70fpsはoptcarrotのパフォーマンスでしょうね」

Rubyと型


つっつきボイス: 「Matzと_ko1さんの二人がこんなに手こずるなんて、型理論ってどんだけ難しいんだと思ってこの翻訳記事↓見つけました」

参考: 「型」の定義に挑む

「話がバートランド・ラッセルの数理論理学から始まってて、抽象度むっちゃ高い」「きわめつけは以下↓: 厳密に定義しすぎると発展を妨げるって」「(数学科出身につき)それ気持ちわかるわー」「エンジニアの発想じゃなくて数学者の発想だなーと思いました」

“型の定義とは何か?” この質問に明確かつ正確な回答を与えられれば、そこから生じる多くの誤解や無意味な議論を避けることができる。しかし、この質問に対して、そのような明確かつ正確な回答を持ってしまった場合、科学の前進を鈍化させ、”知識の成長を妨げ”、”調査の道筋を既知の狭いチャネル内に引き入れてしまう”ことにもつながりかねない。
postd.ccより

参考: Wikipedia: 型理論

JavaScript

LeaderLine: 指示線を描画するJSライブラリ


github.com/anseki/leader-lineより


つっつきボイス: 「これ大好きー: だいぶ昔にこういうのを力技で実装したときはうまくいかなかったのを思い出した(´・ω・`)」

parcel人気急上昇?

10日しないうちに★10,000に迫る勢いです。

github.com/parcel-bundler/parcelより

設定ファイルなしでも動くというのが殺し文句のようです。


つっつきボイス: 「JS環境選びってギャンブル」「今からでも遅くはない?」

参考: webpack時代の終わりとparcel時代のはじまり

図と例で学ぶawaitasyncFrontend Weeklyより)


nikgrozev.comより


つっつきボイス:asyncとかpromiseはほんと面倒」

JavaScript: 5分でわかるPromiseの基礎(翻訳)

mapforEachの違い

パフォーマンスも含めて比較した記事です。

// 同記事より
arr.forEach((num, index) => {
    return arr[index] = num * 2;
});
// arr = [2, 4, 6, 8, 10]
let doubled = arr.map(num => {
    return num * 2;
});
// doubled = [2, 4, 6, 8, 10]

つっつきボイス:map()forEach()はRubyの#map#eachとだいたい同じようなものだと思ってる」「他の言語でforeachを見た気がするけどどの言語だったかな…」

参考: Wikipedia-ja Foreach文

CSS/HTML/フロントエンド

HTML 5.2 RECがリリース

今年10月に出たHTML 5.1 2nd Editionの立場は…

目立つ部分だけ目次レベルでざっとdiff取ってみました(左がHTML 5.2、右がHTML 5.1 2nd Edition)

最近の非同期CSS読み込み(Frontend Weeklyより)

今ならrel="preload"でJSなしでできるということです。

<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'">

はじめてのCSS Gridレイアウト(Frontend Weeklyより)

CodePenを多用して説明しています。

See the Pen CSS Grid: Calendar by Geoff Graham (@geoffgraham) on CodePen.


つっつきボイス: 「そういえばbabaさんが『gridって直下の要素しかgridできないのが不便なんですよね』って言ってました」「display: flex;が見えてあれ?と思ったけどliの中だけだった」

GoogleがSEOスターターガイドを改訂

肝心のガイドはまだ日本語になっていません。

Unicode Consortiumが追加待ちのグリフにフィードバックを募集


blog.unicode.orgより


つっつきボイス: 「文字が全部鼻に見えるw」「これレビューできる人世界に何人というレベルでは」

今気づきましたが、17372r-n4923-dam1-chart.pdfに旧かな文字まで並んでいて絶句してしまいました。もはや誰得。


17372r-n4923-dam1-chart.pdfより

その他

Microserviceアーキテクチャのパターン


microservices.ioより

昨日BPS社内のエンタープライズ・アーキテクチャ勉強会でMicroserviceのアーキテクチャに言及されたので探してみました。

エンタープライズアプリケーションアーキテクチャパターンを読む: 1.概要

Googleが動画を自動で漫画に変換するアプリを実験公開


research.googleblog.comより


つっつきボイス: 「うう、すげー」「コマ割りまでやってる」「今後結婚式に出席したらきっと山ほど見られますよ」

参考: Google、動画を自動で漫画化する実験アプリ公開。AIポートレートカメラマンやスクラッチ動画生成も

Recho: SlideShareなどにTwitterレスを表示できるChrome拡張


つっつきボイス: 「ほとんどニコ動」

go-torch: Goプログラムをブラウザでビジュアルプロファイリングするツール


つっつきボイス: 「この間もこんなグラフ見ましたね」「名前ど忘れ…frame graphだった」

参考: Go言語のプロファイリングツール、pprofのWeb UIがめちゃくちゃ便利なので紹介する

コードをフェンスの向こうに放り投げる

正規表現のよさげな本

番外

Ruby Tuesday


つっつきボイス: 「asakusa.rbが火曜日開催だから?」「そういうことか」

中国語にひらがなの「の」が絶賛混入中

人間デコーダー

写真は残念ながら削除されたようです。


つっつきボイス: 「この郵便屋さんものすごく優秀なんじゃ」「普通迷宮入りですよね」

むやみになつかしい感


つっつきボイス: 「大昔にTK-80↓の横でラジカセでFM聴いてたら実行/停止に応じていろんな音階が出たのをつい思い出しちゃって」

磁石とクリップ


今週は以上です。

バックナンバー(2017年度後半)

週刊Railsウォッチ(20171208)最近のRailsフロントエンド事情、国際化gem、mallocでRubyのメモリが倍増、るびま記事募集ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Frontend Weekly

frontendweekly_banner_captured

Viewing all 1381 articles
Browse latest View live