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

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

$
0
0

こんにちは、hachi8833です。いつの間にかもう5月が終わろうとしています。

それでは今週のRailsウォッチいってみましょう。

Rails: 今週の改修(Rails公式より)

今回はRailsコミット系を1トピックにまとめてみました。タイトルは一応韻を踏んでいます。

リサイクル可能なキャッシュキーを追加

DHH提案の新しい内部機能です。コミットを見るとけっこう大きな改修になっています。

# actionpack/lib/abstract_controller/caching/fragments.rb#L86
+      def combined_fragment_cache_key(key)
+        head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) }
+        tail = key.is_a?(Hash) ? url_for(key).split("://").last : key
+        [ :views, (ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]), *head, *tail ].compact
+      end

DHHのPRメッセージの中ほどをざっくり日本語にしてみました。

この問題は、明確なバージョンを分離してキーを安定化することで解決できる。従来だと”projects/1-20170202145500″のようにキーを組み合わせていたが、”products/1″や関連するバージョン(”20170202145500″など)といった組み合わせてないキーを安定して利用できるようになる。”projects/1″にどれほど頻繁にアクセスしようとも、単に同じキャッシュキーに書き込まれる。つまりこれがリサイクル部分になる。

つっつきボイス: 「#combined_fragment_cache_key自体はそもそもかなりraw levelなメソッドだし、キャッシュにはCacheHelper経由でアクセスするのが普通だから、普通なら直接触ることはないよ、ということか」「less-frequently-accessed-but-still-valid keysって書いてあるのがポイントかな。アクセス頻度は低くてもcache outしたくないようなデータをどうにかしたいと」「fetchにコストや時間がかかるようなデータなら永続化してもいいくらいのことってありますね」「cache versionとcache keyの管理を分離したい、という感じ」

rails consoleコマンドからirbにオプションを渡せるよう再修正

Rails 5からの機能だったのがいつの間にか消えていたので修正したそうです。

つっつきボイス:「↓これかー」「誰だっARGV.clearなんか足したのはw」

# railties/lib/rails/commands/console/console_command.rb#L82
-        ARGV.clear # Clear ARGV so IRB doesn't freak.

データベースのダンプで’SchemaDumper.ignore_tables’が効くように修正

# activerecord/lib/active_record/tasks/mysql_database_tasks.rb#L60
         args.concat(["--routines"])
         args.concat(["--skip-comments"])
         args.concat(Array(extra_flags)) if extra_flags
+
+        ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
         if ignore_tables.any?
+          args += ignore_tables.map { |table| "--ignore-table=#{configuration['database']}.#{table}" }
+        end
+
         args.concat(["#{configuration['database']}"])

         run_cmd("mysqldump", args, "dumping")

MySQLのほか、PostgreSQLやSQLiteも同様に修正されています。

つっつきボイス:「この機能、あれば使うかも」

キャッシュ機能にunless_existオプションを追加

# activesupport/lib/active_support/cache/strategy/local_cache.rb#L117
          def write_entry(key, entry, options)
-            local_cache.write_entry(key, entry, options) if local_cache
+            if options[:unless_exist]
+              local_cache.delete_entry(key, options) if local_cache
+            else
+              local_cache.write_entry(key, entry, options) if local_cache
+            end
+
             super
           end

unless_exist、つまりキャッシュが既にあれば上書きしないという処理です。

つっつきボイス: 「この修正ってキャッシュのatomicityがちゃんと保証されているのかな?」「できるかどうかはキャッシュエンジンに依存しそうだし」「unless_existチェックが終わってから書き込むまで間に別のプロセスが書き込むとかありえる」

Rubyのバックエンドパフォーマンス改善(RubyWeelkyより)

RailsConf 2017で行われたパネルディスカッションです。40分ほどの動画と概要が紹介されていて、パフォーマンスについていろいろ参考になります。

途中にこんなことも書いてあります。

Always

Be

Benchmarking

元記事では最後にThe Complete Guide to Rails Performanceをおすすめしています。3月17日のRailsウォッチでもこの書籍のことを簡単に取り上げました。

morimorihogeさんが「お、これはよさそう。『FASTとは何か』の定義から始まってて、ちゃんとしてそうに見える」と早速購入方法を調べ始めました。


http://schneems.com/2017/05/17/ruby-backend-performance-getting-started-guide/より

みっちり読む甲斐がありそうです。まともに買うと99ドルですが、頑張ってクーポンを探すともっと安く買えそうです。

補足

ご多分に漏れず、RailsConf 2017の動画がごっそりYouTubeにアップされていますね。後でチェックしてみようっと。


https://www.youtube.com/results?search_query=RailsConf+2017より

ハイパーメディアクライアントをRubyで書く(RubyWeelkyより)


https://robots.thoughtbot.com/writing-a-hypermedia-api-client-in-rubyより

「ハイパーメディアって何ぞや?」という点が気になりました。http://steveklabnik.github.com/hypermedia-presentation/のスライドがポイントのようですが、おっきな字ばっかりでよくわかりません。


http://www.steveklabnik.com/hypermedia-presentation/#1より

つっつきボイス: 「高橋メソッドはこうやって読むには向いてないなー」

akioさんが同記事の中から見つけた「HAL – Hypertext Application Language」の方がどうやらずっとよくまとまっていますね。HAL、覚えておこう。

Polyfill: 古いRubyで新しい機能を使えるようにするgem(RubyWeelkyより)

JavaScriptの方のPolyfillsを思い出してしまいました。Ruby 2.4、2.3、2.2をターゲットにしています。

つっつきボイス:「ここまでして古いRuby使わなきゃいけないんだろうかw」「Kernelにパッチ当てて新しいRubyのメソッドを使うよりはましなのかな?」

morimorihogeさんが「コード側でRubyバージョンをRUBY_VERSIONで調べて切り替えていたら、おそらくこのgemは効かないのではないか」と指摘しました。「Rubyバージョンをrespond_to?でチェックしているコードならこのgemとやっていけるだろうけど」「いったんconflictしたら根が深そう」とも。

Railsは最新のRubyが推奨されるので、よほど古いRailsアプリでなければ普通にRubyをアップグレードする方がよさそうですね。

RubyでUnicodeを修正する


https://blog.daftcode.pl/fixing-unicode-for-ruby-developers-60d7f6377388より

元記事の著者は名前を見るだけで一発でポーランド人とわかります。Lの小文字に斜めの線が入っているような文字が特徴です。

Unicode絵文字が普及したおかげで、日本を含むアジア方面ではおなじみの文字コードの苦労がやっと海外でも共有され始めているようです。私も珍しく「ふふ、ふふふ、もっと、もっと苦しむがよい」という気持ちが湧き上がってしまいました。

lltsv: LTSVパーサー

morimorihogeさんが見つけました。JSONよりお手軽そうです。

私も、個人的にCSVよりもタブ区切りテキストを好んでいる(=表計算にそのまま貼り付けられるから)ので、LTSVに期待しちゃいます。

Squasher: 増えすぎたマイグレーションを圧縮するgem(RubyFlowより)

年月を経てずらりと増えたマイグレーションを1つのコマンドに集約するgemです。★600越えと人気のほどがうかがえます。
うまく使うとマイグレーションファイルの整理に役立ちそうです。
「実行前にはspringとかzeusは止めましょう」と注意書きがあります。

つっつきながら、「これとよく似たgemがあったはずだけどなー」という話になり、調べてみたところridgepoleというgemでした。こちらはRails標準のマイグレーションの代わりに使う、より本格的なスキーマ管理向けだそうです。

参考: ridgepole {名} : 《建築》棟木

つっつきボイス:「ridgepole、そういえば某プロジェクトで知らないうちに入ってたことありましたよ」

Napsack: CIでテストをノードに分割して比較可能にするgem

Pro版もあるそうです。CIや監視がらみはPro版あり多いですね。類似のgemにrrrspecがあることを教わりましたが、また違うアプローチのようです。

Rubyで学ぶグラフ理論の初歩(RubyFlowより)

あきれるほど短い記事なので、グラフ理論のとっかかりによいと思います。心なしか字も大きめなので読みやすいと思います。あくまでとっかかりですが。

つっつきボイス:「Practical Graph Theoryと名乗るほどの内容じゃないかなー」

Electrino: Chromiumの代わりにChromeブラウザを使うJSアプリフレームワーク

なぜ今までなかったんだと思ってしまいました。20日で★2000越えの快挙です。「まだ機能がろくにないけど」と謙遜気味のREADMEです。

名前からElectronの縮小版ということがわかります。Chromiumエンジンを丸呑みしているElectronだと100MB越えなのに、Electrinoだと100kBクラスですよ、閣下。

つっつきボイス:「cookieやプロファイルはブラウザと共用するのかな」「それはないな、プロファイルはちゃんと仕切られるはず」

私はnativefierというElectronでWebページをアプリ化するのをよくやりますが、Electrinoに乗り換えてほしいものです。

Go言語1.8.2、1.8.3セキュリティアップデート

1.8.2では暗号化の楕円関数あたりが修正されましたが、直後に1.8.3がリリースされました。めまぐるしいです。

HPEが160テラバイトのメモリ空間を持つ「The Machine」のプロトタイプを発表

テラバイト、ペタバイト、エクサバイト、ゼタバイトの次はヨタバイトなんですね。


今週は以上です。

関連記事

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h


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

$
0
0

こんにちは、hachi8833です。
Railsの認証用gemの定番中の定番であるDevise gemHow-To Wiki目次のうち、「ワークフローのカスタマイズ」の章見出しを日本語化し、概要や用途を付けました。今後も随時更新します。

Devise How-To原文は走り書きの傾向があり、見出しが内容を適切に表していなかったり、メモ程度の記述しかないことがあります。しかも記事を開いてみないと更新日がわからないので、こういうのが自分でも欲しかったのでした。

ワークフローのカスタマイズ

(以下は今後新しい順に並べ替えます)

ユーザーのパスワードを自動生成する(シンプルな登録方法)

28 Jan 2014 · 3 revisions
ユーザーのパスワードを自動生成する(シンプルな登録方法)原文

新規ユーザーにメールアドレスだけを入力させ、パスワードを自動生成してメール送信する方法です。

sign_inとsign_outのデフォルトルーティングを変更する

5 Nov 2016 · 7 revisions
sign_inとsign_outのデフォルトルーティングを変更する原文

Deviseのデフォルトルーティング(/users/sign_in/users/sign_out)を/login/logoutなどに変更する方法です。

デフォルトのSign_up登録パスをカスタムパスに変更する

20 Aug 2016 · 5 revisions
デフォルトのSign_up登録パスをカスタムパスに変更する原文

新規ユーザーの登録(=サインアップ)URLをDeviseデフォルトの/users/sign_upから/sign_upなどに変更する方法です。

ユーザー登録ページへのルーティングをカスタマイズする

Aug 2012 · 2 revisions
ユーザー登録ページへのルーティングをカスタマイズする原文

Deviseのユーザーモデルが2つある場合にそれぞれについてsign_upsign_inを設定する方法です。

サインイン/サインアウト時に特定のページにリダイレクトする

2 Sep 2016 · 14 revisions
How To: Redirect to a specific page on successful sign in out

ログイン後やログアウト後のリダイレクト先を特定のページに変更する方法です。

ユーザープロファイル編集後のリダイレクトをカスタマイズする

5 Apr 2015 · 11 revisions
How To: Customize the redirect after a user edits their profile

ユーザーが自分のプロファイルを変更した後に特定のページにリダイレクトする方法です。Devise::RegistrationsControllerのサブクラスを作る方法と、ルーティングだけで行う方法の2つが紹介されています。

セッション終了(サインアウト)後のリダイレクト先をカスタマイズする

17 Feb · 8 revisions
How To: Change the redirect path after destroying a session i.e. signing out

ユーザーがログアウトした後に特定のページにリダイレクトする方法です。モデルのインスタンスが複数ある場合の方法も紹介されています。

確認をオーバーライドして、確認中にユーザーが独自のパスワードを選べるようにする

Jul 2016 · 73 revisions
How To: Override confirmations so users can pick their own passwords as part of confirmation activation

ユーザー名かメールアドレスだけを入力すればアカウントを作れるアプリで、ユーザーが自分に送信された登録確認メールのリンクをクリックすると、アプリの登録確認画面でユーザーが自分の使いたいパスワードを選択できるようにする方法です。以下の場合について詳細な手順が記載されています。

  • Rails 3/4、Devise 3.1〜3.5.1
  • Rails 3/4、Devise 2.x〜3.0、Devise 3.5.2以降
  • Rails 2.3、Devise 1.0.9

アカウント削除後のUserデータを保存する(論理削除)

11 Feb 2016 · 6 revisions
How to: Soft delete a user when user deletes account

ユーザーがアカウントを削除してもユーザーのデータを実際には消さずに保存しておき、単にアカウントを無効にする方法です。

ローカライズされたスコープでOmniAuthを使う

18 Mar 2015 · 4 revisions
How To: OmniAuth inside localized scope

セッションで設定されているロケールのスコープに沿ってOmniAuthで認証する方法です。

認証失敗後のリダイレクトにロケールを適用する

18 Feb 2015 · 5 revisions
How To: Redirect with locale after authentication failure

認証失敗後のリダイレクト先ページをロケールに応じて変える方法です。Railsのi18n APIに記載されている方法とは異なります。

管理者がアカウントを有効にするまでsign_inできないようにする

18 Aug 2016 · 17 revisions
How To: Require admin to activate account before sign_in”

confirmableモジュールを使わず、管理者(モデレーター: moderator)が承認するまでユーザーのアカウントを有効にしないようにする方法です。

Deviseで単一ユーザーシステムを構築する

10 Aug 2016 · 10 revisions
How To: Set up devise as a single user system

個人ブログアプリなど、ユーザーが1人に限定されたプライベートアプリをDeviseで構築する際に、新規ユーザー登録ページを非公開にする方法です。ユーザーが1人も登録されていない状態で一般ユーザーが登録ページにアクセスしようとするとトップにリダイレクトされ、ユーザーが1人登録済みの場合はsign_inページにリダイレクトされます。

2段階認証

11 Jun 2016 · 2 revisions
How To: Two step confirmation

以下の2つの記事への内部リンクのみです。具体的な手法は記載されていません。

サインイン・サインアウト・サインアップ・プロファイル更新後に元のページにリダイレクトする

27 Mar · 35 revisions
How To: Redirect back to current page after sign in, sign out, sign up, update

ユーザーがアカウント操作を行った後、ユーザーが元いたページにリダイレクトされるようにします。

サインアウトに成功したらHTTPSからHTTPにリダイレクトする

13 Jun 2012 · 1 revision
How To: redirect from HTTPS to HTTP on successful sign out

ログイン中はアプリへのアクセスをHTTPSにし、ログアウトしたらHTTPに戻す方法です。

サインインに成功したら特定のページにリダイレクトする

15 Jun 2016 · 21 revisions
How To: redirect to a specific page on successful sign in

ログイン後に直前のページに戻る方法、OAuthでログイン後に直前のページに戻る方法、OAuthでログインできなかった場合に直前のページに戻る方法、リダイレクトのループを防ぐ方法が紹介されています。

サインインおよびサインアウトに成功したら特定のページにリダイレクトする

15 Jun 2013 · 2 revisions
How To: Redirect to a specific page on successful sign in and sign out

ApplicationControllerに#after_sign_in_path_for#after_sign_out_path_forを定義してリダイレクトする方法と、ログアウト後にリダイレクトしないようにする方法です。

サインイン成功時、またはサインアウト成功時に特定のページにリダイレクトする

15 Jun 2013 · 2 revisions
How To: Redirect to a specific page on successful sign in and sign out

Deviseのデフォルトであるroot_pathへのリダイレクトをモデル名_root_pathで変更できます。ルートURL /にアクセスした一般ユーザーを単にログインページにリダイレクトしたい場合は、#authenticatedというルーティングヘルパーメソッドを使うのが簡単です。
Rails 4以後は名前の同じルーティングが2つあるとアプリ読み込み時に例外が発生しますので注意が必要です。authenticated_root_pathroot_pathは異なるルーティング名ですが同じURLに応答します。

サインイン/アウトに成功したら特定のページにリダイレクトする

2 Sep 2016 · 14 revisions
How To: Redirect to a specific page on successful sign in out

ApplicationControllerでafter_sign_in_path_forヘルパーやafter_sign_out_path_forヘルパーを使ってリダイレクトします。サインイン後にユーザーを元のページにリダイレクトするには、request.fullpathを使うのが簡単です。サインアウト後のリダイレクト先は自由に設定できますが、サインイン後のリダイレクト先はそうではありません。

サインアップ(登録)に成功したら特定のページにリダイレクトする

13 Dec 2016 · 13 revisions
How To: Redirect to a specific page on successful sign up (registration)

「Rails 5 + Devise 4.2ではこの方法で動かなかったのでこのエントリは削除する方がよいのではないか」という指摘があります。最終的に「サインイン・サインアウト・サインアップ後に元のページにリダイレクトする」(←注意: How-Toの目次にはエントリがありません)でできたそうです。

ユーザーを認証できなかった場合に特定のページにリダイレクトする

16 Mar 2015 · 17 revisions
How To: Redirect to a specific page when the user can not be authenticated

特定のサブドメインでフルパスのURLを使った例が紹介されています。DeviseはデフォルトではフルパスのURLを使わないため、Devise::FailureAppを継承したカスタムfailure appでフルパスのURLに対応しています。Rails 3と4、Rails 2のサンプルコードがあります。

セッションタイムアウト後にログインページにリダイレクトしないようにする

23 Jul 2014 · 8 revisions
How To: Do not redirect to login page after session timeout

デフォルトのDeviseではセッションタイムアウト後にユーザーをログインページにリダイレクトするので、Devise::FailureAppを継承したカスタムfailure appで動作を変更します。

ゲストユーザーを作成する

4 days ago · 32 revisions
How To: Create a guest user

ゲストのUserオブジェクトを使って、ログインしていない一般ユーザーでもセッションを管理できるようにする方法です。ここではゲストのオブジェクトを作成してidをsession[:guest_user_id]でデータベースに保存し、current_or_guest_userでゲストかどうかを判定します。

ユーザーが自分のパスワードを変更できるようにする

10 Jan · 48 revisions
How To: Allow users to edit their password

ビューだけで行う方法(新しいDeviseとStrong ParametersではApplicationControllerの変更も必要)、既存ユーザーにのみパスワード変更を許可してユーザー登録機能を無効にする方法、Deviseに手を加えずにパスワードの変更方法を独自実装する方法が紹介されています。

全ページで認証を必須にする

16 Jan 2016 · 6 revisions
How To: Require authentication for all pages

全ページで認証を必須にし、かつアプリのルートで「サインインが必要です」メッセージを表示しないようにします。database_authenticatableなしでomniauthableを使う場合は方法が少し異なります。

Deviseでカスタムのメールバリデータを使う

24 Feb · 6 revisions
How to: Use a custom email validator with Devise

メールバリデータのコード例が掲載されています。カスタムメールバリデータはDeviseを改変しなくても使えます。

Devise自身のメールバリデータはあえてメールアドレスを極力リジェクトしない方針なので、入力されたメールアドレスが誤っているとユーザーに配信されない可能性もあります(メール配信側の問題による可能性もあります)。

パスワード変更をユーザーにメール通知する

12 Apr · 7 revisions
Notify users via email when their passwords change

この機能はDevise 3.5.3からありますが、デフォルトではオフになっています。devise.rbで設定を変更するだけで機能を有効にできます。

パスワードの最小文字数をカスタマイズする

1 Oct 2014 · 1 revision
Customize minimum password length

アプリ全体での設定方法と、モデルごとに最小文字数を設定する方法が紹介されています。


補足

  • 原文ではsign inとlog in、sign outとlog outがそれぞれ同じ意味で使われています。訳文では、紛らわしくなる場合を除き、原則として原文に沿ったカタカナを使用します。
  • 同じ手順が複数のカテゴリに含まれていることもあります(原文に従っています)
  • 原文が更新されていることにお気づきの場合は、ぜひ@techrachoまでお知らせください。更新いたします。

関連記事(Devise)

[Rails 5.1] 新機能: delegate_missing_toメソッド(翻訳)

$
0
0

こんにちは、hachi8833です。今回のBigBinaryシリーズは、週刊Railsウォッチでも紹介していなかったRails 5.1の新機能です。

DHHはこのメソッドをdecoratorで使うことを想定してるとのことです。

概要

新機能: delegate_missing_to(翻訳)

#method_missingを使うときには、#respond_to_missing?も併用するべきです。しかし、#method_missing#respond_to_missing?を両方使うとコードが冗長になるのも確かです。

DHHが自ら上げた#23824で、冗長なコードの典型的な例を見ることができます。

(訳注: このコード例はそのままAPIドキュメントでも使われています)

class Partition
  def initialize(first_event)
    @events = [ first_event ]
  end

  def people
    if @events.first.detail.people.any?
      @events.collect { |e| Array(e.detail.people) }.flatten.uniq
    else
      @events.collect(&:creator).uniq
    end
  end

  private
    def respond_to_missing?(name, include_private = false)
      @events.respond_to?(name, include_private)
    end

    def method_missing(method, *args, &block)
      @events.public_send(method, *args, &block)
    end
end

DHHは、こうしたコードを改善するために新しいModule#delegate_missing_toメソッドの利用を提案しています。利用例は次のとおりです。

class Partition
  delegate_missing_to :@events

  def initialize(first_event)
    @events = [ first_event ]
  end

  def people
    if @events.first.detail.people.any?
      @events.collect { |e| Array(e.detail.people) }.flatten.uniq
    else
      @events.collect(&:creator).uniq
    end
  end
end

SimpleDelegatorクラスでは不足な理由

BigBinary社では従来RubyのSimpleDelegatorクラスを使っていました。このクラスの問題は、このdelegatorは実行時にどんな種類のオブジェクトでも使われる可能性があるため、呼び出しが委譲される先のオブジェクトを静的に確定できないという点です。

DHHはこのパターンについて次のように言っています

この程度のシンプルな機能のために継承ツリーをハイジャックしなくても済む方がいい。

(訳注: 続きはこうなっています)

継承とsuperを使うのもどうかと思う。#delegate_missing_toなら意図が明確になる。

#delegateメソッドでは不足な理由

Module#delegateメソッドを使う手もあります。しかしその場合は全メソッドをホワイトリスト化しなければなりませんし、ホワイトリストが非常に長くなってしまうこともあります。以下は私たちの実際の案件から引用した実例コードです。

delegate :browser_status, :browser_stats_present?,
         :browser_failed_count, :browser_passed_count,
         :sequential_id, :project, :initiation_info,
         :test_run, :success?,
         to: :test_run_browser_stats

すべてを委譲するならdelegate_missing_to

状況によってはあらゆるmissing methodを委譲したくなることがありますが、#delegate_missing_toならそうした作業をすっきり行えます。なお、この委譲は委譲先のオブジェクトのpublicなメソッドに対してのみ有効であることにご注意ください。

詳しくは#23930のPRをご覧ください。

訳注

現在の5.1-stableの#delegate_missing_toは以下のようになっています。

# 5-1-stable/activesupport/lib/active_support/core_ext/module/delegation.rb#L262
  # オブジェクト内の呼び出し可能なものはすべて対象にできる
  # (例: インスタンス変数、メソッド、定数)
  def delegate_missing_to(target)
    target = target.to_s
    target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

    module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def respond_to_missing?(name, include_private = false)
        # 見落としのように見えるかもしれないが、privateなものは委譲されないので
        # あえてinclude_privateを渡さないようにしている
        #{target}.respond_to?(name) || super
      end
      def method_missing(method, *args, &block)
        if #{target}.respond_to?(method)
          #{target}.public_send(method, *args, &block)
        else
          super
        end
      end
    RUBY
  end

#23824では#delegate_missing_toが何でも委譲することの是非などについて多少議論が紛糾していました。DHHは最後に「このパターンはdecorator用である」と述べています。

関連記事

[Rails] Devise Wiki日本語もくじ2「認証方法のカスタマイズ」「OmniAuth」(概要・用途付き)

$
0
0

こんにちは、hachi8833です。前回の「ワークフローのカスタマイズ」もくじ」に続き、Devise How-To Wikiの「認証方法のカスタマイズ」部分の目次に概要や用途を加えました。OmniAuthの目次も含まれています。

今後も随時更新いたしますので、原文更新にお気づきの方は@techrachoまでお知らせいただけると助かります。

Devise How-To: 認証方法のカスタマイズ

※新しい順に並べ替えてあります。

ゲストユーザーを作成する

25 May 2017 · 32 revisions
How To: Create a guest user

ゲストのUserオブジェクトを使って、ログインしていない一般ユーザーでもセッションを管理できるようにする方法です。

ここではゲストのオブジェクトを作成してidをsession[:guest_user_id]でデータベースに保存し、current_or_guest_userでゲストかどうかを判定します。

Recaptcha

1 Feb 2017 · 47 revisions
How To: Use Recaptcha with Devise

ReCaptcha gemを使って、ボット除けの画像キャプチャ認証を導入する方法が紹介されています。

パスワードなしでアカウントを編集できるようにする

14 Jan 2017 · 66 revisions
How To: Allow users to edit their account without providing a password

独自のコントローラを作成する方法と、Devise::RegistrationsControllerupdate_resourceをオーバーライドする方法の2つが紹介されています。

サブドメインを使う

2 Dec 2016 · 7 revisions
How To: Use subdomains

Railsアプリでサブドメインを使っている場合にInternet Explorerで生じるエラーの回避方法が紹介されています。設定変更のみで行なえます。

ユーザー名とメールアドレスのどちらでもサインインできるようにする

8 Nov 2016 · 60 revisions
How To: Allow users to sign in using their username or email address

サインインのほか、パスワード変更時にもユーザー名とメールアドレスの両方を使えるようにする方法や、Gmailやme.comで使われている「メールアドレスのユーザー名部分もユーザー名として使えるようにする方法」も紹介されています。

MySQLとMongoid向けの注意事項も含まれています。

LDAPで認証する

27 Oct 2016 · 7 revisions
How To: Authenticate via LDAP

コード例ではwarden(Deviseを支えるRackの認証フレームワーク)を呼んでいます。

(old)シンプルなトークン認証の例

2 Sep 2016 · 14 revisions
How To: Simple Token Authentication Example

この記事は既に古くなっている(TokenAuthenticatableがDeviseから削除されたため)ので、Gistのコードを参照するようにとのことです。

Deviseで単一ユーザーシステムを構築する

10 Aug 2016 · 10 revisions
How To: Set up devise as a single user system

個人ブログアプリなど、ユーザーが1人に限定されたプライベートアプリをDeviseで構築する際に、新規ユーザー登録ページを非公開にする方法です。

ユーザーが1人も登録されていない状態で一般ユーザーが登録ページにアクセスしようとするとトップにリダイレクトされ、ユーザーが1人登録済みの場合はsign_inページにリダイレクトされます。

ユーザーがメールアドレス以外の文字列を使ってサインインできるようにする

12 Mar 2016 · 8 revisions
How To: Allow users to sign in with something other than their email address

メールアドレスをユーザー名に使って欲しくない場合の方法です。config/initializers/devise.rbの設定変更かモデルの変更で行えます。Strong Parameter周りの変更も必要です。あとは必要に応じてビューのレイアウトやdevise.*.ymlのメッセージを変更します。

HTTP Basic認証

25 Jun 2015 · 6 revisions
How To: Use HTTP Auth Basic with Devise

DeviseでいわゆるBASIC認証を行う方法が簡単に紹介されています。なお、BASIC認証やdigest認証はDeviseがなくてもRailsの機能だけでできます。

HTTPS(SSL)を使う

6 Apr 2015 · 11 revisions
How To: Use SSL (HTTPS)

DeviseのビューをHTTPSで保護する方法です。Devise 1.0と1.1で方法が少し異なります。

ただし、この方法で部分的に保護するより、サイト全体をHTTPSで保護することが推奨されています。

メールアドレスだけでユーザー登録を開始できるようにする

26 Oct 2014 · 24 revisions
How To: Email only sign up

フォームでメールアドレスが入力されたら確認メールをユーザーに送信し、ユーザーがメールのリンクからWebサイトに戻ってパスワードを設定できるようにします。

手順1を省略すると、登録フォームでパスワードも入力できるようになります。

Rails 4の場合は、「確認をオーバーライドして、確認中にユーザーが独自のパスワードを選べるようにする」も参照してください。

HTTP 認証

15 Dec 2012 · 2 revisions
How To: Use HTTP Basic Authentication

DeviseでのBASIC認証、digest認証、NginxやApacheと併用する場合の方法が紹介されています。

※BASIC認証やdigest認証はDeviseがなくてもRailsの機能だけでできます。

Deviseでリモート認証する

16 Nov 2012 · 1 revision
How to: Remote authentication with Devise

Remote authentication with deviseへのリンクだけが貼られています。Railsで外部リソースを使う場合の外部リソースの認証方法です。Wardenのstrategyを作成することで行います。

メールアドレスの大文字小文字を区別しないようにする

9 Dec 2011 · 5 revisions
How To: Use case insensitive emails

Deviseの設定を追加するだけで、ユーザー登録、サインイン、パスワードを忘れたときの処理などでメールアドレスの大文字小文字を区別しないようになります。

User.find_by_emailは大文字小文字を区別するので、代わりにUser.find_for_authenticationを使う必要があります。

OmniAuth

※並べ替えは行っていません。

OmniAuth: 概要

12 Feb 2017 · 171 revisions
OmniAuth: 概要原文

Facebookを例に、Rails + Devise + OmniAuthの基本的な利用方法を解説しています。OmniAuth以外の認証をオフにする場合の方法やトラブルシューティングも紹介されています。

171回もの更新が行われており、需要の高さがうかがえます。

OmniAuth認証を複数のモデルで共用する方法

3 Mar 2017 · 8 revisions
OmniAuth認証を複数のモデルで共用する方法原文

Devise + OmnuAuthはデフォルトでは1つのモデルでしか利用できません。これを複数モデルで共有できるようにする方法を紹介しています。

OmniAuth: 結合テスト

12 Nov 2016 · 21 revisions
OmniAuth: 結合テスト原文

OmniAuthをモック化して結合テストに使う方法を紹介しています。元記事はDeviseではなくOmniAuthのWikiです。

Omniauthableでのサインアウトとセッション保持

9 Dec 2013 · 5 revisions
Omniauthable, sign out action and rememberable

Devise + Omniauthableではデフォルトでsign_outルーティングが追加されません。このルーティングの追加方法や、Devise::Controllers::Rememberableでセッションを保持する方法を紹介しています。

補足

  • 原文ではsign inとlog in、sign outとlog outがそれぞれ同じ意味で使われています。訳文では、紛らわしくなる場合を除き、原則として原文に沿ったカタカナを使用します。
  • 同じ手順が複数のカテゴリに含まれていることもあります(原文に従っています)
  • 原文が更新されていることにお気づきの場合は、ぜひ@techrachoまでお知らせください。更新いたします。

関連記事(Devise)

RubyのModule Builderパターン #1 モジュールはどのように使われてきたか(翻訳)

$
0
0

こんにちは、hachi8833です。今回の翻訳記事は、Rubyならではのデザインパターンとでも言うべき「Module Builderパターン」の詳細な解説です。RubyのModuleが実はクラスであることをうまく利用していて、Railsなどのフレームワーク側で有用性が高いパターンであるように思えました。

元記事が非常に長いので次のように分割しました。

  • #1 モジュールはどのように使われてきたか(本記事
  • #2 Module Builderパターンとは何か
  • #3 Rails ActiveModelでの利用例
    • あとがき: Module Builderパターンという名前について

概要

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

RubyKaigi 2017@広島でも発表されたshioyamaさんことChris Salzbergさんの記事です。このときのセッションでもModule Builderパターンを扱っています。

本記事の図はすべて元記事からの引用です。また、パターン名は英語で表記しました。

RubyのModule Builderパターン #1 モジュールはどのように使われてきたか(翻訳)

最近私はRubyできわめてパワフルなパターンを発見しましたが、今のところあまり知られておらず、その価値もそれほど理解されていないようです1。私はこれにModule Builderパターンと名付け、Mobility gemの設計で多用しています。Mobilityは数か月前に私がリリースした、プラグイン機能を持つ翻訳/多言語フレームワークであり、自分でも非常に重宝しています。そして私は、Mobility開発中に学んだことを皆さんにも共有すべきと感じたのです。

このパターンの中核にあるModule Builderはきわめてシンプルかつエレガントであり、クラスにmixinされるカスタマイズ可能な名前付きモジュールを動的に定義できるという万能性を備えています。これを実現しているのはModuleクラスのサブクラス化であり、これらサブクラスは実行時に初期化され、そこで生成されたモジュールが他のクラスにincludeされます。

Moduleクラスサブクラス化」と聞いて頭がクラクラしてくるようでしたら、ぜひこの先もお読みください。Module Builderは一見深遠な奥義のように見えて、その実非常に有用な側面があります。Module Builderは、dry-rbなどのプロジェクトで大規模に利用されて大きな成果を上げているのですが、Module Builderのせっかくの本質的なシンプルさは高度なコーディングスタイルに覆い隠されてしまい、見えなくなってしまっています。Module BuilderパターンはRailsでもときおり使われています2が、ごく小規模にとどまっています。Rubyistの多くはこのパターンに気づいていないと言ってしまってよいと思います(日常の業務で使ったことが一度でもあるかどうかは別にしても)。

Module Builderについて解説するために、革新的なまでに複雑なコード例を多数ご紹介し、最後にMobilityからMethodFoundというgemに切り出されたコードの中から、私の書いたコードをいくつかご紹介します。これらの例は、まず多くの読者が理解できる中心となるシンプルなコンセプトから始まり、続いて少しばかり高度な話題に進み、現実のアプリがこのパターンから明らかに大きなメリットを得られることを示します。

Rubyのモジュールについて

最初にRubyの「モジュール」についておさらいしておきましょう。モジュールは基本的にメソッドや定数の集まりであり、どのクラスにもincludeでき、コードの重複を軽減し、再利用やコンポジション3を行いやすい単位で機能をカプセル化します。includedextendedといったコールバックを使えば、モジュールがクラスやモジュールにinclude(またはextendなど)されるたびに何らかのカスタムコードを実行できます。

さて、以下のMyModuleというモジュールに#fooというメソッドがあるとしましょう。

module MyModule
  def foo
    "foo"
  end
end

これで、MyModuleincludeしたクラスに#fooメソッドを追加できます。

class MyClass
  include MyModule
end

a = MyClass.new
a.foo
#=> "foo"

これはかなり正統な使い方ですが、大きな限界があります。#fooは事前に定義されているので変えられず、戻り値もハードコードされています。

次にもう少しだけ「現実的な」場合を見てみましょう。Pointというクラスに2つの属性xyがあるとしましょう。話を簡単にするためにStructで定義します。

Point = Struct.new(:x, :y)

それでは、2つの点のxの値とyの値をそれぞれ足し算できるモジュールをひとつ作成しましょう。このモジュールをAddableと呼ぶことにします。

module Addable
  def +(point)
    Point.new(point.x + x, point.y + y)
  end
end

Point.include Addable
p1 = Point.new(1, 2)
p2 = Point.new(2, 3)

p1 + p2
#=> #<Point:.. @x=3, @y=5>

今度は先ほどのモジュールよりも再利用性がやや高まりましたが、それでも頑固な制限が残っています。このモジュールは属性xyを持つPointというクラスがないと機能しないのです。

この定数Pointをモジュールから削除し、self.classに置き換えることでこの点を少し改善できます。

module Addable
  def +(other)
    self.class.new(x + other.x, y + other.y)
  end
end

これでAddableモジュールは、アクセサxyがある任意のクラス4で利用できるようになり、柔軟性がかなり高まりました。さらにRubyはきわめて動的に型を扱えるので、これらの要素に#+メソッドがありさえすれば、(潜在的には)このモジュールを使って2つのデータ型のさまざまな組み合わせを足し算できるようになります。

しかもRubyのモジュールは、superキーワードによる継承やincludeを使ってモジュールのメソッドをコンポジションにできる点にもご注目ください。したがって、この足し算メソッド呼び出しでログを出力したければ、次のようにPointクラスの内部で#+を定義すればよいのです。

class Point < Struct.new(:x, :y)
  include Addable

  def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
  end
end

期待どおり、次のようにログが出力されます。クラスのメソッドが最初に呼び出され、モジュール内に定義されたメソッドでsuperされます。

p1 = Point.new(1, 2)
p2 = Point.new(2, 3)

p1 + p2
Enter Adder...
Exit Adder...
#=> #<Point:.. @x=3, @y=5>

モジュールには他にも多くの側面がありますが、ここまではごく一部を紹介するにとどめました。しかし本記事の主眼はModule Builderのビルダーの方であり、モジュールそのものではないので、今度はビルダーを見てみることにしましょう。

「モジュールをビルドするモジュール」をビルドする

「モジュールをビルドするモジュール」をビルドする

モジュールをビルドするモジュール

モジュールが強力である理由は、機能の特定の共有パターンをカプセル化するからです(効果的に使えばの話ですが)。先の例で言うと、この機能は、変数のペア(座標の点や、xyをペアで持つ任意の他のクラス)を足すことです。このような共有パターンを見い出してモジュール(またはモジュールのセット)の形で書くことで、複雑な問題をシンプルかつモジュラリティの高いソリューションによって扱えるようになります。

しかし先のモジュールには「変数xyを備えたクラス」という制限がまだ残っているため、一般性はそれほど高くありません。この制限を外して、任意の変数セットで使えるようにできるとしたらどうでしょう。しかしどうやって?

なお一度定義されたモジュールは設定できなくなるので、制限を外すには他の方法が必要です。1つの方法は、「必要な機能を自分自身がクラスに追加する別のモジュール」をモジュール上に定義することです。このメソッドを#define_adderと呼ぶことにします5

module AdderDefiner
  def define_adder(*keys)
    define_method :+ do |other|
      self.class.new(*(keys.map { |key| send(key) + other.send(key) }))
    end
  end
end

ここでは、Addableモジュールでやったような#+メソッドの定義ではなく、メソッドを定義するメソッドを定義します。このメソッドはモジュールのインスタンスメソッドを動的にクラスに文字どおり「ブートストラップ」する(=自分で自分を持ち上げる)ので、本記事では以後、メソッドを定義するメソッドを「ブートストラップメソッド」と呼ぶことにします。

このブートストラップメソッドは任意の数の引数を取り、splat演算子*で配列keysに割り当てます。配列keysは、足し算する変数の名前(厳密には、変数の値を返すメソッドの名前)になります。次に#+というメソッドを定義します。このメソッドはキーごとに変数の値を足し、その結果を使ってself.class.newでインスタンスをひとつ作成します。

この方法でうまくいくかどうかやってみましょう。いくつかの異なるreader属性を持つ新しいクラスLineItemを作成します。#define_adderをクラスメソッドにする必要があるため、モジュールを(includeではなく)extendします。

class LineItem < Struct.new(:amount, :tax)
  extend AdderDefiner
  define_adder(:amount, :tax)
end

これで、先の例で座標の点を追加したときと同様に、行項目同士を足し算できるようになりました。

l1 = LineItem.new(9.99, 1.50)
l2 = LineItem.new(15.99, 2.40)

l1 + l2
#=> #<LineItem:... @amount=25.98, @tax=3.9>

動きました!しかも、変数名にも変数の個数にも依存していません。つまり、モジュールの柔軟性がさらに高まったのです。

しかしここでひとつ問題があります。先のログ出力コードをPointクラスからコピーし、次のように#+に貼り付けてログ出力を呼び出すことにします。

class LineItem < Struct.new(:amount, :tax)
  extend AdderDefiner
  define_adder(:amount, :tax)

  def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
  end
end

ここで再び2つの行項目を足してみると、ログは出力されず、例外が発生してしまいます。

NoMethodError: super: no superclass method `+' for #<struct LineItem amount=9.99, tax=1.5>

何が起こったのでしょうか?

#define_adder#+メソッドをどのように「ブートストラップ」しているのか、よく見てみましょう。#define_adder#define_methodでメソッドをクラス(ここではLineItem)に直接追加しています。クラスは、渡されたメソッドの定義を1つしか持てないため、クラス内で後から#+を定義すると元の定義が飛んでしまいます。メソッドが定義されるときにはincludeも継承も行われていないため、ここには「スーパークラス」はありません。

この問題を解決するには「モジュールをビルドする」というマジックが少々必要です。最初に問題の解決方法を紹介し、次にこの問題をどのように解決しているかを解説します。

module AdderIncluder
  def define_adder(*keys)
    adder = Module.new do
      define_method :+ do |other|
        self.class.new(*(keys.map { |key| send(key) + other.send(key) }))
      end
    end
    include adder
  end
end

先ほどのLineItemを使ったログ出力コードで、AdderDefinerモジュールの代わりにこのAdderIncluderモジュールを使うと、今度は次のとおり正常に動作します。

l1 = LineItem.new(9.99, 1.50)
l2 = LineItem.new(15.99, 2.40)

l1 + l2
Enter Adder...
Exit Adder...
#=> #<LineItem:... @amount=25.98, @tax=3.9>

コードが動作する秘密は、#define_adderでの#+メソッド定義で無名モジュールを使っていることです。これは先ほどのような、呼び出し側のクラスでメソッドを直接定義する手法とは異なります。

Rubyのあらゆるモジュール(これは小文字のmodule、つまりRubyの一般的なモジュールである点にご注意ください)は、あるクラスの単なるインスタンスであることを思い出しましょう。そして「あるクラス」とは、大文字のModuleクラスなのです。Moduleクラスは他のクラスと同様にnewメソッドを持つので、ここではブロック引数をひとつ取り、そのブロックは新しく作成されたモジュールのコンテキストで評価されます。

モジュールをその場で生成したければ、単にModule.newにメソッド定義を含むブロックを渡せばよいのです。このようにして生成したモジュールは、通常の(名前付き)モジュールと同じように利用できます。

mod = Module.new do
  def with_foo
    self + " with Foo"
  end
end

title = "The Ruby Module Builder Pattern"
title.extend mod
title.with_foo
#=> "The Ruby Module Builder Pattern with Foo"

これはテストでよく使われる手軽なトリックで、1度きりのテストでしか使わない使い捨てモジュールのためにグローバル名前空間を汚したくない場合に使います(Classにも使えます)6

AdderIncluderモジュール内のブロックで定義されているメソッドは、AdderDefinerで定義した#+メソッドと同じであり、その意味で2つのモジュールの動作はきわめて似通っています。両者の重要な違いは、AdderDefinerの場合はメソッドをクラス上に定義しているのに対し、AdderIncluderの場合はメソッドを含むモジュールクラスにincludeしている点です。

次のようにancestorsでチェインを表示すれば、このモジュールがLineItemクラス自身の直下にあることを確認できます。

LineItem.ancestors
#=> [LineItem,
#<Module:0x...>,
...

このancestorsチェインを見れば、ここでsuperが正常に動作する理由がはっきりわかります。LineItem#+を呼ぶとこのメソッドからsuperが呼ばれてクラス階層を遡り、ここで使いたい#+が定義されている無名モジュールにたどりつきます。続いて#+で足し算が実行されて結果を返します。この結果は、LineItemの足し算でログ出力コードをコンポジションにしたものです。

この動作は一見奇妙かつ見慣れないものですが、ブートストラップメソッドで無名モジュールを動的にincludeするテクニックは実際非常に便利であり、こうした理由によって広く使われています。特にRailsのように、モデルを定義したりルーティングを設定するだけでdefine_adder的なブートストラップメソッド呼び出しが多数トリガされる柔軟なフレームワークで多用されています。このテクニックは、これと同じレベルの柔軟性を必要とするその他のgem(私のMobilityなど)でも使われています。

このように、モジュールビルドのブートストラップはRubyにおいてきわめて重要なメタプログラミング技法であると考えられます。実際、このテクニックはRailsのようなフレームワークのきわめて動的な特性を支えています。

しかしここからが本題です。私としてはこのブートストラップ技法よりも、これからご紹介するModule Builderの方がはるかに高い能力を示すことを皆さまに理解いただきたいと考えており、その意味でModule Builderパターンはブートストラップよりもいっそう注目に値します。説明のため、先のAdderモジュールに立ち返り、先ほどとは少し違う方法でこのモジュールを定義することにします。

(「#2 Module Builderパターンとは何か」に続く)

図版(#1で使用されているもの)

a. “Drawing Hands” by M.C. Escher. reference
b. “Automated space exploration and industrialization using self-replicating systems.” From Advanced Automation for Space Missions: 5. REPLICATING SYSTEMS CONCEPTS: SELF-REPLICATING LUNAR FACTORY AND DEMONSTRATION. reference

関連記事

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

[保存版]人間が読んで理解できるデザインパターン解説#1: 作成系(翻訳)

[保存版]人間が読んで理解できるデザインパターン解説#2: 構造系(翻訳)

Rubyで学ぶデザインパターンを社内勉強会で絶賛?連載中


  1. 私がこの「Moduleクラスのサブクラス化」というアイデアを最初に知ったのはPiotr Solnicaによる2012年の記事でした。最近だとEric AndersonAndreas Robeckeの記事でも言及されていますが、それ以外にはほとんど見かけません。 
  2. 記事執筆時点では、ここここで使われています。 
  3. ここでは、モジュールを別の場所でincludeし、メソッドチェインの複雑なコンポジションをsuperで形成するという意味です。このテクニックはMobilityで多用しました。 
  4. 正確に言うと、2つの属性の値を引数として取るイニシャライザも必要です。 
  5. ここでは、includeでモジュールのメソッドをクラスメソッドとしてクラスに追加するというトリックを使っています。このクラスはモジュールがincludeされるときにextendされます。これはよくあるトリックですが、これがちょっと謎に見えるのであれば、先に進む前にこの謎を読み解いてみてください。 
  6. 無名モジュールのこのような「名前空間を汚さない」性質はメタプログラミングでも有用です(定数名の衝突を気にしないで済むため)。 

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

$
0
0

こんにちは、hachi8833です。

Rails: 今週の改修

新機能: HTTP/2プッシュ用のEarly Hintsを実装


#30744より

Aaron Patterson氏のロングインタビュー↓でも言及されていた「アーリーレスポンス」がHTTP 103 Early Hintsとして実装されました。

[インタビュー] Aaron Patterson(後編): Rack 2、HTTP/2、セキュリティ、WebAssembly、後進へのアドバイス(翻訳)

とにかく、Kazuhoは新しい標準化を提案することを思いつきました。それは新しいレスポンスコードで、私はそれが101か110だと思っています。これはアーリーレスポンスと呼ばれるもので、HTTP/1の普通のレスポンスコードなのですが、本質的にプッシュ(HTTP/2プッシュ)と同じものです。使っているプロキシがこのレスポンスコードを理解し、それをHTTP/2プッシュに変換するというわけです。
この方法の良い点は、HTTP/1で動くサーバーがあり、そのサーバーの通信相手であるプロキシがHTTP/2で話せるなら、プッシュを行いたいということをプロキシが理解できるという点です。本質的にHTTP/1とHTTP/2のいいとこ取りができるので、HTTP/1サーバーにまったく影響を与えずに引き続きプッシュを行えます。


つっつきボイス: 「上のロングインタビュー時点では@kazuhoさんから提案中だったのが、Aaronさんの期待どおり見事採用されてる」「HTTP/2 Early HintsはRailsに限らない話で、プロトコルが拡張されたことの方が大きい」「ほほー、レスポンスコードは?」「103番ですね」
「Early Hintsについてはこちらのブログ↓見ればひととおりわかります。Web開発者必読

「しかし103番とは随分と若い番号」「HTTP 1xx番台はInformationalなのか: ところでこのWikipediaにはまだ103は書き足されてないから更新必要ですね」
「変更は意外にシンプルだった↓」「環境変数で指定できる: デフォルトはnilだそうです」

# 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

存在しないAction Cableサブスクリプションの解除メッセージをわかりやすくした

actioncable/lib/action_cable/connection/subscriptions.rb
      def remove(data)
         logger.info "Unsubscribing from channel: #{data['identifier']}"
-        remove_subscription subscriptions[data["identifier"]]
+        remove_subscription find(data)
        end

つっつきボイス: 「これは普通にリファクタリングですね」

テストでRuby 2.5の未使用変数warningが出たのを修正

# railties/test/generators/argv_scrubber_test.rb
-        message = nil
         scrubber = Class.new(ARGVScrubber) {
-          define_method(:puts) { |msg| message = msg }
+          define_method(:puts) { |msg| }
         }.new ["new", "--rc=#{file.path}"]
         args = scrubber.prepare!
         assert_equal ["--hello", "--world"], args

文言修正系PR

# activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
-          raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer")
+          raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
-    #  NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt>
-    #  objects that have already been created, e.g. any model timestamp
-    #  attributes that have been read before the block will remain in
-    #  the application's default timezone.
+    # NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt>
+    # objects that have already been created, e.g. any model timestamp
+    # attributes that have been read before the block will remain in
+    # the application's default timezone.

つっつきボイス: 「修正と関係ありませんが、英語圏の人ってPostgresって書くの好きですね: 私は翻訳のときは常にPostgreSQLと書き直してます」「日本語のポスグレは口頭でしか使わない感じですね」

compatibility.rbのリファクタリング(DRY化)

コミットメッセージどおり、重複したコードをprivateにまとめています。

# activerecord/lib/active_record/migration/compatibility.rb
          if block_given?
-            super(table_name, options) do |t|
-              class << t
-                prepend TableDefinition
-              end
-              yield t
+            super do |t|
+              yield compatible_table_definition(t)
             end
            else
...
        private
+          def compatible_table_definition(t)
+            class << t
+              prepend TableDefinition
+            end
+            t
+          end

Rails

Rails 5とGriddlerとMailgunでメールを受信してパースする(RubyFlowより)


hackernoon.comより


つっつきボイス: 「ほほー、自前のメールサーバーを使わずにRailsでメール受信トリガの処理を実行する方法か: 普通メール受信をトリガにして何かするときは、postfixとか使って.forwardあたりでスクリプトに受信メールを流し込んで実行したりするけど」

  • Griddler — Railsでメール受信するgem
  • Mailgun — 開発者向けのAPIベースのメールサービス

「なおAWSを使っていいのであれば、SES(Simple Email Service)S3に受信できるので、S3のfile createのイベントをLambdaに流して同じことができるはず」


そこからNAT越え(NAT traversal)の話題になりました。

ngrokというサービスとプログラムを使うと、双方ともNATの裏にいてもtunnelingできるらしい」「ご家庭でゲームのためにUPnPのNAT越え頑張ったりしますね」「UDPホールパンチングなんてのも」「ゲームやらない人なのでこのあたり知らないことだらけ」

shioyamaさんのMobility gem記事

以前のウォッチでもご紹介したRails多言語化gemです。Module Builderパターンの実例についてここでも熱心に解説されています。
この方の英文は(内容の難しさと量は別にしても)とても読みやすいと思います。

RubyのModule Builderパターン #1 モジュールはどのように使われてきたか(翻訳)

Skinny: Scalaで書かれたRails的フレームワーク


github.com/skinny-framework/skinny-frameworkより

ちょっと珍しいので拾ってみました。★も600越えと意外に伸びています。


つっつきボイス: 「これ見てて思ったけど、Railsの方法論はいろんなフレームワークに広がりましたね」
「Railsが流行ってから他のWebアプリケーションフレームワークも軒並みCLIのscaffold的なgeneratorとかを搭載するようになったし、つくづくRailsの影響は大きかったと思う」「以前PHPフレームワークのSymfony使ってたけど、v2はもうホントRailsっぽいCLI」


symfony.comより

Ruby

Ruby 2.5.0-preview1がリリース(Ruby公式ニュースより)

新機能

  • バックトレースを逆順で見やすく表示
  • トップレベルの定数探索を廃止
  • do/endブロック内でrescue/else/ensureを使えるようになった
  • yield_self

その他2.4以降の変更

  • Onigmoの非包含演算子(absence operator)
  • Bundlerが標準ライブラリの仲間入り
  • rubygemsが2.6.13に
  • rdocが6.0.0.beta2に、従来のIRB lexerに代えてRipperを導入し高速化
  • Unicode 10.0.0をサポート


つっつきボイス: 「Ruby 2.5はbundlerがbundleされた点がとてもうれしいので、ぜひ早く安定版が出て欲しい」

RubyGems 2.6.14でセキュリティバグ修正

Aaron Pattersonさんが書いています。RubyGems 2.6.14にアップデートするには以下を実行します。

gem update --system

良いRubyコードを書くためのあまり知られていない7つの技(RubyFlowより)

良記事です。つっつきも大いに盛り上がりました。このRubyGuidesというサイトの他の記事もよさそうです。


つっつきボイス:

  • Integer#digits「桁をバラして配列で取り出せるのかー: 使いみち思いつかないけど」「数学パズルとかに使えるかな?」
  • #tap「このメソッドは、使う人はすごく使うけど使わない人は全然使わないですね: こんなふうに1回だけ#tapするのは気持ちわかる」
User.new.tap { |user| user.name = "John" }

「でもこのサンプルコードみたいにチェインするのは好きじゃない」「う、読みづらい…」

"hello".bytes.to_a.tap {|arr| p arr }
  .collect {|byte| byte.to_s(16) }.tap {|arr| p arr }
#=> [104, 101, 108, 108, 111]
#=> ["68", "65", "6c", "6c", "6f"]
  • #values_at「Arrayの何番目と何番目と指定していっぺんに取り出せるんですね」「これ、Hash#values_atの方が便利そう↓」「paramsのこれとこれだけ取り出したりできるのか」
h = {1=>"a", 2=>"b", 3=>"c"}
p h.values_at(1,3,4)               #=> ["a", "c", nil]
# [h[1], h[3] ,h[4]] と同じ
h = { a: 1, b: 2, c: 3 }
h.transform_values {|v| v * v + 1 }  #=> { a: 2, b: 5, c: 10 }
h.transform_values(&:to_s)           #=> { a: "1", b: "2", c: "3" }
h.transform_values.with_index {|v, i| "#{v}.#{i}" }
                                     #=> { a: "1.0", b: "2.1", c: "3.2" }
  • Kernel#itself「自分自身にアクセスしたいことってたまにある」「これは考えて使わないとよく分からなくなりそう」
string = 'my string' # => "my string"
string.itself.object_id == string.object_id # => true
  • Enumerable#cycle「テストデータを無限に生成するのとかに使えるかも」
a = ["a", "b", "c"]
a.cycle {|x| puts x }  # print, a, b, c, a, b, c,.. forever.
a.cycle(2) {|x| puts x }  # print, a, b, c, a, b, c.

「あー良い記事でござった」「little-knownダナ確かに」

RubyのGILを理解する


dev.to/enetherより

GILといえば卜部さんのブログにこんなことが書いてありました。

GVL (じーぶいえる) GILとも。Global VM Lock。嫌われもの。だがパフォーマンスを犠牲にしつつもみんなを[BUG]から守ってくれてるんだぞ。もっと感謝すべきだ。
‘10年代のRubyコア用語集より


つっつきボイス: 「よく話題になるやつです: ↑ホントこのとおり」「GってGiantのGだったりもしますね」「まあ意味は変わりませんが」「せっかくスレッドがCPUに分割されてもGILが発生すると一斉に一時停止しちゃう」

RubyのSymbolってなくせないの?


つっつきボイス: 「RubyのSymbolって私好きなので、問題だと思ってませんでした」「frozen_string_literalがある今となっては、もしかするとSymbolっていらないのかも」

「Symbolを持つ言語はLispやSmalltalkなど他にもあるけど、おそらくRubyのSymbolって当初の概念からかなり踏み出して独自路線になっちゃったのかも: Symbol#to_sできちゃうとか」「確かに: Symbolをputsするにはto_sが必要だけど、それが当初の想定を超えて文字列代わりにどんどん使われたり」「RailsのHashWithIndifferentAccessなんてのができたり」
「JavaにはSymbolないので、キーの文字列がたまに一意にならなくて数値に変えたりとつらかったなー」

Ruby Rogueポッドキャスト: Martin FowlerとRubyコードをリファクタリングしよう


devchat.tv/ruby-roguesより

Ruby Rogueはこの間のウォッチでも軽くご紹介しました。
Martin Fowler氏はRefactoring — Ruby Editionの著者です。この本の日本語版があったのに今は絶版なのが何とも残念。


つっつきボイス: 「お、目次もあるし、内容もいい感じ」「このポッドキャストは月一ぐらい?」「もっと頻繁みたいです」「それは凄い: 長く続いてるのは大事」
「そういえば日本語だとrebuild.fmという音声ポッドキャストが随分長くやってますね」「rebuild.fmは毎回いろんなベテランエンジニアの話が聴けるのがよくて、BPS社内にも愛聴者がいたと思う」


rebuild.fmより

「Ruby Rogueも同じ感じでやってるようです: DHHインタビューとか、かと思うと元ストリートパフォーマーからRails開発者に転身した人の話とか、盛りだくさんですね」「字幕があれば英語ヒアリング練習にもいいかも」

bolt: SSHやWinRM経由で多数のコマンドを実行するgem(GitHub Trendingより)

$ bolt command run 'ssh -V' --nodes neptune,mars
neptune:

OpenSSH_5.3p1, OpenSSL 1.0.1e-fips 11 Feb 2013

mars:

OpenSSH_6.6.1p1, OpenSSL 1.0.1e-fips 11 Feb 2013

Ran on 2 nodes in 0.27 seconds

つっつきボイス: 「capあれば同じことできますね」「capって何でしたっけ」「capistranoのコマンド」「あ、そうだった」
「この種のツールはやっぱりsingle binaryで動いてくれる方がうれしいので、自分は使わないかな: Go言語とかで作られてる方が便利だなあ」「perlのスクリプトとか、環境が変わると微妙なバージョン違いで動かなかったりしますね」

  • capistrano — Rubyで書かれたデプロイ/サーバー操作ツール


capistranorb.comより

Rubyはどうやって開発を進めているか


つっつきボイス:伽藍とバザールですね: そうとう昔のですが、今日のOSSの歴史を辿るとまず間違いなく参照される文書: これ以外で伽藍という用語を見たことないけど」
「90年代にオープンソースの隆盛を予言したやつですね: 当時騒いだのは開発者より文化人の方が多かったような覚えが」
「伽藍が読めなかった」「がらん、って大聖堂とかの方がわかりやすいかも」「cathedralだからカテドラルか」


そういえば伽藍という言葉を覚えたのは西遊記でした。六丁六甲、五方掲諦、四値功曹、護駕伽藍。

Ruby trunkより

提案: ファイルが存在する場合にxモードで開いたらraiseすべき->#11258と重複

# C言語: file.txtを作成するが、既に存在していたらraiseする
open('file.txt', 'wx') {|f| f.puts("Some text") }

つっつきボイス: 「openのxモードとか知らなかったー」「wxって最初意味分からなかった」「PHPだけどこんなの見つけた↓」

‘x’ 書き込みのみでオープンします。ファイルポインタをファイルの先頭に置きます。 ファイルが既に存在する場合には fopen() は失敗し、 E_WARNING レベルのエラーを発行します。 ファイルが存在しない場合には新規作成を試みます。 これは open(2) システムコールにおける O_EXCL|O_CREAT フラグの指定と等価です。 このオプションはPHP4.3.2以降でサポートされ、また、 ローカルファイルに対してのみ有効です。
http://php.net/manual/ja/function.fopen.phpより

「manではこう↓」

open(2) システムコールにおける O_EXCL|O_CREAT

「あった、これこれ↓」

O_EXCL
この呼び出しでファイルが作成されることを保証する。このフラグが O_CREAT と 一緒に指定され、 pathname のファイルが既に存在した場合、 open()は失敗する。
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/open.2.htmlより

「他の言語と同じようにPOSIXのopen()に準拠しようよ、ってことか」


そこからPOSIXのシステムコールの名称の話題になりました。

O_EXCLとかO_CREATとかO_TRUNCみたいな、本体が5文字の短いシステムコールは古くからあるやつで、O_NOFOLLOWみたいに長いのはたぶん後から追加されたやつです」「へええ」「昔はこういうところをケチらざるを得なかったんですね」

JavaScript

asyncawaitを4つの例で学ぶ(JavaScript Liveより)

// 同記事より
async function startSynchronous() {
  try {
    const pentaCodeAvatarUrl = await getPentaCodeAvatar();
    const reactAvatarUrl = await getReactAvatar();
    const totalURL = pentaCodeAvatarUrl + reactAvatarUrl;
    console.log(totalURL);
  } catch (e) {
    console.error('Error in startSynchronous (Async Await Based)', e);
  }
}

startSynchronous();

つっつきボイス: 「出たなPromise」「asyncawaitをガンガン使うようなJSコード、納期が超緩い案件ならやってみたい」

amatudaさんのRubyConf Malaysia 2017スライド

CSS/HTML/フロントエンド

Bootstrap 4.0が「ついに」Beta版に


getbootstrap.comより


つっつきボイス: 「公式サイトのトップ画面がついに変わった!」「Bootstrap 4.0で長く待たされた人たちはみんな仏の境地になってますね」「ちょうど今年の学生に『Bootstrap 4.0は当分先です』と教えたところだった」

リモートつっつき: 「Formの書き方が変わり、-xsが廃止になってアイコンフォントも標準付属でなくなったので、3からそのまま入れ替えるとめちゃくちゃに壊れますが、ブレークポイント4段なのがとても良い☺️」
「アイコンフォントの分離は、require(‘boostrap’);時にcssの@importを辿ってWebpackがフォントを読み込もうとして😞 になるのを回避できるので逆に良い」

5分でタイポグラフィを改善するコツ(HackerNewsより)


pierrickcalvez.comより

デザイン寄りの記事です。


つっつきボイス: 「こういうの、日本語の縦書きやルビになると全然違ってきますよねー」「確かに」

Go言語

caddy: Let’s Encryptの証明書を自動で使えるHTTP/2サーバー(GitHub Trendingより)

1か月足らずで驚異の★14,000超えです。


つっつきボイス: 「single binaryで動くので良さそう」「OpenSSLとかもbundleされてるなら嬉しいな」

supervisord: Goで書かれたsupervisor(GitHub Trendingより)

$ supervisord -c supervisor.conf -d

$ supervisord ctl status
$ supervisord ctl stop <worker_name>
$ supervisord ctl start <worker_name>
$ supervisord ctl shutdown

つっつきボイス: 「本家のsupervisord↓と名前が同じでさえなければなー: このままじゃ議論になったときも区別が大変」「コマンドとかconfigは互換保とうとしてるっぽいけど、完全に同じでないと実際に運用しようとするときに問題になりそう」


本家supervisord.orgより

rat: シェルコマンドを組み合わせてターミナルアプリを作るツール(GitHub Trendingより)


github.com/ericfreese/ratより

tig的なCLIツールを作れます。

wow: Go CLI用のかわいいスピナー(GitHub Trendingより)


その他

fd: 改良版findコマンド(GitHub Trendingより)


github.com/sharkdp/fdより

find -iname '*PATTERN*'
fd PATTERN   // 書式がシンプル

Rustで書かれています。カラー表示がうれしいです。

sorting.at: ソートのビジュアル表示


sorting.atより

最初Hacker NewsのSorting Visualizationsにしようかと思いましたがこちらの方がずっと美しくて明快だったので。


つっつきボイス: 「ソートのアルゴリズムを学んでない人にはよさそう」
「ところでソートといえばこの名作動画を思い出しますね↓」「こ、これはw」「なごむー」

cosmos: さまざまな言語で書かれたアルゴリズムとデータ構造コレクション

# https://github.com/OpenGenus/cosmos/blob/master/code/search/binary_search/binary_search.rb より
=begin
    Part of Cosmos by OpenGenus Foundation
=end

def BinarySearch(arr, l, r, x)

    if x.nil?
        return -1
    end

    if r >= l
        mid = (l + r)/2
        if arr[mid] == x
            mid
        elsif arr[mid] > x
            BinarySearch(arr, l, mid-1, x)
        else
            BinarySearch(arr, mid+1, r, x)
        end
    end
end

arr = [3,5,12,56,92,123,156,190,201,222]

number = 12

puts "Position of #{number} is #{BinarySearch(arr, 0, arr.length-1, number)}"

Rubyが少ないのが残念。主催はOpenGenusという団体です。

番外

RoBozzle: 子供も大人も楽しめるパズルゲー


robozzle.comより

Hacker Newsのコメント欄で見つけました。まだ遊んでませんが、ちょっと倉庫番を思い出しました。


つっつきボイス: 「これで思い出したけど、codecombatっていう、ゲームでプログラミングを学ぶサイトも面白いですよ↓: お、今見てみたら前よりずっと進化してる」「ローカライズもかなり頑張ってる感じですね」


codecombat.comより


今週は以上です。

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

週刊Railsウォッチ(20171006)PostgreSQL 10ついにリリース、Capybaraコードを実画面から生成するnezumiほか

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

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

Ruby 公式ニュース

Rails公式ニュース

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Frontend Weekly

frontendweekly_banner_captured

Hacker News

160928_1654_q6srdR

Github Trending

160928_1701_Q9dJIU

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

$
0
0

概要

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

パターンの種別は原則として英語表記にしました。

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

Service Object(単にServiceとも呼ばれます)は、肥大化したActiveRecordモデルを分割(訳注: TechRacho翻訳記事にリンクしました↓)し、コントローラをスリムかつ読みやすくするうえで非常に有用な、Ruby on Rails開発における一種の聖杯とも呼ぶべきパターンです。

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

どんなときにService Objectを使うか

このパターンは非常にシンプルかつ強力なので、つい使いすぎてしまうほどです。特に、多数のモデルに対するコールバックやアクセスを含み、多くの手順で構成される複雑なアクションを定義する場所が必要で、かつ他に適切な置き場所がない場合に有用です。Service Objectは、モデルで外部クラスとやりとりするコールバックによって生じる問題(関連記事)を軽減するのにも用いられます。

Service Objectパターンを最大限に利用するための注意点

1. 命名規則を1つに定める

プログラミングで難易度が高いのは、適切かつ意味のわかる名前を割り当てることです。Service Objectの場合、UserCreatorTwitterAuthenticatorCodeObfuscatorなどのように「〜or」で終わる名前を付ける方法が広く採用されています。この命名規則に従おうとすると、OrderCompleterのように英語的に少し無理が生じることがあります。

そして私は、CreateUserAuthenticateUsingTwitterObfuscateCodeCompleteOrderのように、コマンドやアクションを先に書く命名方法ならややわかりやすくなることに気づきました。この方がService Objectの責務が明確になります。

どの方法を選ぶにせよ、一度命名規則を決めたらそこから外れないようにすることが重要です。

2. Service Objectを直接インスタンス化しないこと

Service Objectをインスタンス化しても、単にcallメソッドを実行する以外にあまり使いみちがありません。callメソッドを実行するのであれば、次のように抽象化してService Objectへの呼び出しをより簡潔にすることを検討しましょう。

module Service
  extend ActiveSupport::Concern
  class_methods do
    def call(*args)
      new(*args).call
    end
  end
end

このモジュールをincludeすることで、UserCreator.new(params).callUserCreator.new.call(params)UserCreator.call(params)のように簡潔に記述でき、読みやすさも向上します。このService Objectはインスタンス化も可能なので、内部のステートを取り出す必要が生じた場合にも有用です。

3. Service Objectの呼び出し方法を1つに定める

個人的にはcallメソッドを使うのが好みですが、他の方法を使わない特別な理由があるわけではなく、performrunexecuteも候補として優れています。重要なのは呼び出し方法を常に統一することです。というのも、クラスの責務は既にクラス名に明示されており、これ以上明確にする必要はないからです。

呼び出し方法を統一しておけば、新しいService Objectを実装するたびに名前を考える面倒がなくなりますし、他のプログラマーは実装の詳細をチェックしなくてもService Objectの使い方をすぐに理解できるという効用もあります。

4. Service Objectの責務を1つに絞り込む

これはService Objectの呼び出し方法を1つに統一しておけばある程度実現できますが、それだけではService Objectに複数の責務が混入する可能性が残ります。Service Objectにさまざまなアクションをまとめることもできますが、アクションのセットは1種類に限定するべきです。

Service Objectでありがちなアンチパターンとしては、たとえばManageUserというserviceがユーザーの作成と削除の両方を引き受けてしまうというものがあります。そもそもmanageという言葉だけでは責務があいまいになってしまい、オブジェクトがどんなアクションを実行すべきかを制御する方法も明確になりません。代わりにDeleteUserCreateUserという2つのserviceに分けて導入すれば、コードも読みやすくなり、より自然になります。

5. Service Objectのコンストラクタを複雑にしない

一般に、実装するほとんどのクラスではコンストラクタをシンプルにしておくのがよい考えです。Serviceの主な呼び出し方法はクラスメソッドを呼び出すことですが、コンストラクタの責務を「引数をserviceのインスタンス変数に保存すること」に限定する方がずっとメリットが多くなります。

class DeleteUser
  def initialize(user_id:)
    @user = User.find(user_id)
  end

  def call
    #…
  end
end

上と下を見比べてみましょう。

class DeleteUser
  def initialize(user_id:)
    @user_id = user_id
  end
  def call
    #…
  end
  private
  attr_reader :user_id
  def user
    @user ||= User.find(user_id)
  end
end

下のようにすれば、テストのときにコンストラクタではなくcallメソッドに集中できるようになりますし、コンストラクタに何を置くことができ、何を置けないかという線引きも明確になります。開発者が日々決定しなければならないことはたくさんあるので、こういう点を標準化して決めごとをひとつ減らしましょう。

6. callメソッドの引数をシンプルにする

Service Objectに2つ以上の引数が与えられる場合、引数をわかりやすくするためにキーワード引数の導入を検討するとよいでしょう。Service Objectの引数が1つの場合であっても、キーワード引数にしておくことで読みやすさが向上するでしょう。

UpdateUser.call(params[:user], false)

上と下を見比べてみましょう。

UpdateUser.call(attributes: params[:user], send_notification: false)

7. 結果はステートリーダー経由で返す

Service Objectから何らかの情報を取り出さなければならないような状況はめったにありませんが、その必要が生じた場合に取りうるアプローチはいくつか考えられます。

Service Objectはcallメソッドの結果をたとえば次のように返すことができます。実行が成功した場合はtrue、失敗した場合はfalseを返す、という具合です。

しかし、callメソッドがService Object自身を返すようにすればより柔軟になります。この方法にすると、Service Objectインスタンスのステートを読み出せるようになります。

update_user = UpdateUser.call(attributes: params[:user])
unless update_user.success?
  puts update_user.errors.inspect
end

この方法は、例外のraiseなど、めったに起きない事象を扱うような場合にも効果的にエッジケースとやりとりできます。

begin
  UpdateUser.call(attributes: params[:user])
rescue UpdateUser::UserDoesNotExistException
  puts "そのユーザーは存在しません"
end

8. callメソッドの可読性を下げないようにする

callメソッドはService Objectの中心となるメソッドです。Service Objectでは、callメソッドをできるだけ読みやすく保つことをおすすめします。callメソッドには関連する手順だけを記述し、それ以外のロジックは最小限に抑えるようにします。andorなどを使って、特定の手順のフローを制御してもよいでしょう。

class DeleteUser
  #…
  def call
    delete_user_comments
    delete_user and
      send_user_deletion_notification
  end
private
  #…
end

9. callメソッドをトランザクションでラップすることを検討する

Service Objectが自らの責務を果たすために複数の手順が必要になった場合、手順をトランザクションでラップするとよいでしょう。こうしておけば、その手順に失敗した場合にも常にロールバックできます。

10. Service Objectが増えたら名前空間でグループ化する

Service Objectは遅かれ早かれ何十個にも増えるでしょう。コードをうまく編成するために、名前空間で共通のService Objectをグループ化することをおすすめします。Service Objectをグループ化する名前空間は、「外部サービス」や「高レベルの機能」など考えられるどんな基準で決めてもかまいません。ただし、Service Objectの命名規則や配置を読みやすい素直なものにするのが名前空間の主要な目的であることをお忘れなく。規則を1つにしていれば、適切な配置は自然に定まります。不要な選択肢を増やさないようにするのがコツです。

まとめ

Service Objectはテストを書きやすくするのに役立つシンプルかつ強力なパターンであり、広く応用できます。Service Objectは実装が容易ですが、その分実装がばらつかないよう制御が必要です。Service Objectの適切な命名規則を定めることで呼び出しや結果の受け取りを統一し、Service Objectクラスの内部状態をシンプルかつ読みやすく保てば、コードベースでこのパターンから多くのメリットを得ることができます。

Service Objectパターンで簡単な抽象化が必要になったら、BusinessProcess gemや類似のユースケースを検討しましょう。rails-patterns gemではさらに薄いレイヤを提供しています。

次回のpart 2は「Query Object」です。

関連記事

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

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

リファクタリングRuby: サブクラスをレジストリに置き換える(翻訳)

Rails: クラスレベルの3つのアクセサを比較する(翻訳)

$
0
0

概要

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

Rails: クラスレベルの3つのアクセサを比較する(翻訳)

attr_accessorは、インスタンス変数のgetterとsetterを提供する非常に有用なマクロです。これと同じ効果をクラス変数でも得られれば、クラス変数でクラスを設定できて便利です。そのための方法として少なくともattr_accessorcattr_accessorclass_attributeの3つのアプローチがあります。継承を使わない場合はどれも同じに使えますが、継承がからむ場合の動作はかなり異なります。

1. attr_accessor: 値を継承したくない場合に使う

この場合は通常のattr_accessorを使いますが、ここではクラスレベルで使います。attr_accessorを使うと、各子クラスに変数が定義されますが、親クラスに設定された値は継承されません。各子クラスの属性の初期値はnilに設定されます。

class Parent
 class << self
   attr_accessor :foo
 end
end
class Child < Parent
end
Parent.foo = 100
Child.foo #=> nil  // 値は継承されない
Child.foo = 200
Child.foo #=> 200
Parent.foo #=> 100 // 子クラスで値を変更しても親クラスの値には影響しない

訳注: attr_accessorはRubyのメソッドです。

2. cattr_accessor: 値をすべてのクラスで共有したい場合に使う

この場合はcattr_accessorで親クラスの値を子クラスに継承します。このとき、cattr_accessor宣言をclass << selfブロックの内側に置く必要はありません。ここで重要なのは、子クラスでクラス変数の値を再定義すると親クラスと子クラスの両方で値が変更されるという点です。

class Parent
  cattr_accessor :foo
end
class Child < Parent
end
class Grandchild < Child
end
Parent.foo = 100
Child.foo #=> 100
Child.foo = 200
Child.foo #=> 200
Parent.foo #=> 200     // 値は継承ツリーの上位に反映される
Grandchild.foo #=> 200 // 値は継承ツリーの下位に反映される

訳注: cattr_accessormattr_accessorのエイリアスです。

3. class_attribute: 継承した値を親クラスに影響を与えずに上書き可能にしたい場合に使う

私はこのシナリオが最も有用だと思います。class_attributeを使うと親クラスの変数値が継承され、継承した値は親クラスに影響することなく子クラスで安全に再定義できます。

class Parent
  class_attribute :foo
end
class Child < Parent
end
class Grandchild < Child
end
Parent.foo = 100
Child.foo #=> 100      // 値は継承される
Child.foo = 200
Child.foo #=> 200
Parent.foo #=> 100     // 値を再定義しても親クラスには影響しない
Grandchild.foo #=> 200 // 再定義した値はその子クラスに継承される

関連記事

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

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)

Rails: render_async gemでレンダリングを高速化(翻訳)


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

$
0
0

元記事が非常に長いので次のように分割しました。

概要

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

RubyKaigi 2017@広島でも発表されたshioyamaさんです。このときのセッションでもModule Builderパターンを扱っています。

本記事の図はすべて元記事からの引用です。また、パターン名は英語で表記しました。

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

Module Builder

ここまでに扱った3種類のadder系モジュールを振り返ってみましょう。

  1. Addable: 2つの変数xy#+メソッドを追加する。
  2. AdderDefiner: クラスメソッドdefine_adder(任意の変数のセットに#+メソッドを定義する)を追加する。

  3. AdderIncluder: これもクラスメソッドdefine_adder#+メソッドを定義する)を追加するが、このメソッドはsuperでコンポジションにできる。

1.のAddableモジュールはさほど柔軟ではありませんが、今思えば、これはこれでいくつかよい点があります。

  • #+だけをincludeするので、define_adderのような人工的なブートストラップが残らない(AdderDefinerAdderIncluderはそうではない)
  • superで親クラスからメソッドにアクセスできる(AdderIncluderでもできるがAdderDefinerではできない)
  • モジュールに名前があるのでancestorsチェインで簡単に確認できる。AdderDefinerAdderIncluderの場合、それ自身ancestorsチェインに表示されるが、ブートストラップメソッドが呼び出されたかどうか、どのように呼び出されたかの確認が困難(または確認不能)。

このセクションでは、こうしたadder系モジュールの利点を保つ方向で問題を解決する方法をご紹介します。前のセクションで追求した柔軟性もこの方法で得られます。

Module Builder

最初に、Moduleクラスを継承するAdderBuilderクラスを定義します。

class AdderBuilder < Module
  def initialize(*keys)
    define_method :+ do |other|
      self.class.new(*(keys.map { |key| send(key) + other.send(key) }))
    end
  end
end

小さなクラスですが、とても密度の高い内容です。このクラスで行われていることを詳しく説明する前に、動作を確認してみましょう。ここでは、このクラスをインスタンス化してモジュールにしたものをLineItemに(extendではなく)includeします。

class LineItem < Struct.new(:amount, :tax)
  include AdderBuilder.new(:amount, :tax)
end

l1 = LineItem.new(9.99, 1.50)
l2 = LineItem.new(15.99, 2.40)
l1 + l2
#=> #<LineItem:... @amount=25.98, @tax=3.9>

すると、このちっぽけなクラスはAdderDefiner内のブートストラップメソッド#define_adderと同じように動作します。唯一違う点は、この動作のためのクラスメソッドをまったく定義していないことです。

いったいどうやっているのでしょうか?まず、ここではModuleクラスをサブクラス化しています。Moduleクラスはnewを呼べるので、サブクラス化してinitializeなどのメソッドを独自に定義できるだろうと推測できます。そして先に示したとおり、本当にできるのです。

クラスの初期化部分を再録します。

def initialize(*keys)
  define_method :+ do |other|
    self.class.new(*(keys.map { |key| send(key) + other.send(key) }))
  end
end

このコードには2つのレベル(訳注: ネストのこと)があり、両方のレベルを認識することが重要です。1つのレベルでは、Moduleクラスの新しいインスタンスを初期化しています。このインスタンスはそれ自身がいわゆる(小文字の)モジュールであり、他のクラスにmixinできるメソッドコレクションです。次のレベルではこのモジュールを初期化し、メソッドのひとつとして#+を定義しています。

この#+メソッド自身は、Module.newに渡される引数(keysは上述の:amount:taxです)に含まれるメソッド名に沿って定義されます1。このメソッドの中でself.class.newを呼ぶと、この新しいモジュールをincludeするクラスのコンテキストでこのnewが評価され、そこでLineItem(またはPointのようにこのモジュールをincludeしたクラスなら何でもよい)を評価します。

すなわちAdderBuilder.new(:amount, :tax)は、以下と機能的に同等な方法でモジュールを評価します。

Module.new do
  def +(other)
    self.class.new(amount + other.amount, tax + other.tax)
  end
end

上はAddableで使ったコードですが、xyはそれぞれamounttaxに差し替えられます。しかしこのパワーは、モジュールの動的な定義で任意の変数名を指定できるという事実から生み出されています。

そして、このパターンの真の創意はまさしくここに潜んでいるのです。すなわち、モジュールを「メソッドや定数が固定されたコレクション」としてではなく、「欲しいコレクションをその場でビルドできる設定可能なプロトタイプ」として定義できるということです。これならクラスメソッドも不要ですし、モジュールのインクルード手順をブートストラップするメタプログラミング的トリックも不要です。このビルダはモジュールを直接ビルドするので、1ステップでモジュールをインクルードできます。

それだけではありません。先のセクションで解説したテクニックで生成される無名モジュールと異なり、この方法で作成されるモジュールには名前があるので、次のようにancestorsチェインではっきりと確認できます。

LineItem.ancestors
[LineItem, #<AdderBuilder:0x...>, Object, ... ]

これはまさしく、Moduleクラスのサブクラス化で達成したカプセル化の素敵な副産物です2。無名モジュールでは、以下のようにデバッグ時に読み取りにくくなります。

LineItem.ancestors
[LineItem, #<Module:0x...>, Object, ... ]

Module Builderパターンはこのように、無名モジュールのメリット(モジュールのインスタンスを「その場で」定義でき、かつグローバル定数の名前空間を汚さない)を得ながら、通常のモジュールと同様にインスタンスが名前を持つので、デバッグトレースが簡単です。そのうえ、ブートストラップするクラスをincludeするクラスでは新しいメソッドを定義する必要も呼び出す必要もありません。

それだけではありません。先ほどのコードでは、includeする側のクラスにログ出力のコードを少々追加しましたが、これもModule Builderとして書き直せます。これをLoggerBuilderと呼ぶことにします。

class LoggerBuilder < Module
  def initialize(method_name, start_message: "", end_message: "")
    define_method method_name do |*args, &block|
      print start_message
      super(*args, &block).tap { print end_message }
    end
  end
end

後はログ出力するメソッドの名前でLoggerBuilderのインスタンスを作成すれば、includeまたは継承したメソッドのログを出力できるようになります。

class LineItem < Struct.new(:amount, :tax)
  include AdderBuilder.new(:amount, :tax)
  include LoggerBuilder.new(:+, start_message: "Enter Adder...\n",
                                end_message: "Exit Adder...\n")
end

これだけで、足し算できる行項目からログが出力されます。

l1 = LineItem.new(9.99, 1.50)
l2 = LineItem.new(15.99, 2.40)

l1 + l2
Enter Adder...
Exit Adder...
#=> #<LineItem:... @amount=25.98, @tax=3.9>

もちろん、includeまたは継承されるどのメソッドも必要に応じてログ出力できます。

LineItem.include(LoggerBuilder.new(:to_s, start_message: "Stringifying...\n"))

l1.to_s
Stringifying...
=> "#<struct LineItem amount=9.99, tax=1.5>"

他にもいろんな側面がありますが、ここではごく表面的な説明にとどめます。現実に使われている、よりシンプルかつパワフルな実例については、Dry-rbのDry Equalizerをご覧ください。これは、AdderBuilderで使ったModuleクラスのサブクラス化と同じように、Equalizerクラスのインスタンスを使って同等性(equality)メソッドを動的にクラスに定義します。

Module Builderと「コンポジション」

本記事では、クラスにモジュールをincludeする文脈で「コンポジション(composition、composed)」という言葉が多用されていることにお気づきでしょうか。あるメソッドをオーバーライドするモジュールをincludeしてからsuperを呼ぶと、実質的に必ず関数(メソッド)のコンポジションになるのですが、このことは一般には指摘されていません。

訳注: コンポジションについては「Effective Java 16章「継承よりコンポジションを選ぶ」」をご覧ください。

これは、先のセクションで説明したログ出力コードについても同様です。AdderBuilderLoggerBuilderのインスタンスを両方includeすると、#+は関数コンポジションになるので、次のようにも書けます。

class LineItem < Struct.new(:amount, :tax)
  def add(other)
    self.class.new(amount + other.amount, tax + other.tax)
  end

  def log(result)
    print "Enter Adder..."
    result.tap { print "Exit Adder..." }
  end

  def +(other)
    log(add(other))
  end
end

したがって、superは本質的に最後のメソッド呼び出しの出力を受け取って、現在のモジュールのメソッドの結果に組み入れます。

偶然にも、コンポジションでは、あるModule Builderから生成された複数のインスタンスがきわめて興味深い方法で束ねられます。上の例では、ログ出力モジュールと追加用モジュールが束ねられます。Module Builderを使えば、さまざまなモジュールを設定して束ね、メソッドやクラスのより複雑な振舞いをビルドすることができます。

その方法のひとつは、メソッドフローの「分岐」です。モジュールをいくつか定義し、それぞれのモジュールが同じメソッドをオーバーライドします。そしてメソッド内の条件と引数がモジュールのステートと一致したら何か特別な処理を行い、一致しない場合はsuperによって制御フローがクラス階層の上のモジュールに移動する、というものです。

「コンポジションの分岐」といえば、Rubyを使った経験が少しでもある方なら例のmethod_missingメソッドを連想することでしょう。superせずにmethod_missingをオーバーライドする人はまずいないはずです。そんなことをしたら、クラスに定義されていないメソッドまでキャッチしてしまう可能性があり、一般的にはそうした動作を望む人はいないでしょう。

method_missingの典型的なオーバーライドでは、以下のようにsuperするのが普通です。

def method_missing(method_name, *arguments, &block)
  if method_name =~ METHOD_NAME_REGEX
    # ... 何か特別な処理を行う ...
  else
    super
  end
end

上のMETHOD_NAME_REGEXは、「何か特別な処理を行う」べきかどうかを調べる正規表現です。

実際私は、MobilityFallthroughAccessorsというModule Builderでまさにこのフロー分岐を使いました。FallthroughAccessorsの各インスタンスは属性のセット(=ステートを表す)で初期化され、属性の1つにロケールサフィックスを追加したメソッド名(例: フランス語のタイトル属性はtitle_fr)が生成される場所でメソッド呼び出しをインターセプト(intercept: 奪い取る)します。私はこれを「i18nアクセサ」と呼んでいます。メソッド名が一致すれば、そのサフィックスのロケールに含まれる属性値が返され、一致しない場合、制御は引き続きクラス階層を上昇します。

Module Builderでこのようにmethod_missingのコンポジションを行うと、正規表現のネストした条件が一連のモジュール全体に広がります。私はこれをインターセプタ(interceptor)と呼んでいます。これらのインターセプタは全体がカプセル化され、そのクラス自身が外部に依存しないという事実があります。そのためMobilityのi18nアクセサは、翻訳された各属性に自由に「プラグイン」できるようになります。しかも両者は互いに完全に独立しています。

私はこのパターンがMobilityで有用であることに気づき、MethodFoundというgemに切り出しました。MethodFoundは本質的にMethodFound::Interceptorという1つのModule Builderであり、1つの正規表現やproc(ここではステート)に一致するメソッド呼び出しをインターセプトし、メソッド名/正規表現でキャプチャできたマッチ(またはprocの戻り値やメソッド)のすべての引数をブロック(上の「何か特別な処理を行う」の部分)に渡します3。このブロックは、モジュールをincludeするクラスのインスタンスのコンテキストで評価されます。

次に例を示します。

class Greeter < Struct.new(:name)
  include MethodFound::Interceptor.new(/\Asay_([a-zA-Z_]+)\Z/) { |method_name, matches|
    "#{name} says: #{matches[1].gsub('_', ' ').capitalize}."
  }
  include MethodFound::Interceptor.new(/\Ascream_([a-zA-Z_]+)\Z/) { |method_name, matches|
    "#{name} screams: #{matches[1].gsub('_', ' ').capitalize}!!!"
  }
  include MethodFound::Interceptor.new(/\Aask_([a-zA-Z_]+)\Z/) { |method_name, matches|
    "#{name} asks: #{matches[1].gsub('_', ' ').capitalize}?"
  }
end

上のmethod_missingは3つのインターセプタのコンポジションです。Greeterクラスにはこれら以外にトレースがないにもかかわらず、含まれている正規表現マッチャーに対応する3つのモジュールビルダをancestorsで直接表示できます(表示のため、インターセプタはModule#inspectをオーバーライドしています)。

Greeter.ancestors
=> [Greeter,
#<MethodFound::Builder:0x...>,
#<MethodFound::Interceptor: /\Aask_([a-zA-Z_]+)\Z/>,
#<MethodFound::Interceptor: /\Ascream_([a-zA-Z_]+)\Z/>,
#<MethodFound::Interceptor: /\Asay_([a-zA-Z_]+)\Z/>,
Object, ...]

つまり、クラスにないメソッドが呼び出されると、呼び出しはクラス階層を上昇して最初の「ask」インターセプタの正規表現と一致するかどうかをチェックします。一致する場合は値を1つ返し、一致しない場合はその上の階層にある「scream」インターセプタで次の正規表現と一致するかどうかをチェックし、という具合に繰り返します。

結果は次のとおりです。

greeter = Greeter.new("Bob")
greeter.say_hello_world
#=> "Bob says: Hello world."
greeter.scream_good_morning
#=> "Bob screams: Good morning!!!"
greeter.ask_how_do_you_do
#=> "Bob asks: How do you do?"

上の例ではmethod_missingとModule Builderが使われていますが、ここで1つの疑問が生じます。上のような実装をModule Builderを使わずに、しかも同じ程度に柔軟な方法で行うことは果たして可能でしょうか。

仮にModule Builderを使わないことにすると、まずモジュールのステートの置き場所がなくなってしまいます。「普通の」モジュールにはそんなものを置く場所はありません。つまり、インターセプタ内の正規表現やブロックは別の場所に置く必要があるでしょう。これらを自然に置ける場所があるとすれば、includeする側のクラス自身しかありません。

このような方法には多くの問題があり、現実の例を1つ見てみれば問題点が明確になるはずです。

図版(#2で使用しているもの)

c “Proposed demonstration of simple robot self-replication.” reference

関連記事

[保存版]人間が読んで理解できるデザインパターン解説#1: 作成系(翻訳)

[保存版]人間が読んで理解できるデザインパターン解説#2: 構造系(翻訳)

Rubyで学ぶデザインパターンを社内勉強会で絶賛?連載中


  1. 実際には、これらのキーはクロージャを用いて定義されています。 
  2. amounttaxといった変数をincludeするこのModule Builderクラスに#inspectメソッドも定義しておけば、ancestorsチェインがさらに読みやすくなります。 
  3. MethodFoundでは他にも行っていることがあります。正規表現やprocに一致するメソッド名を「キャッシュ」してインターセプタモジュール上で定義し、以後の呼び出しを大幅に高速化しています。method_missingでメソッド名と一致した後にインターセプタ上のメソッドを調べてみればおわかりいただけると思います。 

週刊Railsウォッチ(20171020)Rubyが来年で25周年、form objectでサニタイズ、コアなString解説本ほか

$
0
0

こんにちは、hachi8833です。WPA2プロトコルの脆弱性の次は、特定のセキュリティチップでRSA秘密鍵の脆弱性が見つかったそうです。

10月3回目のRailsウォッチ、いってみましょう。

来年2/24に品川でRuby 25周年記念イベント


http://25.ruby.or.jp/より

Matzを始めとする講演がいくつか行われるそうです(詳細は変更の可能性あり)。

Ruby25 周年記念イベント開催実行委員会
委員長 まつもとゆきひろ(一般財団法人 Rubyアソシエーション理事長)
副委員長 井上 浩(ネットワーク応用通信研究所 代表取締役)
委員
– 前田修吾(ネットワーク応用通信研究所 取締役)
– 高橋征義(一般社団法人 日本Rubyの会 代表理事/株式会社達人出版会 代表取締役)
– 笹田耕一(一般財団法人 Rubyアソシエーション 理事/クックパッド株式会社)
http://25.ruby.or.jp/images/sponsorship.pdfより

Rails: 今週の改修

公式の更新がなかったのでcommitから見繕いました。

headless chrome driverとchromedriver-helper gemを追加

# actionpack/test/abstract_unit.rb
+class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase
+  driven_by :selenium, using: :headless_chrome
+end
# railties/lib/rails/generators/rails/app/templates/Gemfile
  # Adds support for Capybara system testing and selenium driver
   gem 'capybara', '~> 2.15'
   gem 'selenium-webdriver'
+  # Easy installation and use of chromedriver to run system tests with Chrome
+  gem 'chromedriver-helper'
   <%- end -%>
 end

つっつきボイス: 「上はy-yagiさんでした」「capybaraたんガンバレー」

GROUP BYORDER BYLIMITを使った場合のCOUNT(DISTINCT ...)を修正

# activerecord/lib/active_record/relation/calculations.rb
         if operation == "count"
           column_name ||= select_for_count
           if column_name == :all
-            if distinct && !(has_limit_or_offset? && order_values.any?)
+            if distinct && (group_values.any? || !(has_limit_or_offset? && order_values.any?))
               column_name = primary_key
             end
           elsif column_name =~ /\s*DISTINCT[\s(]+/i

つっつきボイス:#30886の問題が修正されたのか↓」

#30886より
モデル.group(:id).order('1 DESC').limit(30).distinct.count

# 期待するSQL(モデルは`device`)
SELECT COUNT(DISTINCT "devices"."id") AS count_id, "devices"."id" AS devices_id FROM "devices" GROUP BY "devices"."id" ORDER BY 1 DESC LIMIT $1

# 実際のSQL: SELECT COUNT(DISTINCT *)の部分がおかしい
SELECT COUNT(DISTINCT *) AS count_all, "devices"."id" AS devices_id FROM "devices" GROUP BY "devices"."id" ORDER BY 1 DESC LIMIT $1

db/schema.rbでMySQLのauto_increment: trueチェックを追加

# activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
           def prepare_column_options(column)
             spec = super
             spec[:unsigned] = "true" if column.unsigned?
+            spec[:auto_increment] = "true" if column.auto_increment?

to_s(:db)をアルファベット文字列のrangeにも対応

# activesupport/lib/active_support/core_ext/range/conversions.rb
module ActiveSupport::RangeWithFormat
   RANGE_FORMATS = {
-    db: Proc.new { |start, stop| "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" }
+    db: -> (start, stop) do
+      case start
+      when String then "BETWEEN '#{start}' AND '#{stop}'"
+      else
+        "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'"
+      end
+    end
   }

つっつきボイス:Range#to_s(:db)ってのがあるとは」「手元のRails環境でrails consoleやってみると確かにString(アルファベット文字)でto_s(:db)がエラーになる↓: これが修正されたということか」

irb(main):003:0> range = (1..100)
=> 1..100
irb(main):004:0> range.to_s
=> "1..100"
irb(main):005:0> range.to_s(:range)
=> "1..100"
irb(main):006:0> range.to_s(:db)
=> "BETWEEN '1' AND '100'"
irb(main):007:0> range = ('a'..'z')
=> "a".."z"
irb(main):008:0> range.to_s(:db)
ArgumentError: wrong number of arguments (given 1, expected 0)
        from (irb):8

「他にも#to_s(db)できるクラスってあるかな: お、ActiveSupport::TimeWithZoneにもある」

    # Returns a string of the object's date and time.
    # Accepts an optional <tt>format</tt>:
    # * <tt>:default</tt> - default value, mimics Ruby Time#to_s format.
    # * <tt>:db</tt> - format outputs time in UTC :db time. See Time#to_formatted_s(:db).
    # * Any key in <tt>Time::DATE_FORMATS</tt> can be used. See active_support/core_ext/time/conversions.rb.
    def to_s(format = :default)
      if format == :db
        utc.to_s(format)
      elsif formatter = ::Time::DATE_FORMATS[format]
        formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
      else
        "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby Time#to_s format
      end
    end

RailsのRuboCopを0.50.0にアップデート

# .codeclimate.yml
 engines:
   rubocop:
     enabled: true
-    channel: rubocop-0-49
+    channel: rubocop-0-50

つっつきボイス: 「今さらですが、Railsフレームワーク開発にもRuboCopが使われているんだなと思って」「CodeClimateで回してますな」


codeclimate.comより

Rails

RubyのアプリサーバーがMac OS High Sierraで落ちる理由


blog.phusion.nlより

この間のRailsウォッチでPumaが落ちる問題(#1421)をお伝えしましたが、他のRubyサーバーも含め、原理や対応策を詳しく解説しています。PassengerでおなじみのPhosionなので、Passengerは次の5.1.11でこの問題の回避策を含めるそうです。

High SierraでObjective-Cライブラリが変更されたことで顕在化しましたが、根本的にはマルチスレッドとfork()が絡み合った問題であり、Linuxも無縁ではないそうです。


つっつきボイス: 「High Sierraでは落ちるべきときに即落ちるようになったということか」「マカーだけどMacで本番サーバーを動かすことはないし、最近はDocker使うことも多いのでそっちに移行すればいいんじゃないかしら」

Puma/Unicorn/Passengerの効率を最大化する設定(Awesome Rubyより)


speedshop.coより

以前もTechRachoでパフォーマンス関連の記事を翻訳させていただいたNate Berkopecさんが以下の値の設定について解説しています。

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

つっつきボイス: 「解説系の記事だけど、もう少しだけ指標をバシッとわかりやすく出してくれたらうれしいかな: Rails serverに特化しているようでもないし」「例のComplete Guide to Rails Performanceの著者なので、そっちを買ってくださいということかもですね」

railsspeed.comより

default_scopeを使ってはいけない理由(Ruby Weeklyより)


andycroll.comより

とても短い記事です。他の記事も小粒に抑えられていて読みやすいです。


つっつきボイス: 「うんうん、ActiveRecordのdefault_scopeは使うと後で困ったことになるやつ」「ちょうどBPSの新人君がこの間default_scope踏んで相当頭にきてた」

よくある?Rails失敗談 default_scope編

書籍: Effective Testing with RSpec 3(Ruby Weeklyより)


pragprog.comより


つっつきボイス: 「RSpec 3の基本を学ぶにはよさそうかな」「ただ、実際業務で欲しいテストケースやマッチャーを本だけで網羅するのはたぶん不可能: たとえばDBが4つあるアプリのテストをどうやって書くか、とか」「そこから先は自分で頑張るかググって調べることになりますね」

form objectで属性をサニタイズする(Awesome Rubyより)


drivy.engineeringより

短いですが良記事です。

# 同記事より
class SanitizedVatNumber < Virtus::Attribute
  def coerce(value)
    value.respond_to?(:to_s) ? value.to_s.gsub(/\s+/, '') : value
  end
end

つっつきボイス: 「おー、こういうの結構好き: form objectVertusは相性がいいし、そこに#coerceを組み合わせるのがうまい」「テストが単純になるのもいい」

参考

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

ActiveRecordクエリのトリック


medium.com/rubyinsideより

  1. 関連付けられたテーブルに条件付きでJOIN
  2. nested JOINする別の方法
  3. 存在確認クエリ
  4. サブクエリ
  5. ActiveRecordの基本を思い出そう
  6. 論理値

つっつきボイス: 「新しい話は特にないけど、クエリの使い方の一覧として見ておくといいかも」

Railsで動的なエラーページを作る(RubyFlowより)


pooreffort.comより


つっつきボイス: 「エラーページを動的にしたい気持ちはわかるけど、Railsサーバーが死んだら表示されなくなるのでおすすめしない」

Rails APIにはどのJSON Serializerがいいか(RubyFlowより)


www.carlosramireziii.comより


つっつきボイス: 「Jbuilderは最近それほど評価されてないのをよく見かけるので、ActiveModel::Serializersが市民権を獲得しつつあるのかもしれない」

Rails: ActiveModelSerializersでAPIを作る–Part 1(翻訳)

Aruba: コマンドラインツールをCucumber/RSpec/Minitestでテストするgem(Awesome Rubyより)

1か月ほどで★700個超えです。


つっつきボイス: 「どうやって使うのかなと思ったら、#run_commandなどのhelperを追加してCLIのテストを書けるようにしているのか↓」

# app.cucumber.proの /04_aruba_api/command/find_a_started_command.feature より
require 'spec_helper'

RSpec.describe 'Run command', :type => :aruba do
  before(:each) { run_command('echo hello') }
  let(:command) { find_command('echo hello') }

  before(:each) { stop_all_commands }

  it { expect(command).to be_successfully_executed }
  it { expect(command.commandline).to eq 'echo hello' }
end

ReportsKit: Railsで使える美しいチャート/表(Ruby Weeklyより)


github.com/tombenner/reports_kitより


つっつきボイス: 「この種のChartライブラリは他にもあるけど、JSのobjectでとても深いnested設定オブジェクトを作る代わりにYAMLで設定書ける↓というのはそれだけで嬉しいですね」「サイトでBootstrap使っているのがひと目でわかる」

measure: post
filters:
- author
- created_at
dimensions:
- created_at
- author
chart:
  type: line
  options:
    scales:
      yAxes:
      - stacked: true

Redisの作者だけど15年間英語で苦しみまくってた

英語の学習速度がこんなに遅かったのは、習い始めに英語を目で読んでばかりでリスニングをやってなかったせいだ。おかげで発音がめちゃめちゃなまま単語を覚えてしまった。今英語学んでるやつに言っとく: 話せるようになりたかったら絶対リスニングやれ。

4年前の記事ですが、ほろっときたので翻訳しようと思います。


redis.ioより

Railsのディレクトリを解説(Ruby Weeklyより)

一度見れば十分だと思いますが、Rails本体だけでなく、著名なgemで追加されるディレクトリも解説しているのが珍しいと思いました。

nullalign: NOT NULL制約のつけ忘れを警告するgem(Ruby Weeklyより)

$ bundle exec nullalign
There are presence validators that aren’t backed by non-null constraints.
--------------------------------------------------------------------------------
Model              Table Columns
--------------------------------------------------------------------------------
Album              albums: name, owner_id
AttendanceRecord   attendance_records: group_id, attended_at
CheckinLabel       checkin_labels: name, xml
CheckinTime        checkin_times: campus
CustomField        custom_fields: name, tab_id

作者のTom CopelandさんはRuby Hero 2008の受賞者です。

PgParty: Active RecordのPostgreSQL 10パーティショニングgem(Ruby Weeklyより)

# github.com/rkrage/pg_partyより
class CreateSomeListRecord < ActiveRecord::Migration[5.1]
  def up
    create_list_partition :some_list_records, partition_key: :id do |t|
      t.text :some_value
      t.timestamps
    end

    create_list_partition_of \
      :some_list_records,
      partition_key: :id,
      values: (1..100).to_a

     create_list_partition_of \
       :some_list_records,
       partition_key: :id,
       values: (100..200).to_a
  end
end

つっつきボイス: 「PostgreSQL 10のRange Partitioningはとても簡単に使えるようになってていいらしい」「↓この記事がわかりやすかった」

Vanilla Rails: Railsのgemをどこまで断捨離できるか(Awesome Rubyより)

メリットは「速い」「わかりやすい」「gemにわずらわされない」など、デメリットは「生産性が落ちた」「車輪の再発明」だったそうです。


つっつきボイス: 「rspec-railsとhttpとactive_model_serializersだけは手放せないのね」
httpってgemあるんですね↓: すごく検索しにくそう」「Net::HTTPとは別のHTTPクライアントですが、今ならhttp_clientかな」

Rails 5ブロンズ試験のベータ版が11/12に実施

Ruby trunkより

ubygems.rbを削除(Ruby Weeklyより)

# tool/sync_default_gems.rb
   case gem
    when "rubygems"
 -    `rm -rf lib/rubygems* lib/ubygems.rb test/rubygems`
 +    `rm -rf lib/rubygems* test/rubygems`
      `cp -r ../../rubygems/rubygems/lib/rubygems* ./lib`
 -    `cp -r ../../rubygems/rubygems/lib/ubygems.rb ./lib`
      `cp -r ../../rubygems/rubygems/test/rubygems ./test`

ubygems.rbって何だろうと思ったら、オプションを-rubygemsと書けるようにするためだけのものでした」「あー、あるあるそういうの」「このオプションに別れを惜しむ人が結構いるんですね↓」


e5e1f9より

Ruby

Rubyのreduceの基本を知る

# 同記事より
def select(list, &fn)
  list.reduce([]) { |a, i| fn[i] ? a.push(i) : a }
end
select([1,2,3]) { |i| i > 1 }
# => [2, 3]

つっつきボイス:#inject#reduce(同じだけど)は使うたびにちょっとドキドキする」「#injectって実はパフォーマンスあまりよくなくて、最近は#sumの方が速かったりしますけどね」「reduceの動作をLispで説明しているのがわかりやすいかも↓」

(+ (+ (+ (0) 1) 2) 3)

rotoscope: 高速メソッド呼び出しロガーgem(Ruby Weeklyより)

まだ★は少ないですが、Shopifyなので一応。ちょっとしたデバッグに便利そうです。

# github.com/Shopify/rotoscopeより
require 'rotoscope'

class Dog
  def bark
    Noisemaker.speak('woof!')
  end
end

class Noisemaker
  def self.speak(str)
    puts(str)
  end
end

log_file = File.expand_path('dog_trace.log')
puts "Writing to #{log_file}..."

Rotoscope.trace(log_file) do
  dog1 = Dog.new
  dog1.bark
end

こんなふうにログを取れます。

event,entity,method_name,method_level,filepath,lineno
call,"Dog","new",class,"example/dog.rb",19
call,"Dog","initialize",instance,"example/dog.rb",19
return,"Dog","initialize",instance,"example/dog.rb",19
return,"Dog","new",class,"example/dog.rb",19
call,"Dog","bark",instance,"example/dog.rb",4
call,"Noisemaker","speak",class,"example/dog.rb",10
call,"Noisemaker","puts",class,"example/dog.rb",11
call,"IO","puts",instance,"example/dog.rb",11
call,"IO","write",instance,"example/dog.rb",11
return,"IO","write",instance,"example/dog.rb",11
call,"IO","write",instance,"example/dog.rb",11
return,"IO","write",instance,"example/dog.rb",11
return,"IO","puts",instance,"example/dog.rb",11
return,"Noisemaker","puts",class,"example/dog.rb",11
return,"Noisemaker","speak",class,"example/dog.rb",12
return,"Dog","bark",instance,"example/dog.rb",6

つっつきボイス: 「rotoscope、何かの発表で使っているところを見たような気がする」

GCPのStackdriverでRubyアプリのデバッグとログ出力が可能に


つっつきボイス: 「ここRailsでも使えるかな?」「使えるみたいです↓」「おー、StackDriverでRailsのログ見られるのいいな: Dockerコンテナでログをどうやって収集するかがいつもつらみなんで」

GitHubリポジトリ: stackdriver

Haml 5.0.4がリリース

Rubyで関数型プログラミング(Ruby Weeklyより)

同記事で以下のPFaaOという見慣れないパターンに言及していました。


つっつきボイス: 「pure functionは関数型言語で言う関数のようだ: 入力が同じなら必ず出力が同じ」
「数学だとそういうの何って言いましたっけ…」「単射かな」「単射は英語でinjective mappingなのかー」

k0kubunさんの「YARV-MJIT」記事

RubyKaigi 2017のLTではLLVMでJITをやっていたのが、今度はYARVでやってみたそうです。


つっつきボイス: 「k0kubunさんまじつよい」

mrubyのdo end内でrescueできるようになった

↓このコミットのようです。

書籍: Mastering Ruby Strings and Encodings(Ruby Weeklyより)

この間のTechRacho翻訳記事「ガイジン向けRubyKaigiガイド」の著者であるRichard Schneeman氏の推薦文付きです。

# 同記事より
'é' == 'é' # => false
[128077, 32, 128078].pack('U*')

つっつきボイス: 「これ個人的に好きな内容: 買ってみよう」「Stringだけで1冊書くってすごい」
「普通なら表示できない文字もpackでできると」「packって確かC言語由来でしたっけ」

Rubyカンファレンスを初めて仕切ってみた感想(Ruby Weeklyより)


2017.southeastruby.comより

以前のウォッチでも紹介したSouthEast Ruby 2017です。


つっつきボイス: 「カンファレンスの運営はほんと大変だと思う」「ざっくりだけど、会場費8000ドルといった予算も記事に書かれているのがいいですね: 自分でやってみたい人には参考になるかも」

台湾で2018年4月にRubyとElixirのカンファレンスが開催


2018.rubyconf.twより


つっつきボイス: 「RubyとElixirが並ぶカンファレンス初めて」「Rubyが液化して滴ってるみたいダナー」

JavaScript

JavaScriptの正規表現の奥深い概念


geekstrick.comより

// 同記事より
  var myRegExp = /Geeks Trick/igm;
  alert("Source: " + myRegExp.source +
        "\n Ignore Case : " + myRegExp.ignoreCase +
        "\n Global : " + myRegExp.global +
        "\n Multiline : " + myRegExp.multiline +
        "\n Last Index: " + myRegExp.lastIndex)

つっつきボイス: 「JSの正規表現、もうちょっと知っておきたいので読んでみようかな」「JSの正規表現はlook behindがないのが常々残念です: パフォーマンス悪いのはわかってるんですが」「\p{Katakana}みたいなUnicode文字クラス指定もないし」

CSS/HTML/フロントエンド

Mozillaの新生MDN Web DocsにMicrosoft/Google/W3C/Samsungなども参加


blogs.windows.comより

複数ブラウザ向けのWeb関連ドキュメントがWeb Docsに集約されることになるようです。Appleはリストに載ってませんね。

その他

DockerがKubernetesをネイティブサポート


blog.codeship.comより


つっつきボイス: 「これはもしかするとDocker Swarmの終了が近いのかな? Docker Swarmをproductionで使っているのを見たことないけど」

developerブートキャンプは衰退期に入ったか

ここには引用しませんが、記事からリンクされているReuterの統計情報を見ると2014年をピークに激減しています。

Algorithmia: AI機能のマーケットプレイス


algorithmia.comより

Googleが出資しているそうです。とりあえずアカウントを作って検索してみたところ、「japanese」ではまだ6件でしたが、その場で試すこともできました。


lgorithmia.comより

Alpha Goがゼロから学習して既存の人工知能を破る


deepmind.comより

番外

ネイティブみたいに発音できなくてもいい

この@e_kazumaさんの「今日のタメ口英語」は言葉のセンスが今風かつ非常によくて気に入りました。


今週は以上です。

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

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

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Frontend Weekly

frontendweekly_banner_captured

Hacker News

160928_1654_q6srdR

RailsのCSRF保護を詳しく調べてみた(翻訳)

$
0
0

概要

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

RailsのCSRF保護を詳しく調べてみた(翻訳)

現在Railsを使っていればCSRF保護を使うことがあるでしょう。この機能はRailsのほぼ初期から存在し、即座に導入して開発を楽にできるRailsの機能のひとつです。

CSRF(Cross-Site Request Forgery)を簡単に説明すると、悪意のあるユーザーがサーバーへのリクエストを捏造して正当なものに見せかけ、認証済みユーザーを装うという攻撃手法です。Railsでは、一意のトークンを生成して送信のたびに真正性を確認することでこの種の攻撃から保護します。

最近私がUnbounceのある機能を使ったとき、CSRF保護と、CSRF保護をクライアント側のJavaScriptリクエストでどう扱うかについて考慮が必要になりました。そのとき、自分がCSRF保護についてほとんど何も知らないどころか、CSRFが何の略語なのかも知らないことに気づきました。

そこで私は、Railsコードベースでこの機能がどのように実装されているかを詳しく調べることにしました。本記事では、RailsでのCSRF保護の動作を追ってみました。レスポンスごとのトークンが最初にどうやって生成されるか、およびサーバーへのリクエストの真正性のバリデーションについても解説いたします。

基本

CSRFには2つの要素で構成されます。最初にサイトのHTMLに一意のトークンを埋め込みます。これと同じトークンはセッションcookieにも保存されます。ユーザーがPOSTリクエストを送信するときに、HTMLに埋められていたCSRFトークンも一緒に送信されます。Railsはページのトークンとセッションcookie内のトークンを比較し、両者が一致することを確認します。

CSRF保護の利用法

Rails開発者はCSRF保護を無料で利用できます。最初に、application_controller.rbファイルでCSRF保護をオンにする以下の1行を有効にします。

protect_from_forgery with: :exception

次に、application.html.erbに次の1行を追加します。

<%= csrf_meta_tags %>

これでおしまいです。この機能は長年Railsに搭載されているので、開発者はこれを利用するかどうか決めるだけでよいのです。しかしこの機能はどのように実装されているのでしょうか?

生成と暗号化

まずは#csrf_meta_tagsから調べてみましょう。これはHTMLに真正性トークンを埋め込むシンプルなビューヘルパーです(gist: csrf_helper.rb)。

# actionview/lib/action_view/helpers/csrf_helper.rb

def csrf_meta_tags
  if protect_against_forgery?
    [
      tag("meta", name: "csrf-param", content: request_forgery_protection_token),
      tag("meta", name: "csrf-token", content: form_authenticity_token)
    ].join("\n").html_safe
  end
end

csrf-tokenタグにご注目ください。すべてのマジックはここで起きます。tagヘルパーは#form_authenticity_tokenを呼んで実際のトークンを取り出します。そしてActionControllerのRequestForgeryProtectionモジュールに進むと面白くなってきます。

RequestForgeryProtectionモジュールは、CSRF関連の一切を取り扱います。中でも有名なのはApplicationControllerで見かける#protect_from_forgeryです。これはリクエストごとにCSRFバリデーションをトリガするフックを設定し、リクエストの真正性を照合できなかった場合のレスポンスを設定します。この他にもCSRFトークンの生成/暗号化/復号化も担当します。このモジュールはスコープが小さい点が気に入りました。ビューヘルパーを別にすれば、CSRF保護の実装は1ファイルに収まっています。

続いて、CSRFトークンがHTMLに達するまでを詳しく見てみましょう。
#form_authenticity_tokenは、セッション自身を含む任意のオプションパラメータを#masked_authenticity_tokenに渡すシンプルなラッパーメソッドです(gist: request_forgery_protection.rb)。読みやすさのためコードの一部を省略しています。

# actionpack/lib/action_controller/metal/request_forgery_protection.rb

# トークンの値を現在のセッションに設定
def form_authenticity_token(form_options: {})
  masked_authenticity_token(session, form_options: form_options)
end

# リクエストごとに異なる真正性トークンのマスキング版を作成する
# マスキングはBREACHなどのSSL攻撃の緩和のため
def masked_authenticity_token(session, form_options: {}) # :doc:
  # ...
  raw_token = if per_form_csrf_tokens && action && method
    # ...
  else
    real_csrf_token(session)
  end

  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
  encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
  masked_token = one_time_pad + encrypted_csrf_token
  Base64.strict_encode64(masked_token)
end

Rails 5でフォームごとのCSRFトークンが導入されたため、masked_authenticity_tokenメソッドはやや複雑になっています。本記事では本来の実装である「リクエストごとに1つのCSRFトークンがmetaタグに達する」ところを追うことにします。この場合、上のelse分岐で#real_csrf_tokenの戻り値にraw_tokenが設定されます。

#real_csrf_tokensessionを渡す理由がおわかりでしょうか。このメソッドは実際には2つの動作を実行するからです。1つは暗号化されていない生トークンの生成、もう1つはトークンのセッションcookieへの埋め込みです(gist: equest_forgery_protection.rb)。

# actionpack/lib/action_controller/metal/request_forgery_protection.rb

def real_csrf_token(session) # :doc:
  session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
  Base64.strict_decode64(session[:_csrf_token])
end

このメソッドは最終的に、アプリのレイアウトの#csrf_meta_tagsが呼び出されると呼び出されることを思い出しましょう。これは昔ながらのRailsマジックです。この賢い副作用によって、セッションcookieのトークンがページのトークンと一致することが保証されます。保証される理由は、ページのトークンのレンダリングが行われるときには必ず同じトークンがcookieに挿入されるからです。

とにかく、#masked_authenticity_tokenの最後の方を見てみましょう(gist: request_forgery_protection.rb)。

# request_forgery_protection.rb
  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
  encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
  masked_token = one_time_pad + encrypted_csrf_token
  Base64.strict_encode64(masked_token)

ここでは暗号化が行われます。セッションcookieにはトークンを挿入済みなので、このメソッドはプレーンテキストHTMLで使われるトークンを返す作業にかかります。ここではいくつかの点に注意します(主にSSL BREACH攻撃の緩和のためですが、ここでは立ち入りません)。Rails 4以降はセッションcookie自体を暗号化するようになったため、セッションcookieに含めるトークンそのものは暗号化されない点にご注目ください。

最初に、生トークンの暗号化で使うワンタイムパッドを生成します。ワンタイムパッドは、長さの揃った平文メッセージをランダム生成キーで暗号化する手法で、メッセージの復号には同じキーが必要です。「ワンタイム(1回限り)」と呼ばれる理由は、メッセージごとに異なるキーを用い、利用後は破棄されるからです。Railsでは、CSRFトークンを新しく作成するたびに新しいワンタイムパットを生成し、平文トークンをビットごとのXOR操作で暗号化するためにこの機能を実装しています。このワンタイムパッド文字列は暗号化文字列の前に追加され、HTMLで使えるようBase64でエンコードされます。

CSRFトークン暗号化の仕組みの概要を図示します。デフォルトのトークンは長さは32文字ですが、ここでは12文字にしています。

操作が完了すると、マスクされた真正性トークンはスタックに戻され、アプリでレンダリングされたレイアウトに達します(gist: index.html)。

<!-- index.html -->
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="vtaJFQ38doX0b7wQpp0G3H7aUk9HZQni3jHET4yS8nSJRt85Tr6oH7nroQc01dM+C/dlDwt5xPff5LwyZcggeg==" />

復号化と照合

CSRFトークンの生成と、トークンがHTMLとcookieに達するまでの解説が終わりましたので、次はRailsへのリクエストのバリデーションを見てみることにしましょう。

ユーザーがサイトにフォームを送信すると、フォームの他のデータとともにCSRFトークンが送信されます(デフォルトのparam名はauthenticity_tokenです)。トークンは、HTTPヘッダーX-CSRF-Tokenでも送信できます。

先ほどApplicationControllerに以下を追加したことを思い出しましょう。

protect_from_forgery with: :exception

この#protect_from_forgeryメソッドは、すべてのコントローラアクションのライフサイクルの途中にbefore-actionを追加します。

before_action :verify_authenticity_token, options

このbefore_actionで、リクエストのparamsやヘッダーにあるCSRFトークンと、セッションcookieとの比較を開始します(gist: request_forgery_protection.rb)。

# actionpack/lib/action_controller/metal/request_forgery_protection.rb

def verify_authenticity_token # :doc:
  # ...
  if !verified_request?
    # エラー処理 ...
  end
end

# ...

def verified_request? # :doc:
  !protect_against_forgery? || request.get? || request.head? ||
    (valid_request_origin? && any_authenticity_token_valid?)
end
照合が成功する場合のフローに注目できるよう、一部のコードを省略しています。

いくつかの管理系タスクを実行後(HEADリクエストやGETリクエストなどの照合は不要です)、#any_authenticity_token_valid?の呼び出しで本格的な照合プロセスが開始されます(gist: request_forgery_protection.rb)。

# request_forgery_protection.rb
def any_authenticity_token_valid? # :doc:
  request_authenticity_tokens.any? do |token|
    valid_authenticity_token?(session, token)
  end
end

リクエストはトークンをフォームのparamsまたはヘッダーとして渡すことがあるので、Railsではいずれかのトークンがセッションcookie内のトークンと一致することだけが求められます。

#valid_authenticity_token?はそこそこ長いメソッドですが、要するに#masked_authenticity_tokenの逆操作を行って復号し、トークンを比較するだけです(gist: request_forgery_protection.rb)。

# request_forgery_protection.rb
def valid_authenticity_token?(session, encoded_masked_token) # :doc:
  # ...

  begin
    masked_token = Base64.strict_decode64(encoded_masked_token)
  rescue ArgumentError # encoded_masked_token の Base64は無効
    return false
  end

  if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
    # ...

  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
    csrf_token = unmask_token(masked_token)

    compare_with_real_token(csrf_token, session) ||
      valid_per_form_csrf_token?(csrf_token, session)
  else
    false # 不正なトークン
  end
end

最初に、Base64でエンコードされた文字列を受けて復号し、「マスク済みトークン」を得る必要があります。この後で、トークンのマスクを解除してセッションのトークンと比較します(gist: request_forgery_protection.rb)。

# request_forgery_protection.rb
def unmask_token(masked_token) # :doc:
  one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
  encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
  xor_byte_strings(one_time_pad, encrypted_csrf_token)
end

#unmask_tokenでトークン復号に必要な暗号マジックを行う前に、マスク済みトークンを必要なパーツ(ワンタイムパッド、暗号化済みトークン自身)に分割しておきます。続いて2つの文字列のXORを取ると、最終的な平文トークンを得られます。

最後に#compare_with_real_token(これはActiveSupport::SecureUtilに依存しています)でトークン同士が一致することを確認します(gist: request_forgery_protection.rb)。

# request_forgery_protection.rb
def compare_with_real_token(token, session) # :doc:
  ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
end

ついにリクエストが承認されました!「通るがよい(you shall pass)」

訳注: これはロード・オブ・ザ・リングのセリフのもじりです(本当はyou shall not pass: ここは通さぬ)。

まとめ

Railsには実にさまざまな要素があるので、Railsでは動いて当たり前のCSRF保護でこんなに頭を使ったのは初めてでした。たまにはこうやって魔法のカーテンの向こう側を覗いて実際の動作を眺めてみるのも楽しいものです。

CSRF保護の実装は、コードベースにおける「責任の分割」のよい例になっていると思います。モジュールを1つ作成し、小さくとも一貫したパブリックなインターフェイスで公開することで、コードベースにほぼまったく影響を与えずに背後の実装に任せることができます。Railsチームが長年にわたってフォームごとのトークンなどの新しい機能をCSRF保護に追加してくれているおかげで、CSRF保護が実際に動く様子をこうして見ることができます。

Railsのコードベースを詳しく調べるたびに、多くのことを学べます。本記事が、Railsのマジックに出会って仕組みを探るときのヒントになればと願っています。

関連記事

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

Ruby: 8/27発表のRubyGems脆弱性と修正方法のまとめ

そのパッチをRailsに当てるべきかを考える(翻訳)

Railsのsecret_key_baseを理解する(翻訳)

$
0
0

概要

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

Railsのsecret_key_baseを理解する(翻訳)

secret_key_baseの値の意味や、Railsアプリでの使われ方を知りたいと思ったことはありませんか。この設定値はRails 4から導入され、developmentやproductionといった環境ごとに定義されるのが普通ですが、値の目的はシンプルです。すなわち、値はアプリのkey_generatorメソッドのsecret入力として使われます

このメソッドはRails.application.key_generatorでアクセスでき、引数を取らず、ActiveSupport::CachingKeyGeneratorインスタンスをひとつ返します。続いて、CachingKeyGeneratorクラスが提供するgenerate_keyメソッドを使ってキーを導出します。secret_key_baseは、さまざまなセキュリティ機能で個別のキーを引き続き使えるようにしつつ、開発者が(訳注: 個別のキーを)設定する負荷を軽減する役割を持っています。

CachingKeyGeneratorクラスは特にActiveSupport::KeyGeneratorクラスをラップします。このクラスは名前のとおり、導出したキーを内部のハッシュにキャッシュして保存します。ハッシュのエントリはsalt入力によってインデックス化されます(gist: rails_app_key_generator.rb)。

# rails_app_key_generator.rb
# アプリのkey_generatorからキーを求める
# 戻り値: #<ActiveSupport::CachingKeyGenerator:0x00000004ae1b00 @key_generator=#<ActiveSupport::KeyGenerator:0x00000004ae1b28 @secret="...", @iterations=1000>, @cache_keys=#<Concurrent::Map:0x00000004ae1ad8 entries=0 default_proc=nil>>
Rails.application.key_generator

# 戻り値: "\a\xDD|\xEB\xE2\xD3\xEC\x05\xCA@C\xFBVD\xFB\xE5\x93o\e(\xDA\x83\x95}\xD3\x15\x91V\xA5y&6"
Rails.application.key_generator.generate_key('salt input', 32)

アプリのkey_generatorsecret_key_baseは、Railsフレームワーク内の3つのコア機能で利用されます。

  1. 暗号化cookieで使うキーの導出: coookies.encryptedでアクセス可能
  2. HMAC署名されたcookieで使うキーの導出: cookies.encryptedでアクセス可能
  3. アプリのすべての名前付きmessage_verifierインスタンスで使うキーの導出

1. 暗号化cookie

このcookieは、暗号化によってコンテンツに完全性と機密性を提供します。Railsのセッションcookieは完全性と機密性のために暗号化cookieからビルドされます。

暗号化方式に応じて、secret_key_baseから1つまたは2つのキーが生成されます。

GCM暗号化方式が使われると、config.action_dispatch.authenticated_encrypted_cookie_saltで定義されたsaltを使ってキーが1つ生成されます。このデフォルト値は"authenticated encrypted cookie"です。

参考: Wikipedia-ja: Galois/Counter Mode (GCM)

CBC暗号化方式が使われると、キーが2つ導出されます。AESアルゴリズムをCBCモードで使っているので、メッセージはMACによる認証(authenticate)も必要です。暗号化キーとベリファイキーは、config.action_dispatch.encrypted_cookie_saltconfig.action_dispatch.encrypted_signed_cookie_saltで定義されたsaltを用いて導出されます。それぞれのデフォルト値は、"encrypted cookie""signed encrypted cookie"です。

参考: Wikipedia-ja: CBCモード (Cipher Block Chaining Mode)
参考: Wikipedia-ja: メッセージ認証コード(MAC: Message Authentication Code)

2. 署名済みcookie

署名済みcookieは、HMACとSHA1ハッシュ関数を用いたセキュリティによってコンテンツに完全性を提供します。署名済みcookieの実装は暗号化cookieの実装に似ており、secret_base_keyから導出したキーを1つ使います。

署名済みcookieで使うキーを導出するときは、config.action_dispatch.signed_cookie_saltで定義された設定値がsaltに使われます。デフォルト値は"signed cookie"です。

3. アプリのmessage_verifier

Railsフレームワークでsecret_key_baseが使われる最後の場所は、アプリのmessage_verifierメソッドであり、これもRails.applicationでアクセス可能です。アプリのkey_generatorメソッドの場合と同様に、このメソッドもverifier_nameのみを引数として取り、引数はインデックス化に使われてMessageVerifierインスタンスに保存されます。この引数は、secret_base_baseからキーを1つ導出する際のsalt入力としても用いられます(gist: rails_app_message_verifier.rb)。

# rails_app_message_verifier.rb
# 戻り値:  #<ActiveSupport::MessageVerifier:0x00000003f4b430 @secret="...", @digest="SHA1", @serializer=Marshal>
Rails.application.message_verifier('bins')

# 戻り値: "BAh7BjoNdmVyaWZpZWRJIgp2YWx1ZQY6BkVU--9c516b0cc3bfcfd759550181541a24ca1294507e"
signed_msg = Rails.application.message_verifier('bins').generate({ verified: 'value' })

# 戻り値: {:verified=>"value"}
Rails.application.message_verifier('bins').verify(signed_msg)

# ActiveSupport::MessageVerifier::InvalidSignature をraiseする
Rails.application.message_verifier('bins').verify("unknown msg")

アプリのmessage_verifierメソッドは、メッセージ完全性機能のための簡単で便利なセキュリティAPIを提供し、いわゆる「remember me」トークンの実装や、署名済みURLによるリソースアクセス制御によく用いられます。このメソッドは、Rails 5.2で新しく導入されたActiveStorageの機能でも使われています。

ActiveSupport::KeyGeneratorについて

ActiveSupport::KeyGeneratorは、PBKDF2というKDF(鍵導出関数)の単なるラッパーです。このKDFのキー導出がパスワードベースであることを考えると、実際には最善のオプションではありません。特に、PBKDF2は人間が作ったパスフレーズを元に、キーのストレッチングと呼ばれるテクニックを反復的に使ってより強力なキーを生成する設計になっています。現実のRailsアプリで用いられるsecret_key_base値には、SecureRandomrake secretsでランダム生成されるセキュアな数値が使われるのが普通なので、人間が作ったパスフレーズよりも既に十分セキュアかつランダムな値になります。

実際には多くのRailsアプリで64バイト長のsecret_key_base値が使われているので、PBKDF2でキーを導出すると、キーから導出される実際の出力の長さは20バイト(160ビット)に限定されます。Rails全体にわたって導出されるキーには、HKDF(HMAC-based Key Derivation Function)を使う方がより適切のようです。HKDFでは「抽出の後拡大(extract-then-expand)」アプローチを採用しているので、より長いキーを生成できるようになります。

次回

私はRailsでのHKDF実装の検討を開始しました。この機能を改善できたらプルリクを作成するつもりです。今後にご期待ください。

関連記事

RailsのCSRF保護を詳しく調べてみた(翻訳)

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

Ruby: 8/27発表のRubyGems脆弱性と修正方法のまとめ

そのパッチをRailsに当てるべきかを考える(翻訳)

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

$
0
0

概要

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

Rubyroid Labsの別記事「Ruby on Railsで使ってうれしい19のgem(翻訳)」も合わせてどうぞ。

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

私たちRubyroid Labはアプリのアーキテクチャに多くの情熱を注ぎ込んでいます。手がけているプロジェクトの多くが長期にわたっているので、設計のどこかで少し油断すると、機能を1つ追加するのにプロジェクトをスクラッチからやり直す方が早い、といった事態になりかねません。こんな目には遭いたくないものです。

新しく参加したメンバーがロジック把握のためにソースコードを読みとおすだけでかなり時間がかかるようなら、それはプロジェクトが病んでいることを示す兆候のひとつです。本記事では、コードを整理してチームメンバーの笑顔を取り戻してくれるさまざまなgemをご紹介いたします。

1. interactor固定リンク

何らかの複雑なビジネスロジックを書くときには必ず私たちのリストに入るほど素晴らしいライブラリです。さてinteractorとは何でしょうか?Readmeには「ビジネスロジックのカプセル化というひとつの目的だけを持つシンプルなオブジェクトです」とあります。ビジネスロジックのカプセル化といえば皆さんの大好きなService Objectを連想するかもしれませんが、interactorはずっと機能が豊富です。早速コード例をご覧に入れましょう。

# app/interactors/create_order.rb
class CreateOrder
  include Interactor

  def call
    order = Order.create(order_params)

    if order.persisted?
      context.order = order
    else
      context.fail!
    end
  end

  def rollback
    context.order.destroy
  end
end
# app/interactors/place_order.rb
class PlaceOrder
  include Interactor::Organizer

  organize CreateOrder, ChargeCard, SendThankYou
end

コード例を見れば、このgemの実に素晴らしい機能がいくつもあることにお気づきかと思います。
まず、シンプルなinteractorをいくつかまとめて1つの実行可能なチェインにできるという点です。contextという特殊変数は、異なるinteractor同士でステートを共有するのに使われています。
次に、interactorのひとつが何らかの理由で失敗すると、それまでのinteractorはすべてロールバックします。CreateOrderクラスには#rollbackがあり、たとえばChargeCardSendThankYouが失敗すればorderは破棄されます。
実にクールなgemですね。

2. draper固定リンク

Railsでヘルパーを自作したことがあれば、時間とともにヘルパーが増えて手に負えなくなったことがあるでしょう。こうしたヘルパーは一部のデータの整形表示に使われることがほとんどです。このようなときはDecoratorデザインパターンの出番です。draperの文法は、見ればだいたいわかるようになっています。

# app/controllers/articles_controller.rb
def show
  @article = Article.find(params[:id]).decorate
end
# app/decorators/article_decorator.rb
class ArticleDecorator < Draper::Decorator
  delegate_all

  def publication_status
    if published?
      "Published at #{published_at}"
    else
      "Unpublished"
    end
  end

  def published_at
    object.published_at.strftime("%A, %B %e")
  end
end
<!-- app/views/articles/show.html.erb -->
<%= @article.publication_status %>

上のコードを見ると、published_at属性を特定のフォーマットで表示するのが目的になっています。古典的なRailsウェイでは、こういう場合に2つの選択肢がありました。

1つ目は単にそれ用のヘルパーを書くことです。この場合、すべてのヘルパーが同じ名前空間に属するので、プロジェクトが長期化するに連れていまいましい名前衝突が発生するようになり、デバッグも非常に困難になります。

2つ目はモデルの中にメソッドを書いてそれを使うことですが、モデルのクラスの責務を超えてしまうのでよろしくありません。モデルのデフォルトの責務は「データのやりとり」であり、データの表現方法ではないからです。

こういう場合にdraperを使うのは、よりエレガントに目的を達成できるからです。

次の素晴らしい記事も合わせてご覧ください。

Ruby on Railsで使ってうれしい19のgem(翻訳)

3. virtus固定リンク

シンプルなRubyオブジェクトをそのまま使っても要件を満たせないことがあります。たとえば1つのページに複雑なフォームがいくつもあり、フォームごとに異なるモデルとしてデータベースに保存しなければならないとします。こんなときはvirtusです。次の例をご覧ください。

class User
  include Virtus.model

  attribute :name, String
  attribute :age, Integer
  attribute :birthday, DateTime
end
user = User.new(:name => 'Piotr', :age => 31)
user.attributes # => { :name => "Piotr", :age => 31, :birthday => nil }

user.name # => "Piotr"

user.age = '31' # => 31
user.age.class # => Fixnum

user.birthday = 'November 18th, 1983' # => #<DateTime: 1983-11-18T00:00:00+00:00 (4891313/2,0/1,2299161)>

# mass-assignment
user.attributes = { :name => 'Jane', :age => 21 }
user.name # => "Jane"
user.age  # => 21

見てのとおり、virtusは標準的なOpenStructクラスと似ていますが、ずっと機能が豊富です。ぜひvirtusを隅々まで試してみてください。

virtusは手始めに使うのによいgemですが、もっと高度な技法を使いたい場合は、dry-typesdry-structdry-validation gemもぜひお試しください。

4. cells固定リンク

Nick Suttererの手によるRuby on Railsの高度なアーキテクチャをまだご存じない方は、ぜひ一度チェックしてみましょう。このアーキテクチャのコンセプト全体を既存のアプリに必ずしも適用できるとは限りませんが、ユーザーの種類などに応じた条件をいくつも適用していくうちにビューが複雑になりすぎてしまったらcells gemの出番です。cellsはビューの一部を切り離し、Rubyの通常のクラスとしてコンポーネント化できます。次のコードサンプルをご覧ください。

# app/cells/comment_cell.rb
class CommentCell < Cell::ViewModel
  property :body
  property :author

  def show
    render
  end

  private

  def author_link
    link_to "#{author.email}", author
  end
end
<!-- app/cells/comment/show.html.erb -->
<h3>New Comment</h3>
  <%= body %>

By <%= author_link %>
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  def index
    @comments = Comment.recent
  end
end
<!-- app/controllers/dashboard/index.html.erb -->
<% @comments.each do |comment| %>
  <%= cell(:comment, comment) %>
<% end %>

上の例では、ダッシュボードに最近のコメントを表示したいと考えています。すべてのコメント表示をアプリ上で同一にすることが前提です。Railsはレンダリングに共有パーシャルを使うこともできますが、代わりにCommentCellオブジェクトを使います。このオブジェクトは、前述のdraperにビューのレンダリング機能を組み合わせたものとみなせますが、もっと機能が豊富です。すべてのオプションの詳細についてはgemのReadmeをご覧ください。

5. retryable固定リンク

現代的なWebアプリにはさまざまな機能が統合されています。確実なAPI呼び出しが使えることもありますが、ファイルのFTPアップロードや何らかのバイナリプロトコルを使わなければならないこともあります。後者の問題は、呼び出しがこれといった理由なしにときどき失敗することです。このような場合にできる最善手はリトライです。次のようなコードを書いて切り抜けなければならなかったことがあるでしょう。

begin
  result = YetAnotherApi::Client.get_info(params)
rescue  YetAnotherApi::Exception => e
  retries ||= 0
  retries += 1
  raise e if retries > 5
  retry
end

こんなときこそretryableの出番です。このgemを使って上のコードを次のように書き直してみましょう。

Retryable.retryable(tries: 5, on: => YetAnotherApi::Exception) do
  result = YetAnotherApi::Client.get_info(params)
end

コードがずっとすっきりしましたね。retryableは他の状況にも使えるので、ぜひチェックしてみてください。

6. decent_exposure固定リンク

(Rubyの)マジックがあまり好きでない方はこのライブラリを使わなくてもよいでしょう。しかしアプリによっては、きわめてシンプルな標準CRUDアクションがたくさん複製されることがあります。こんなときにはdecent_exposureの出番です。何かを管理する新しいコントローラを作成するときを考えてみましょう。scaffoldすると次のような感じになります。

class ThingsController < ApplicationController
  before_action :set_thing, only: [:show, :edit, :update, :destroy]

  def index
    @things = Thing.all
  end

  def show
  end

  def new
    @thing = Thing.new
  end

  def edit
  end

  def create
    @thing = Thing.new(thing_params)

    respond_to do |format|
      if @thing.save
        format.html { redirect_to @thing, notice: 'Thing was successfully created.' }
      else
        format.html { render :new }
      end
    end
  end

  def update
    respond_to do |format|
      if @thing.update(thing_params)
        format.html { redirect_to @thing, notice: 'Thing was successfully updated.' }
      else
        format.html { render :edit }
      end
    end
  end

  def destroy
    @thing.destroy
    respond_to do |format|
      format.html { redirect_to things_url, notice: 'Thing was successfully destroyed.' }
    end
  end

  private

  def set_thing
    @thing = Thing.find(params[:id])
  end

  def thing_params
    params.require(:thing).permit(:for, :bar)
  end
end

コードが60行にもなればもう少ないとは言えません。Rubyistたるものコードはいつもできるだけ最小限に保ちたいものです。decent_exposureを使えば、以下のようなコードを書けます。

class ThingsController < ApplicationController
  expose :things, ->{ Thing.all }
  expose :thing

  def create
    if thing.save
      redirect_to thing_path(thing)
    else
      render :new
    end
  end

  def update
    if thing.update(thing_params)
      redirect_to thing_path(thing)
    else
      render :edit
    end
  end

  def destroy
    thing.destroy
    redirect_to things_path
  end

  private

  def thing_params
    params.require(:thing).permit(:foo, :bar)
  end
end

できました!何ひとつ機能を失わずにコードが30行そこそこにまで減っています。お気付きのとおり、すべてのマジックを発揮しているのはexposeです。内部の詳しい動作を理解するにはgemのドキュメントをご覧ください。

7. groupdate固定リンク

開発者なら誰しも、異なるタイムゾーンを扱うつらさが身に沁みていることでしょう。データベースで集約(aggregation)を行うときは特にそうで、私も「今月のユーザー数を日別で取れるようにせよ: ただし無料ユーザーは除くこと」などといったオーダーはいつも悩みの種です。でもこのgemがあればそんな心配から解放されます。次の例をご覧ください。

User.paid.group_by_week(:created_at, time_zone: "Pacific Time (US & Canada)").count
# {
#   Sun, 06 Mar 2016 => 70,
#   Sun, 13 Mar 2016 => 54,
#   Sun, 20 Mar 2016 => 80
# }

ご紹介した7つのgemで皆様が快適な開発生活を送ることを願っています。質の高いコードを高い表現力で書けるツールを他にもご存知でしたら、ぜひ私どもまでお知らせください。

関連記事

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

Ruby on Railsで使ってうれしい19のgem(翻訳)

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

$
0
0

概要

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

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

Query Object(または単にQuery)パターンもまた、Ruby on Rails開発者が肥大化したActiveRecordモデルを分割し、コントローラをスリムで読みやすくするのに非常に有用なパターンです。本記事はRuby on Railsを念頭に置いていますが、このパターンは他のフレームワーク(特にMVCベースでActiveRecordパターンを適用できるもの)にも簡単に適用できます。

どんなときにQuery Objectパターンを使うか

ActiveRecordリレーションで実行しなければならないクエリが複雑になったら、Query Objectパターンの利用を検討すべきです。スコープをこの目的に使うことはおすすめできません。

目安として、スコープが複数のカラムとやり取りする場合や、他のテーブルとJOINする場合は、Query Objectへの移行を検討すべきです。これにより、モデルに定義するスコープの数を必要最小限に減らすという副次的効果も得られます。同様に、スコープのチェインを扱う場合は常にQuery Objectの利用を検討すべきです(関連記事)。

Query Objectパターンを最大限に利用するための注意点

1. 命名規則を定める

素晴らしいQuery Objectクラスに楽に名前を付けられるよう、基本的な命名規則をいくつか定めましょう。規則のひとつとして考えられるのは、Queryオブジェクト名の末尾にQueryを追加することです。こうすることで今扱っているものがActiveRecordの子孫ではなくQueryであることを常に意識できます。
その他に、モデル名を複数形にして、Queryがどのオブジェクトと協調動作するよう設計されているかを示す方法も考えられます。たとえばRecentProjectUsersQueryというQuery Objectは、呼び出されるとUserのリレーションを返すことが明確にわかります。どの規則を選ぶにしても、パターンに基づいたクラスの命名法が一貫すれば新規導入クラスの命名に迷う時間を減らせるので、メリットを得られることが多くなります。

2. リレーションを返す.callメソッドをQuery Objectの呼び出しに使う

Service Objectでは、Service Objectを使う専用メソッドの命名方法にある程度選択の余地がありますが、対照的に、RailsでQuery Objectパターンを最大限に活用するには、リレーションオブジェクトを返す.callメソッドを実装すべきです。この規則に従うことで、必要に応じてQuery Objectで簡単にスコープを構成できるようになります(関連記事)。

3. オブジェクトなどのリレーションは常に第1引数で受け取る

導入するQuery Objectの呼び出しでは、第1引数でリレーションを受け取るのがよい方法です。Query Objectをスコープとして利用するときに第1引数のリレーションが必須(2.の推奨事項を参照)になりますし、Query Objectをチェインできるので柔軟性も高まります。Query Objectの使いやすさを損なわないためには、デフォルトのエントリリレーションを設定して、引数なしでもQuery Objectを利用できるようにしましょう。また、リレーションQuery Objectが提供されたときと同じ主題(テーブル)を持つQuery Objectから常にリレーションを返すことも重要です。

4. 追加オプションを受け取れるようにする

追加オプション受け取りの必要性は、既存のQuery Objectや新規Query Objectの導入時にサブクラス化することである程度回避できますが、いずれQuery Objectで追加オプションを受け取る必要が生じます。Query Objectで追加オプションを受け取れるようにしておけば、結果をどのように返すかというロジックをカスタマイズできるので、Query Objectを柔軟なフィルタとして効果的に利用できます。コードが読みにくくならないよう、追加オプションは必ずハッシュまたはキーワード引数として渡し、デフォルト値も設定しておくことをおすすめします。

5. 読みやすいクエリメソッドを書くことに集中する

Queryのコアロジックを.callメソッド自身の中に保存する場合であっても、Query Objectの別のメソッドに保存する場合であっても、常に読みやすさを心がけるべきです。他の開発者はQuery Objectの意図を確認する際にクエリメソッドを調べるので、クエリメソッドで少し手間をかけておけばQuery Objectが活用しやすくなります。

6. Query Objectを名前空間でグループ化する

プロジェクトの複雑さや、ActiveRecordをどの程度利用するかによって多少異なりますが、いずれQuery Objectはどんどん増えていきます。コードを整理するよい方法のひとつは、類似したQuery Objectを名前空間でグループ化することです。Queryが扱うモデルの名前でグループ化しても構いませんし、十分な理由付けがなされていれば何を使っても構いません。これまでと同様、Query Objectのグループ化方法も1つに決めておくことで、新規導入するクラスの適切な配置が楽に決まります。Query Objectをすべてapp/queriesディレクトリに保存する方法もおすすめです。

7. すべてのメソッドを.callの結果に委譲することも検討

Query Object用のmethod_missingを実装して全メソッドを.callメソッドの結果に委譲する方法も考えられます。この方法の場合、Query Objectは単に通常のリレーションとして用いられます(例: RecentProjectUsersQuery.call.where(first_name: “Tony”)ではなくRecentProjectUsersQuery.where(first_name: “Tony”)になる)。しかし、メタプログラミングと同様、この方法を選ぶ際には十分な検討と理由付けを行うべきです。

まとめ

Query Objectパターンは、実装の複雑なクエリ/リレーション/スコープを抽象化できるシンプルなパターンであり、テストも簡単になります。上述のシンプルな規則に従うことで、可読性や柔軟性を失わずにこのパターンを簡単に利用できるようになります。開発者自身はもちろん、何より将来そのコードを使う他の開発者にとってメリットになります。そのようなQuery Objectの実装例を以下に示します。

module Users
  class WithRecentlyCreatedProjectQuery
    DEFAULT_RANGE = 2.days

    def self.call(relation = User.all, time_range: DEFAULT_RANGE)
      relation.
        joins(:projects).
        where('projects.created_at > ?', time_range.ago).
        distinct
    end
  end
end

Query Objectパターンをシンプルに抽象化したい場合は、rails-patterns gemが提供するラッパーの導入をご検討ください。

関連記事

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

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

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

リファクタリングRuby: サブクラスをレジストリに置き換える(翻訳)

RubyのModule Builderパターン #3 Rails ActiveModelでの適用例(翻訳)

$
0
0

元記事が非常に長いので次のように分割しました。

概要

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

RubyKaigi 2017@広島でも発表されたshioyamaさんです。このときのセッションでもModule Builderパターンを扱っています。

本記事の図はすべて元記事からの引用です。また、パターン名は英語で表記しました。

RubyのModule Builderパターン #3 Rails ActiveModelでの適用例(翻訳)

Module Builderとカプセル化

ここまでに学んだすべてを投入して、Rubyで最も有名な例のアプリフレームワークのコアで実際に動くいくつかのコードに適用してみることにしましょう。

直前のセクションの議論に続いて、ここではモジュールを使った典型的なステート設定方法(ステートをモジュールに保存する)と、この方法でモジュールを設定する場合の問題点に絞り込みたいと思います。私は、Module Builderならステートを本来あるべき場所、つまりモジュールそれ自身にずっと自然な方法でカプセル化できることを皆さんにご理解いただければと願っています。

ActiveModel::AttributeMethods

今回チェックするモジュールは、ActiveModelのAttributeMethodsです。このモジュールは、プレフィックスやサフィックスをサポートするattribute用メソッドをRailsに追加します1。Railsを少しでも扱ったことがあれば、ActiveModel::Dirtyname_changed?などの変更追跡系メソッドで、このようにプレフィックスやサフィックスを使うパターンに既に慣れ親しんでいることでしょう。このモジュールは、前述のAdderIncluderのようなモジュールブートストラップと、MethodFound::Interceptorのようなmethod_missingによるメッセージインターセプトの両方を実装しています。

メソッドのプレフィックスとアフィックス(訳注: affix: プレフィックスとサフィックスを両方とも指定すること)を1つずつ実装する単純なクラスから始めることにしましょう2

class Person
  include ActiveModel::AttributeMethods

  attribute_method_affix  prefix: 'reset_', suffix: '_to_default!'
  attribute_method_prefix 'clear_'
  define_attribute_methods :name

  attr_accessor :name, :title

  def attributes
    { 'name' => @name, 'title' => @title }
  end

  private

  def clear_attribute(attr)
    send("#{attr}=", nil)
  end

  def reset_attribute_to_default!(attr)
    send("#{attr}=", "Default #{attr.capitalize}")
  end
end

上の属性メソッドは次のように使います。

person = Person.new
person.name = "foo"
person.name
#=> "foo"
person.clear_name
person.name
#=> nil
person.reset_name_to_default!
person.name
#=> "Default Name"

上のコードを見ると、渡されたクラスについてどのようなプレフィックス/サフィックス/アフィックスが定義されているかを知るために、ステートがモジュールのどこかに保存されていることがわかります。そしてこのステートはattribute_method_prefixのコードに保存されることもわかります。これはPerson内で呼び出されてメソッドのプレフィックスを設定します。

def attribute_method_prefix(*prefixes)
  self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix }
  undefine_attribute_methods
end

これによりプレフィックス文字列のarrayがAttributeMethodMatcherクラスのいくつかのインスタンスにmapされ、Personクラスのattribute_method_matchersのarrayに保存されます(アフィックスやサフィックスのメソッドも同様です)。これらのマッチャはモジュールのコア設定ですが、モジュール自身の外部に保存されます

これらのマッチャの中身はどうなっているのでしょうか。理解のために、このクラスの初期化メソッドを見てみましょう。

def initialize(options = {})
  @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
  @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
  @method_missing_target = "#{@prefix}attribute#{@suffix}"
  @method_name = "#{prefix}%s#{suffix}"
end

つまり、マッチャは本質的に2つの要素「プレフィックスとサフィックスのペアと、そこから生成される正規表現」でできています(それ以外はほとんどが利便性のためのものです)。この内部ステートは、2つの異なる(しかし関連する)目的に用いられます。

2つの目的のうち1つは重要であるにもかかわらず、満足なドキュメントがまったくありません(このインラインコメントをご覧ください)。第1の目的とは、あるattributesメソッドから返されるキー/値ペアのハッシュ1つを、定義済みのプレフィックス/サフィックス/アフィックスメソッドをすべてサポートするファーストクラス(first-class)属性メソッドに変換する手段を提供することです。上の例では、属性はキーnametitleを含むハッシュを1つ返し、これがclear_attributereset_attribute_to_default!などのメソッドに割り当てられる属性になります。

MethodFoundの場合と同様に、これもmethod_missingを用いて実装されています。マッチャーの正規表現とマッチし、かつ正規表現でキャプチャされた対象がattributesのキーに含まれているメソッド呼び出しは、すべてインターセプトされて属性のハンドラに割り当てられます。このようにしてreset_title_to_default!reset_attribute_to_default!に割り当てられ、属性の名前(title)がその引数になります。

メソッドのインターセプトは属性のハッシュを制約なしに変更できる点では素晴らしいのですが、method_missingはシロップのように遅いのです。どんな属性が欲しいかが事前にわかっているのであれば、一般にはメソッドを明示的に定義する方がよいでしょう。

これがメソッドマッチャの第2の目的です。プレフィックス/サフィックス/アフィックスを設定(属性ごとにプレフィックスやサフィックスのペアを定義する)した後にdefine_attribute_methodsに1つ以上の属性を渡して呼び出すことでマッチャがトリガされます。Personnameの属性を定義しているので、clear_namereset_name_to_default!はそれぞれclear_attributereset_attribute_to_default!に割り当てられます。

これらのメソッドは、Personクラスに追加される別のメソッド(generated_attribute_methods)で定義およびincludeされる1個の無名モジュールに紐付けられます。これらはインスタンスメソッドとしてメソッドインターセプトより優先されるので、clear_titleclear_nameは両方とも同じ結果を返しますが、前者はmethod_missingにフォールスルーし、後者はこの属性メソッドで扱われます(後者の方がずっと高速です)。

ActiveModelのAttributeMethods

ActiveModelのAttributeMethods

次のようにPersonインスタンスのメソッドをgrepすれば、定義済みの属性メソッドを一覧できます。

person = Person.new
person.methods.grep /name/
#=> [:name, :name=, :reset_name_to_default!, :clear_name, ...]

モジュールのancestorsinstance_methodsを調べてみると、生成されたメソッドが(他の場所ではなく)このモジュールで定義されていることも確認できます。

Person.ancestors
#=> [Person, #<Module:Ox...>, ActiveModel::AttributeMethods, ...]
Person.ancestors[1].instance_methods
#=> [:name, :name=, :reset_name_to_default!, :clear_name]

また、前述したmethod_missingのオーバーライドがActiveModel::AttributeMethodsこの箇所、クラス階層における無名モジュールの直後で行われていることもわかります。

属性メソッドの2つの実装(method_missingによるメソッドインターセプトと、メソッド定義による実装)は、それぞれ適切なアクセスパターンが異なる(前者は大規模な属性セットに、後者は小規模な固定の属性セットに向いています)ので、互いに補完し合います。私のMobility gemでは、i18nアクセサの定義でこれと同じアプローチを実際に採用しています3。RailsのAttributeMethodsはクラスメソッドや変数を多数追加しますが、私のMobilityは何ひとつ追加しません。代わりに、すべてのステートはそれらモジュール内に隠蔽されます。

いよいよAttributeMethodsのもうひとつの実装をお見せしましょう。この実装は私のMobility gemと同様、Module Builderを用いてステートをモジュール内にカプセル化します。

MethodFound::AttributeMethods

このセクションでは、これまで議論してきたすべてのアイデアを集約して現実のアプリに適用します。コードの詳細を解説する前に、クラスに属性メソッドを実装する要素をおさらいしておきましょう。

  1. クラス変数(attribute_method_matchers): プレフィックス/サフィックス/アフィックスのマッチャーのarrayを保持する
  2. method_missingのオーバーライド: 一致する属性マッチャにメソッド呼び出しを割り当て、対応するハンドラにattributesハッシュを割り当てる
  3. includeされた無名モジュールgenerated_attribute_methods): 定義済みの属性メソッドを保持する

今度は、上の各コア要素が別の場所に置かれているところを考えてみましょう。

  • メソッドマッチャは、includeする側のクラスで定義されている
  • 生成された属性メソッドは、このクラスにincludeされている無名モジュールで定義されている
  • method_missingAttributeMethods自身に定義され、そのクラスにもincludeされている

これで、私たちの実装で使われる3つの要素が揃いました。3つの要素はメソッドマッチャのクラス変数を通じて互いに結合し、システム実装のさまざまな部分に広がります。しかしそれだけではありません。メソッドマッチャの機能と生成されたメソッドの機能は(この後でも説明するように)本質的に独立していますが、同じ場所(メソッドマッチャはarrayに、生成されたメソッドは無名モジュールに)に保存されます。

ステートを別の方法で分散する、別の実装を考えます。この実装は、Module Builderを用いて以下の要素を単一のモジュールにまとめます。

  • 単独のプレフィックス/サフィックスペア: ここからマッチャの正規表現が生成される
  • 単独のmethod_missingオーバーライド: メソッドを正規表現でインターセプトする
  • 単独のメソッド: 渡されたプレフィックス/サフィックスペアに対応する属性メソッドを定義する

このModule BuilderにはAttributeInterceptorという名前が付けられており、MethodFoundにあります。これは、前のセクションでお見せしたMethodFound::Interceptorを継承し、形式の制約がないデフォルトのインターセプタビルダではなく、プレフィックスやサフィックスを受け取れるようにイニシャライザをカスタマイズします。コードのサイズはそれほど大きくありませんが、ここではコードをまるごと引用するのはやめておき、代わりにクラスで実際に行われている内容に注目します。

MethodFoundインターセプタを持つAttributeMethodsの実装

MethodFoundインターセプタを持つAttributeMethodsの実装

まずはインターセプトを見てみましょう。上のPersonクラスのdefine_attribute_prefix呼び出しやdefine_attribute_affix呼び出しを以下のように属性インターセプタに置き換えることでインターセプトを再現できます。

class Person
  include MethodFound::AttributeInterceptor.new(prefix: 'clear_')
  include MethodFound::AttributeInterceptor.new(prefix: 'reset_', suffix: '_to_default!')

  attr_accessor :name, :title

  def attributes
    { 'name' => @name, 'title' => @title }
  end

  # ...
end

ここでは2つの属性インターセプタをそれぞれインスタンス化してクラスにincludeしています。アフィックスのための特別な対応は不要なので、単にプレフィックス用とサフィックス用のインターセプタをビルドしています。

これらのモジュールがincludeされると、Personは外部からはActiveModelモジュールを用いた場合と完全に同一に振る舞っているように見えます。しかし内部の実装はかなり異なっており、ancestorsでその違いを確認できます。

Person.ancestors
#=> [Person,
 #<MethodFound::AttributeInterceptor: /\A(?:reset_)(.*)(?:_to_default!)\z/>,
 #<MethodFound::AttributeInterceptor: /\A(?:clear_)(.*)(?:)\z/>,
 ...  ]

前述のMethodFoundインターセプタと同様に、2つのモジュールがそれぞれmethod_missingをオーバーライドし、メソッド名が(モジュールに保存されている)正規表現と一致すればコードパスを分岐します。

このようにして得られる分散/ネスト条件セットは、ActiveModelのこの行と同等です。このコードは、一致をチェックする属性メソッドマッチャを列挙します。

matchers.map { |method| method.match(method_name) }.compact

2つの実装の主要な相違点は、この行がActiveModel::AttributeMethods(これはモジュールです)で実行され、かつPerson(これはクラスです)に保存されたクラス変数を用いるのに対し、私のMethodFoundバージョンではsuperを用い、メソッドのコンポジションを通じて、独立した各モジュールで実行されるという点です。

生成された属性メソッドの実装も同じ要領です。AttributeInterceptorにはdefine_attribute_methodsというメソッドがあり、これは1つ以上の属性名を受け取って、各属性メソッドにモジュールのプレフィックスやサフィックスを加えたものをそのモジュール自身に定義します。繰り返しになりますが、このモジュールには自身のプレフィックスやサフィックスが含まれているので、この作業に必要な情報はすべてモジュール内に揃っています。

したがって、機能は真の意味でカプセル化されます。単一のモジュールが自身のプレフィックスやサフィックスを含み、属性メソッドの呼び出しをキャッチするmethod_missingのオーバーライドや、自身の属性メソッドを生成するためのメソッドも含んでいます。4

このModule Builderを使えば、クラスメソッドやクラス変数をひとつも書かずにActiveModelの実装を以下のように再現できます。

class Person
  [ MethodFound::AttributeInterceptor.new(prefix: 'clear_'),
    MethodFound::AttributeInterceptor.new(prefix: 'reset_', suffix: '_to_default!')
  ].each do |mod|
    mod.define_attribute_methods(:name)
    include mod
  end

  #...
end

MethodFoundMethodFound::AttributeMethodsという別のモジュールをincludeしています。このモジュールは上のコードをシンプルにするクラスメソッド群を追加するので、ActiveModel::AttributeMethodsをまるごと置き換えることができます。このモジュールに含まれるdefine_attribute_methodsの実装も興味深いものです。

def define_attribute_methods(*attr_names)
  ancestors.each do |ancestor|
    ancestor.define_attribute_methods(*attr_names) if ancestor.is_a?(AttributeInterceptor)
  end
end

このクラスはマッチャを自分のステートに保存しないので、このモジュールではマッチャを単に列挙してメソッドを定義するという手が使えません。代わりに、自身のancestorsを通じて列挙します。列挙されるのはプレフィックスやサフィックスのペアを持つ各モジュールインスタンスなので、先祖が属性インターセプタの場合にdefine_attribute_methodsメソッドを呼び出します。これによってインターセプタモジュールごとに属性メソッドが生成され、クラスのインスタンスから呼び出せるようになります。

こうして得られた実装では、前述の関連する要素が独立したモジュールにカプセル化されて、しかもクラス変数やクラスメソッドで結合されていません。このようにカプセル化できるということは、まったく新しい属性インターセプタを設計して従来と同じ方法でincludeできるようになったということです。(属性メソッド生成のための)インターフェイスが同じである限り、内部が完全に変わってしまっても変更は不要です。

これは私にとって「モジュールとはそもそもどうあるべきか?」という問いかけに思えます。モジュールは、必要なものをすべて含み、必要な一部の機能のある側面だけを実行できる、独立した交換可能な単位であるべきです。しかしそれだけではなく、こうしたモジュールを構築するインターフェイスは、Ruby自身のオブジェクトモデル(言語そのものと同じぐらい長い歴史を持つモデル)から直接逸脱してしまいます。このような飾りが長年に渡ってRubyの衣装の袖口に縫い付けられていたにもかかわらず、その存在はほぼ誰にも気づかれずじまいだったのです

Module Builderパターンという名前について

私がご紹介した「Module Builderパターン」について、こんなのはRubyの単なるサブクラスで、そのクラスがたまたまModuleクラスだったというだけじゃないか、何を大げさな、とお思いの方もいらっしゃるかもしれません。技術的な観点からは、おっしゃるとおりでしょう。私はこのアイデアの第一発見者でもなければ、最初に記述したわけでもありません。単にFoo < Moduleしてみたらできたということです。

しかし、プログラミング言語を読むのも書くのも人間なのですから、名前は人間にとって大きな意味があります。もし仮にこのメソッドを持つモジュールがancestorsのリストに埋もれたまま見過ごされていたとしたら誰も気づかなかったでしょうし、どこかのブロガーがサブクラス化について書いたことをすっかり忘れてしまったら、この手法を使うこともないでしょう。このパターンがこれまでほぼ誰にも気づかれることなく存在し続けていたという事実そのものが、その証しです。

だからこそ、私はこれにキャッチーな名前を付けたのです。

私もRubyistとして、このパターンは私たちに必要だと考えます。ある巨大なRubyプロジェクトのコードを手にとって、モジュールが現場でどのように用いられ(かつ誤用され)ているかを考えてみてください。モジュールがトリガする多数のコールバックはありとあらゆる余分なステートをクラスにブートストラップし、クラスがincludeするモジュールをがっちりと結合してしまいます。モジュールは自己完結すべきですが、実行時にモジュールを設定できないことを言い訳に、いろんな場所に設定をばらまくことが正当化されてしまっています。

Rubyはこうしたことをもっとうまく扱えるのですから、私たちももっとうまくやれるはずです。だからこそ、肥大化したモジュールに、アプリの隅々にまでおぞましい触手を伸ばすタコのようなモジュールに今一度目を向け、自由になるチャンスを与えてやってください。それがモジュールのためにも、ひいては皆さまのためにも良いことであると信じています。

バックナンバー

関連記事

[保存版]人間が読んで理解できるデザインパターン解説#1: 作成系(翻訳)

[保存版]人間が読んで理解できるデザインパターン解説#2: 構造系(翻訳)

Rubyで学ぶデザインパターンを社内勉強会で絶賛?連載中


  1. 実際には、ActiveRecordのモデルにはActiveRecord::AttributeMethodsincludeされています。これ自身がActiveModel::AttributeMethodsincludeして、本記事で扱うメソッドの一部(特にdefine_attribute_methods)をオーバーライドしています。2つのモジュールの関連や、永続化(persisted)や、非永続化の属性メソッドの扱いはある意味で複雑なのですが、ここで議論されているアイデアはこの両方に関連しています。 
  2. 以後の説明では、標準的なドキュメントでは指摘されていない部分をあえて強調するために、文中でクラス例から引用するコードを少し変えてありますのでご了承ください。 
  3. Mobility gemではFallthroughAccessorsLocaleAccessorsというModule Builderで2つのケースを取り扱っています。私はこれらもMobilityからI18nAccessorsというgemに切り出しました。 
  4. 実際にはActiveModel::AttributeMethodsと同様、属性をエイリアスするalias_attributeも含まれています。 

Rails: 日付や時刻のカラム名を命名規則に合わせよう(翻訳)

$
0
0

概要

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

Rails: 日付や時刻のカラム名を命名規則に合わせよう(翻訳)

Railsでは、ActiveRecordモデルで使われるupdated_atcreated_atというマネージドのタイムスタンプがデフォルトで使えます。

しかし、さまざまなアプリのschema.rbやマイグレーションを調べてみると、モデルで何とか_dateのようなフィールド名をよく見かけます。

次のように書かないこと

データベースカラム名にdatetimeという語を含める:

class NaughtyMigration < ActiveRecord::Migration[5.1]
  add_column :users, :logged_in_date, :datetime
  add_column :users, :logged_out_time, :date
end

訳注: :datetime:dateは誤りを示すためにわざと入れ替えてあるそうです。

次のように書くこと

時刻にはat、日付にはonというサフィックスを追加する:

class AwesomeMigration < ActiveRecord::Migration[5.1]
  add_column :users, :logged_in_at, :datetime
  add_column :users, :logged_out_on, :date
end

理由

変数名にtimedateという語を追加するのは冗長であり、コードがうるさく見えます。文字列変数にfirst_name_stringという名前を付けないのと同じです。

Railsの慣習では、due_onのように書くことで日付を期待していることが伝わります。このように、データベースに保存されているデータを扱うコードを読む人に、どのようなデータが期待されているかという意図を即座に伝えることができます。

私は、コードが読みやすくなるのであれば、_untilのように書くこともあります。

個人的には、命名の制約がある分適切な名前を考えるのが面倒ではありますが。

関連記事

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

Railsのsecret_key_baseを理解する(翻訳)

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

Railsのトランザクションと原子性のバグ(翻訳)

$
0
0

注記: 本記事のコード例ではhas_and_belongs_to_many関連付け(通称HABTM)が使われていますが、Railsでこれを使ったリレーションは悪手とされています。現在のRailsでは代わりにhas_many :through関連付けを使うのが一般的です。

概要

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

Railsのトランザクションと原子性のバグ(翻訳)

Ruby on Railsのトランザクションは、熟練Rails開発者もつまづくことがあるほど扱いが微妙です。次のいくつかの例では、単純なトランザクションすら意図と違う動きをする可能性があることと、それによって気づかないうちに原子性(訳注: atomicity、不可分性とも)が損なわれることを示します。

設定

本記事では、SurveyとQuestionというモデルを持つシンプルなアプリを使って調べます。Surveyには名前が1つ必要で、Questionには何らかのテキストが必要だとします。例では、SurveyとQuestionの間には多対多(has_and_belongs_to_many)のリレーションが設定されています。

rails new sample
cd sample
rails generate model survey name:string
rails generate model question text:string
rails generate migration CreateJoinTableSurveyQuestion survey question
rake db:create
rake db:migrate
  • app/models/survey.rb
class Survey < ApplicationRecord
  validates :name, presence: true
  has_and_belongs_to_many :questions
  accepts_nested_attributes_for :questions
end
  • app/models/question.rb
class Question < ApplicationRecord
  validates :text, presence: true
  has_and_belongs_to_many :questions
end

コード例A

has_manyhas_and_belongs_to_many関連付けによって提供されるヘルパーメソッドは興味深い挙動を示します。次のスニペットをご覧ください。

survey = Survey.create(name: "Shapes")
question = Question.create(text: "九角形の辺はいくつあるか?")

survey.attributes = { name: "", question_ids: [question.id] }
survey.save
BEGIN
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
COMMIT
BEGIN
ROLLBACK

上のとおり、attributesquestion_ids=(またはquestions=)の代入が行われると、バリデーションの外部でCOMMITされるINSERT文がただちに実行され、その後ROLLBACKします。

以下のようにattributes=saveupdateに差し替えると、期待どおりに原子性が保たれます。updateは内部でwith_transaction_returning_statusのすべての変更をラップしています(with_transaction_returning_statusは、トランザクションにラップされたブロックを1つ取るメソッドで、ブロックが「真らしい」と評価された場合はCOMMITを実行し、ブロックが「偽らしい」と評価された場合はROLLBACKします)。

survey = Survey.create(name: "Shapes")
question = Question.create(text: "九角形の辺はいくつあるか?")

survey.update({ name: "", question_ids: [question.id] })
BEGIN
SELECT "questions".* FROM "questions" WHERE "questions"."id" IN (...)
SELECT "questions".* FROM "questions" INNER JOIN "questions_surveys" ON "questions"."id" = "questions_surveys"."question_id" WHERE "questions_surveys"."survey_id" = ...
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
ROLLBACK

次のように書くこともできます。

survey = Survey.create(name: "Shapes")
question = Question.create(text: "九角形の辺はいくつあるか?")

survey.with_transaction_returning_status do
  survey.attributes = { name: "", question_ids: [question.id] }
  survey.save
end
BEGIN
SELECT "questions".* FROM "questions" WHERE "questions"."id" IN (...)
SELECT "questions".* FROM "questions" INNER JOIN "questions_surveys" ON "questions"."id" = "questions_surveys"."question_id" WHERE "questions_surveys"."survey_id" = ...
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
ROLLBACK

コード例B

transactionメソッドは期待どおりに動作しないことがあります。次のスニペットをご覧ください。

survey = Survey.create(name: "Numbers")
question = Question.create(text: "プランク定数の値はいくつか?")

Survey.transaction do
  survey.update({ name: "", question_ids: [question.id] })
end
BEGIN
SELECT "questions".* FROM "questions" WHERE "questions"."id" = ...
SELECT "questions".* FROM "questions" INNER JOIN "questions_surveys" ON "questions"."id" = "questions_surveys"."question_id" WHERE "questions_surveys"."survey_id" = ...
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
COMMIT

興味深いことに、コード例Aの修正方法はここでは効きません。デフォルトでネストするトランザクションでは、親のトランザクションだけが使われます。

以下のようにActiveRecord::Rollback例外をraiseするか、親のトランザクションでjoinable: falseを指定することで、半端な変更が保存されないようになります。親のトランザクションでjoinable: falseを指定すると、多くのリレーショナルデータベースが備えるメカニズムとしての保存ポイントが内部で使われます。

survey = Survey.create(name: "Numbers")
question = Question.create(text: "プランク定数の値はいくつか?")

Survey.transaction do
  unless survey.update({ name: "", question_ids: [question.id] })
    raise ActiveRecord::Rollback
  end
end
BEGIN
SELECT "questions".* FROM "questions" WHERE "questions"."id" = ...
SELECT "questions".* FROM "questions" INNER JOIN "questions_surveys" ON "questions"."id" = "questions_surveys"."question_id" WHERE "questions_surveys"."survey_id" = ...
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
ROLLBACK

次のように書くこともできます。

survey = Survey.create(name: "Numbers")
question = Question.create(text: "プランク定数の値はいくつか?")

Survey.transaction(joinable: false) do
  survey.update({ name: "", question_ids: [question.id] })
end
BEGIN
SAVEPOINT active_record_...
SELECT "questions".* FROM "questions" WHERE "questions"."id" = ...
SELECT "questions".* FROM "questions" INNER JOIN "questions_surveys" ON "questions"."id" = "questions_surveys"."question_id" WHERE "questions_surveys"."survey_id" = ...
INSERT INTO "questions_surveys" ("survey_id", "question_id") VALUES (..., ...)
ROLLBACK TO SAVEPOINT active_record_...
COMMIT

まとめ

上の例から、いくつかの規則が得られます。

  1. 代入可能な関連付けを扱うときは、attributes APIをじかに使うことを避け、updatecreateを使うこと。
  2. (トランザクションの)ネストを扱う場合は、ROLLBACKを伝搬させる例外を使うか、親トランザクションをJOINできないようにすること。

関連記事

Ruby on Rails のhas_many 関連付けのフィルタテクニック4種(翻訳)

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

Rails: JOINすべきかどうか、それが問題だ — #includesの振舞いを理解する(翻訳)

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

$
0
0

概要

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

原文タイトルは、よくあるヨーダのセリフのもじりです。

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

Object#tryは、Railsアプリでnil値を扱う可能性がある場合をカバーするときや、与えられたメソッドがオブジェクトで必ずしも実装されていないといった場合に柔軟なインターフェイスを提供するときに、かなりよく使われています。#tryのおかげでNoMethodErrorを回避できるのですから、これで問題はなくなったように見えます。NoMethodError例外が起きなくなったからといって、本当に問題がなくなったと言えるのでしょうか?

実際にはそうとは言えません。Object#tryにはいくつかの重大な問題がつきまといますし、たいていの場合もっとよいソリューションをかなり簡単に実装できるのです。

Object#tryのしくみ

Object#tryの基本となるアイデアはシンプルです。nilオブジェクトにメソッド呼び出しを行った場合や、そのオブジェクトにメソッドが実装されていないnilでないオブジェクトにメソッド呼び出しを行った場合に、NoMethodError例外をraiseせず、単にnilを返すというものです。

たとえば、最初のユーザーのメールアドレスを取りたいとします。ユーザーが1人もいない場合に失敗しないようにするために、次のように書くことができます。

user.first.try(:email)

このとき、さまざまな種類のオブジェクトを渡せる一般的なサービスを実装していたとしたらどうでしょう。たとえば、オブジェクトを保存した後、オブジェクトがたまたまそのための正しいメソッドを実装した場合に通知を送信するにはどうしたらよいでしょうか。Object#tryを使うと次のように書けます。

class MyService
  def call(object)
    object.save!
    object.try(:send_success_notification, "MyServiceから保存されました")
  end
end

コードを見れば、このメソッドに引数を与えられることもわかります。

途中でnilを受け取る可能性のあるステップごとに、何らかのメソッドをチェインする必要があるときにはどうすればよいでしょうか。ご心配なく、Object#tryでできます。

payment.client.try(:addresses).try(:first).try(:country).try(:name)

Object#tryの何が問題なのか

一見するとObject#tryはさまざまなケースを扱えそうですが、使うとどんな問題が起きるのでしょうか。

その答えは「たくさん起きる」です。多くの場合、Object#tryの最大の問題は、問題を完全に握りつぶしてしまうことであり、nilが問題である場合にも問題を「解決」してしまいます。他にも、Object#tryを使うと意図がはっきりしなくなる点も挙げられます。次のコードが何をしようとしているかおわかりでしょうか。

payment.client.try(:address)

見た目どおり、支払い(payment)にクライアントがいない場合にnilになるケースを扱っているのでしょうか。それとも、単にクライアントがたまたまnilになったときにNoMethodErrorで失敗したくないから「念のため」使っているだけなのでしょうか。もっと悪い場合を考えると、クライアントがたまたまポリモーフィック関連付けになっていて、しかもaddressesメソッドがモデルに実装されているとは限らないとしたらどうでしょう。あるいは、データ完全性にある問題が生じ、何らかの理由でクライアントが削除された支払いがいくつか発生してしまい、しかも残っていないとしたらどうでしょう。

Object#tryの使いみちの可能性があまりに多いため、上のコードを見ただけでは真の意図を知ることは不可能です。

幸いなことに、Object#tryを取り除いて明確で表現力の高いコードにできる別のさまざまなソリューションがあります。そうしたソリューションを使えば、コードのメンテナンス性と読みやすさがより高まり、バグが発生しにくくなり、二度と意図があいまいにならないようにできます。

Object#tryを使わないソリューション

Object#tryの利用状況に応じた「パターン」をいくつかご紹介します。

1. デメテルの法則を尊重する

デメテルの法則は、構造的な結合を回避するのに便利な規則です(個人的には「法則」というほどではない気がします)。要するに、仮想のオブジェクトAは自分に直接関連することにのみ関心を持つべきであり、協同または関連する相手の内部構造に立ち入るべきではないという規則です。この規則は「メソッド呼び出しのドット.は1つだけにする」と解釈されることが多いのですが、デメテルの法則は本来ドットの数の規則ではなく、オブジェクト間の結合についての規則なので、操作や変換のチェインについてはまったく問題にはなりません。たとえば次の例は法則に違反しません。

input.to_s.strip.split(" ").map(&:capitalize).join(" ")

しかし次の例は違反です。

payment.client.address

デメテルの法則を尊重することで、多くの場合明確でメンテナンス性の高いコードを得ることができます。法則に違反する十分な理由がない限り、法則を守って密結合を回避するようにすべきです。

先のpayment/client/addressを使ったコード例に戻ります。次のコードはどのようにリファクタリングできるでしょうか。

payment.client.try(:address)

最初に行うのは、構造的な結合を減らしてPayment#client_addressメソッドを実装することです。

class Payment
  def client_address
    client.try(:address)
  end
end

これでさっきよりずっとよくなりました。payment.client.try(:address)で無理やりaddressを参照するのではなく、payment.client_addressを実行するだけで済みます。Object#tryが1箇所だけになったので既に1つ改善されました。リファクタリングを続けましょう。

ここから先は2つの選択肢があります。clientがnilになるのが正当か、そうでないかです。clientがnilになるのが正しいのであれば、自信を持って明示的にnilを早期に返します(訳注: いわゆるguard構文です)。こうすることで、clientが1つもないのは有効なユースケースであることがはっきりします。

class Payment
  def client_address
    return nil if client.nil?

    client.address
  end
end

clientは決してnilになってはならないのであれば、先のguard構文をスキップできます。

class Payment
  def client_address
    client.address
  end
end

このような委譲はかなり一般的に行われます。Railsではこういう場合にうまい解決法があるでしょうか?答えは「イエス」です。ActiveSupportは、まさにこういう場合にうってつけのActiveSupport#delegateマクロを提供しています。このマクロを使えば、nilをさっきとまったく同じように扱える委譲を定義できます。

最初の例では、nilになってもよいユースケースを次のように書き換えます。

class Payment
  delegate :address, to: :client, prefix: true, allow_nil: true
end

nilになってはならない場合は次のように書き換えます。

class Payment
  delegate :address, to: :client, prefix: true
end

これで先ほどよりもずっとコードが明確になり、結合も弱まりました。Object#tryをまったく使わずにエレガントに解決するという目的を達成できたのです。

しかし、他の場所ではpaymentにclientがないことを予測しきれていない可能性がまだ残されています(完了していないトランザクションのpaymentなど)。たとえば、トランザクションが完了したpaymentのデータを表示するときになぜかNoMethodError例外でつまづいてしまうことがあります。このような場合に、必ずしもdelegateマクロでallow_nil: trueオプションが必要になるとは限りません。もちろん、Object#tryを使わなければならないということでもありません。この場合の解決法はいくつか考えられます。

2. スコープ付きデータを操作する

完了したトランザクションのpaymentを扱うときにclientが存在することを保証するなら、単に正しいデータセットを扱えるようにするのが手です。Railsアプリではこういう場合に、PaymentコレクションにActiveRecordの何らかのスコープ(with_completed_transactionsなど)を適用します。

Payment.with_completed_transactions.find_each do |payment|
  do_something_with_address(payment.client_address)
end

完了していないトランザクションのpaymentでclientのaddressを使って何かする計画はまったくないので、ここでnilを明示的に取り扱う必要はありません。

にもかかわらず、paymentの作成にclientが常に必須になっているとしても、このコードでNoMethodErrorが発生する可能性は残されています(関連付けられたclientレコードが誤って削除されてしまった場合など)。この場合は修正が必要になるでしょう。

3. データ完全性

特にPostgreSQLなどのRDBMSを使っている場合、データの完全性を確実にする方法はかなりシンプルです。ここで押さえておくべきは、テーブルの新規作成時に適切な制約を追加することです。これはデータベースレベルで行う必要があることを忘れてはなりません。モデルでのバリデーションは簡単にバイパスされてしまうことがあるため、まったく不十分です。clientnilになってしまう問題を回避するには、paymentsテーブル作成時にNOT NULL制約とFOREIGN KEY制約を追加して、clientがまったく割り当てられない状況や、関連付けが一部のpaymentに残っているclientレコードが削除されるような状況を防ぐべきです。

create_table :payments do |t|
  t.references :client, index: true, foreign_key: true, null: false
end

以上で制約の追加はオシマイです。制約の追加を忘れないようにすることで、nilで起きる予想外のユースケースの多くを回避できます。

4. 明示的な変換で型を確定する

私は次のようなかなり風変わりなObject#tryの使い方を何度か目にしたことがあります。

params[:name].try(:upcase)

このコードから、params内のnameキーから何らかの文字列が取り出せることを期待しているのが読み取れます。それならto_sメソッドで明示的に変換することで文字列型を確定させればよいのではないでしょうか。

params[:name].to_s.upcase

これで意図がわかりやすくなりました。

ただし、上の2つのコードは同等ではありません。前者はparams[:name]が文字列であれば文字列を返しますが、nilの場合にはnilを返します。後者は常に文字列を返します。場合によってnilが戻ることが期待されるかどうかは元のコードからははっきりしないので(これはObject#tryのあからさまな問題ですが)、ここでは2つの選択肢が考えられます。

  • params[:name]nilならnilを返すことが期待される場合: 文字列の代わりにnilを扱うのはかなり面倒になるので、あまりよいアイデアとはいえませんが、nilを扱う必然性がどうしても生じることもあるかもしれません。そのような場合はguard構文を追加してparams[:name]nilになる可能性があることを明示的に示す方法が考えられます。
return if params[:name].nil?

params[:name].to_s.upcase
  • 文字列を返すことが期待される場合: この場合はguard構文は不要です。先の明示的な変換をそのまま使いましょう。
params[:name].to_s.upcase

もっと複雑な状況では、Form Objectを使うか、dry-rbなどのもっと安全な型管理を導入する(あるいは両方)のがよいかもしれません。ただしこれらは明示的な型変換と本質的に同じなので、設計を損なわない限りは有用だと思います。

5. 正しいメソッドを使う

ネストしたハッシュの取り扱いは、API開発やユーザー提供のペイロードを扱うときにかなりよく見かけるユースケースです。JSONAPI互換のAPIを扱っていて、更新時にclientの名前を取得したいとしましょう。この場合は次のようなペイロードが考えられます。

{
  data: {
    id: 1,
    type: "clients",
    attributes: {
      name: "some name"
    }
  }
}

しかしAPIのユーザーが提供するペイロードが正しいかどうかがどうしてもわからない場合、ペイロードの構造が正しくないという仮定が成り立つことがあります。

こういう場合の残念な対応方法といえば、もうおわかりですね。Object#tryです。

params[:data].try(:[], :attributes).try(:[], :name)

お世辞にも美しいとは言い難いコードです。しかし面白いことに、このコードは実に簡単にきれいに書き直すことができるのです。

1つの方法は、途中のステップに明示的な変換を適用することです。

params[:data].to_h[:attributes].to_h[:name]

さっきよりよくなりましたが、もう少し表現力が欲しいところです。理想的な方法は、こういう場合のための専用メソッドを使うことです。そうした専用メソッドはいくつかありますが、たとえばHash#fetchは、指定のキーがハッシュにない場合の値も指定できます。

params.fetch(:data).fetch(:attributes, {}).fetch(:name)

これでずっとよくなりましたが、ネストしたハッシュを掘ることにもう少し特化したメソッドがあればさらによいでしょう。幸いなことに、Ruby 2.3.0からまさにこのためのHash#digメソッドが使えるようになりました。このメソッドはネストしたハッシュをくまなくチェックし、中間のキーがない場合にも例外をraiseしません。

params.dig(:data, :attributes, :name)

6. 正しいインターフェイスかダックタイピングを使う

最初に使った、必要な場合に通知を送信する例に立ち戻ります。

class MyService
  def call(object)
    object.save!
    object.try(:send_success_notification, "saved from MyService")
  end
end

このコードの改善方法は2とおりあります。

  • サービスを2つ実装する: 1つのサービスは通知を送信し、もう1つは送信しません。
class MyServiceA
  def call(object)
    object.save!
  end
end

class MyServiceB
  def call(object)
    object.save!
    object.send_success_notification("saved from MyService")
  end
end

リファクタリングしたことでコードがずっと明確になり、Object#tryも取り除けました。しかし今度は、MyServiceAを使う必要があるオブジェクトの種類とMyServiceBを使う必要があるオブジェクトの種類を知る方法が必要になります。これはこれで理解できますが、別の問題となる可能性もあります。この場合は2番目の方法がよいでしょう。

  • ダックタイピングを使う: MyServiceに渡されるすべてのオブジェクトに単にsend_success_notificationメソッドを追加します。このメソッドは何もせず、メソッドの内容は空のままにします。
class MyService
  def call(object)
    object.save!
    object.send_success_notification("saved from MyService")
  end
end

この方法なら、オブジェクトで共通する振舞いを明示的に示せるので、そうした振舞いを認識しやすくなるというメリットも得られます。元のObject#tryには多数のドメイン概念が潜んでいるため、コードの意図がわかりにくくなるという問題があります。そうしたドメイン概念が存在しないのではなく、ちゃんと認識されていないということです。Object#tryを使うとドメイン(概念)も損なわれてしまいます。これもぜひ覚えておきたい重要なポイントです。

7.「Null Object」パターン

上の通知送信の例をもう一度使うことにします。モデルの形はある程度残しつつ、少し変更しました。メソッドの引数をmailerにし、それに対してsend_success_notificationを呼びます。

class MyService
  def call(object, mailer: SomeMailer)
    object.save!
    mailer.send_success_notification(object, "saved from MyService")
  end
end

これで、必要に応じていつでも通知を送信できるようになりました。さて、通知を送信したくないときはどうすればよいでしょうか。そんなときの残念な方法といえば、mailernilを渡してObject#tryを使うことです。

class MyService
  def call(object, mailer: SomeMailer)
    object.save!
    mailer.try(:send_success_notification, object, "saved from MyService")
  end
end

Service.new.call(object, mailer: nil)

ここまでお読みになった方は、この方法を使うべきでないことがおわかりいただけると思います。ありがたいことに、Null Objectパターンを適用すれば、何もしないsend_success_notificationメソッドを実装する何らかのNullMailerのインスタンスを渡すことができます。

class NullMailer
  def send_success_notification(*)
  end
end

class MyService
  def call(object, mailer: SomeMailer)
    object.save!
    mailer.send_success_notification(object, "saved from MyService")
  end
end


MyService.new.call(object, mailer: NullMailer.new)

これでObject#tryよりずっとよいコードになりました。

ぼっち演算子&.とは何か

Ruby 2.3.0で新しく導入された&.は「ぼっち演算子」や「safe navigation operator」などと呼ばれます(訳注: 以下ぼっち演算子で統一)。ぼっち演算子は一見Object#tryとよく似ていますが、Object#tryほどあいまいではありません。nil以外のオブジェクトに対してメソッド呼び出しを行い、かつオブジェクトにそのメソッドが実装されていない場合はNoMethodErrorがraiseされます(Object#tryはそうではありません)。次の例をご覧ください。

User.first.try(:unknown_method)  # `user`がnilであるとする
=> nil

User.first&.unknown_method
=> nil

User.first.try(:unknown_method!) # `user`はnilでないとする
=> nil

User.first&.unknown_method
=> NoMethodError: undefined method `unknown_method' for #<User:0x007fb10c0fd498>

ということは、ぼっち演算子なら安全に使えるからよいのでしょうか?そうでもありません。Object#tryの重大な問題が1つ減っただけで、他の問題はそのまま変わらないからです。

しかしながら、私はぼっち演算子を使ってもよいケースが1つあると考えています。次のコード例をご覧ください。

Comment.create!(
  content: content,
  author: current_user,
  group_id: current_user&.group_id,
)

ここでは、current_userに属するコメントを1つ作成したいと考えています。current_userは作者(author)になることがあり、current_userからgroup_idを代入しますが、このgroup_idnilになる可能性があるとします。

上のコードは次のように書き直せます。

Comment.create!(content: content, author: current_user) do |c|
  c.group_id = current_user&.group_id if current_user
end

次のように書き直すこともできます。

comment_params = {
  content: content,
  author: current_user,
}

comment_params[:group_id] = current_user.group_id if current_user

Comment.create!(comment_params)

しかし、書き直したコードが、元のぼっち演算子&.を使ったサンプルより読みやすくなったとは思えません。このように、意図があいまいになるのと引き換えに読みやすさを優先したい場合には、ぼっち演算子&.が有用なこともあります。

まとめ

私は次の理由から、Object#tryの有効なユースケースはひとつもないと信じています。Object#tryを使うと意図があいまいになってしまい、ドメインモデルに負の影響が生じます。Object#tryは問題を美しくない方法で「解決」してしまいますが、同じ問題をもっとスマートに解決できる方法が「デメテルの法則の尊重と委譲」から、「スコープが正しく設定されたデータを扱う」、「データベースに正しい制約を適用する」、「明示的な変換で型を確定させる」、「正しいメソッドを使う」、「ダックタイピングを利用する」「Null Objectパターン」に至るまで数多く存在するという単純な事実があります。ぼっち演算子&.すら、用途を限定すればずっと安全に使うことができます。

訳注: 本記事では言及されていませんが、!付きのObject#try!は実質ぼっち演算子と同じに使えます。
ぼっち演算子が#try!と少し異なるのは、引数付きだとnilのときに引数が評価されないという点です。
参考: Safe Navigation Operator で呼ばれるメソッドの引数はレシーバが nilなら評価されない

関連記事

Rubyスタイルガイドを読む: クラスとモジュール(2)クラス設計・アクセサ・ダックタイピングなど

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

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

$
0
0

概要

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

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

パフォーマンスに気を配っている開発者なら、#includes#preload#eager_loadなどの読み込みメソッドでN+1クエリを回避する方法をご存知でしょう。しかし自分の仕事が正しかったかどうか、期待する関連付けが本当にpreloadされているかどうかをチェックする方法はあるのでしょうか。そもそもどうやってテストすればよいのでしょうか。方法は2つあります。

Railsアプリに次の2つのクラスがあるとします。1つのorderは複数のorder line(order_lines)を持つことができます。

class Order < ActiveRecord::Base
  has_many :order_lines

  def self.last_ten
    limit(10).preload(:order_lines)
  end
end
class OrderLine < ActiveRecord::Base
  belongs_to :order
end

ここでOrder.last_tenというメソッドを実装しました。これはeager-loadingする関連付けを1つ使って、最新のorderを10件返します。このコードを呼び出した後でちゃんとpreloadされるかどうかを確認してみましょう。

1. association(:name).loaded?

require 'test_helper'

class OrderTest < ActiveSupport::TestCase
  test "#last_ten eager loading" do
    o = Order.new()
    o.order_lines.build
    o.order_lines.build
    o.save!

    orders = Order.last_ten
    assert orders[0].association(:order_lines).loaded?
  end
end

preload(:order_lines)を行ったので、order_linesが読み込まれているのかどうかを知りたいと思います。orders[0]などのOrderオブジェクトを1つ取得する必要があることをチェックするには、オブジェクトの照合を行います。ordersコレクションをチェックしても関連付けが読み込まれているかどうかはわからないため、コレクションのチェックは不要です。

RSpecでのテストは以下のような感じになります。

require 'rails_helper'

RSpec.describe Order, type: :model do
  specify "#last_ten eager loading" do
    o = Order.new()
    o.order_lines.build
    o.order_lines.build
    o.save!

    orders = Order.last_ten
    expect(orders[0].association(:order_lines).loaded?).to eq(true)
    # 次でもよい
    expect(orders[0].association(:order_lines)).to be_loaded
  end
end

2. ActiveSupport::Notificationsでクエリをカウントする

ActiveRecordライブラリにはassert_queriesという便利なヘルパーメソッドがあり、ActiveRecord::TestCaseに含まれているのですが、惜しいことに、ActiveRecord::TestCaseはActiveRecordに含まれていません。これはRailsの内部テストで振舞いをチェックする目的にのみ利用できます。しかし今回の目的に合わせてassert_queriesをエミュレートするのは意外に簡単です。

いくつかのActiveRecordオブジェクトのグラフを操作するが、オブジェクトを返さずに計算値だけを返すという状況を考えてみましょう。このときにN+1問題が発生していないことをどうやって確認すればよいでしょうか。副作用は見当たらず、loaded?かどうかをチェックできるレコードも返されません。何か方法はないものでしょうか。

class Order < ActiveRecord::Base
  has_many :order_lines

  def self.average_line_gross_price_today
    lines = where("created_at > ?", Time.current.beginning_of_day).
      preload(:order_lines).
      flat_map do |order|
        order.order_lines.map(&:gross_price)
      end
    lines.sum / lines.size
  end
end

class OrderLine < ActiveRecord::Base
  belongs_to :order

  def gross_price
    # ...
  end
end

上の状況で、Order.average_line_gross_price_todayがN+1クエリ問題を抱えていないかどうかをどのように確認すればよいでしょうか。order_lines?を読み取るときにorder.order_lines.map(&:gross_price)がSQLクエリをトリガしないことをどのように確認すればよいでしょうか(実はN+1問題が起きています)。

ActiveSupport::Notificationsを使えば、SQL文が実行されるたびに通知を受け取ることができます。

require 'rails_helper'

RSpec.describe Order, type: :model do
  specify "#average_line_gross_price_today eager loading" do
    o = Order.new()
    o.order_lines.build
    o.order_lines.build
    o.save!

    count = count_queries{ Order.average_line_gross_price_today }
    expect(count).to eq(2)
  end

  private

  def count_queries &block
    count = 0

    counter_f = ->(name, started, finished, unique_id, payload) {
      unless %w[ CACHE SCHEMA ].include?(payload[:name])
        count += 1
      end
    }

    ActiveSupport::Notifications.subscribed(
      counter_f,
      "sql.active_record",
      &block
    )

    count
  end
end

上のようにする場合、eager loadingの問題を検出するのに十分な数のレコードを作成しておいてください。order 1件とline 1件だけでは、eager loadingが発生するかどうかにかかわらずクエリの数が同じになってしまうので不十分です。今回はorderのlineが2の場合にのみ、preloadingでのクエリ数(2件、1つはすべてのorder、もう1つはすべてのline)とpreloadingされない場合のクエリ数(3件、1つはすべてのorder、残りは個別のline)に違いが生じることがわかります。修正する前にはテストが失敗することを必ず確認しましょう :)

この方法はもちろん有効ですが、責務を2つの小さなメソッドに分割できたらなおよいでしょう。責務の1つはデータベースから正しいレコードを抽出すること(IOが発生する)、もう1つはデータを変換して計算することです(IOも副作用もなし)。

この種のテストで役に立つRSpecマッチャーとして、db-query-matchers gemをチェックしてみてください。

もっと知りたい方に

本記事を気に入っていただけた方は、日々のRailsプログラミングに役立つ知識をいつも最初に知ることができる弊社のニュースレターをぜひご購読ください。コンテンツはRuby、Rails、Web開発、リファクタリングが中心ですが、その他の話題も扱っています。

大規模で複雑なRailsアプリを手がけている方は、弊社の最新刊『Domain-Driven Rails』もぜひチェックしてみてください。

関連記事

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)

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

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

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

$
0
0

こんにちは、hachi8833です。先週は文化の日でウォッチをお休みいたしました。

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

RubyWorld Conference 2017無事終了


2017.rubyworld-conf.orgより

今年も盛り上がったようです。皆様お疲れさまでした。


つっつきボイス: 「今年は残念ながら行けなかったんで、松江の馴染みのおでん屋食べられなかった(´・ω・`)」「今回はクックパッドの発表がいつもより多かったみたいでした」

Rails: 今週の改修

改良: beforeSendを付けずにRails.ajaxを呼び出せるようになった

// actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
-  unless options.beforeSend?(xhr, options)
+  if options.beforeSend? && !options.beforeSend(xhr, options)

// 変更前 Rails.ajax({ type: 'get', url: '/', beforeSend: function() { return true }, success: function() { // do something } // 変更後 Rails.ajax({ type: 'get', url: '/', success: function() { // do something }

つっつきボイス: 「以前は使わないときにもbeforeSend:書かないといけなかったのか」

改善: rescue画面でソースが改行されないようになった


つっつきボイス: 「行数表示と実際の行が一致するようになったのね」「エディタではワードラップする方が好きなんですが、英語圏だとエディタをワードラップしない人が割りと多い気がします」

Railsから非推奨コードを削除

以下の非推奨コードが削除されました。

  • erubis: 今後はerubiになります。
  • evented Redisアダプタ
  • ActiveRecordの以下を削除
    • #sanitize_conditions
    • #scope_chain
    • .error_on_ignored_order_or_limit設定
    • #verify!の引数
    • #indexesname引数
    • ActiveRecord::Migrator.schema_migrations_table_name
    • supports_primary_key?
    • supports_migrations?
    • initialize_schema_migrations_tableinitialize_internal_metadata_table
    • dirty recordへのlock!呼び出し
    • 関連付けでクラスに:class_nameを渡す機能
    • index_name_exists?default引数
    • ActiveRecordオブジェクトの型変換時のquoted_idサポート
  • ActiveSupportの以下を削除
    • halt_callback_chains_on_return_false
    • コールバック用文字列フィルタの:if:unlessオプション

改良: ActionDispatch::SystemTestCaseにフックを追加

y-yagiさんです。

# actionpack/lib/action_dispatch/system_test_case.rb
+
+    ActiveSupport.run_load_hooks(:action_dispatch_system_test_case, self)

#redirect_backallow_other_hostを追加

# actionpack/lib/action_controller/metal/redirecting.rb
-    def redirect_back(fallback_location:, **args)
-      if referer = request.headers["Referer"]
-        redirect_to referer, **args
      else
-        redirect_to fallback_location, **args
-      end
+    def redirect_back(fallback_location:, allow_other_host: true, **args)
+      referer = request.headers["Referer"]
+      redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
+      redirect_to redirect_to_referer ? referer : fallback_location, **args
     end

つっつきボイス:redirect_backはリファラを見て前画面に戻るやつですね」「前は他のサイトに戻れないよう設定できなかったのか」

Rails

dry-rbでRailsにForm Objectを作る

cucumbersome.netより

# 同記事より
PostcardSchema = Dry::Validation.Schema do
  required(:address).filled
  required(:city).filled
  required(:zip_code).filled
  required(:content).filled
  required(:country).filled
end

つっつきボイス: 「実は今、dry-rbシリーズにどんなgemがあるのかざっくりチェックする記事を準備しているんですが、そのきっかけは同じ作者の@solnicさん(Ruby Prize 2017受賞者)のvirtusのReadmeで以下の記述を見たことでした: よく見たらもう何年も更新されてなくて、最新Rubyでの動作も確認されてないみたいなので」

さらなる進化へ
Virtusを作ったことで、Rubyにおけるデータの扱い、中でもcoercion/型安全/バリデーションについて多くのことを学べました。このプロジェクトは成功を収めて多くの人々が役立ててくれましたが、私はさらによいものを作ることを決心しました。その結果、dry-typesdry-structdry-validationが誕生しました。これらのプロジェクトはVirtusの後継者と考えるべきであり、考慮点やより優れた機能がさらにうまく分割されています。Virtusが解決しようとした同種の現代的な問題について関心がおありの方は、ぜひdryシリーズのプロジェクトもチェックしてみてください。

@solnic
https://github.com/solnic/virtus より抄訳

「なるほど、virtusはとってもいいgemだけど今後はdry-rb系gemがメンテされそう: 次にFormObject作るときはこれ参考にしよう」
「dry-validationはActiveRecordのバリデーション+strong parametersより数倍速いと謳ってますね」「でも本家のバリデーションはRailsの記法にマッチしているからそっちを使いたい」

Service Objectにちょっとうんざり(Awesome Rubyより)


avdi.codesより

# 同記事より
module Perkolator
  def self.process_ipn(params:, redemption_url_template:, email_options:)
    # ...
  end
end

post "/ipn" do
  demand_basic_auth

  redemption_url_template =
    Addressable::Template.new("#{request.base_url}/redeem?token={token}")

  Perkolator.process_ipn(
    params: params,
    redemption_url_template: redemption_url_template,
    email_options: settings.email_options)

  # Report success
  [202, "Accepted"]
end

つっつきボイス: 「Service Objectにクラスメソッドを実装する的な話か」
「Service Objectのメソッドをインスタンスメソッドとして実装するかどうかはケースバイケースなんだけど、一度クラスメソッドとして実装してしまうと後からインスタンスメソッド化するのがものすごく面倒になるのが特に厄介」
「クラスメソッドにしてしまってマルチスレッドが効かなくなるとか、あるあるですな」
「自分の場合、形はクラスメソッドだけど内部でself.newして事実上インスタンスメソッドにすることある」

ほぼほぼすべてのプロジェクトで使う25のgem(RubyFlowより)


hackernoon.comより


つっつきボイス: 「これまでRailsウォッチでいろんなgemを取り上げてきたせいか、初めて見るgemがほとんどありませんでした: それだけ定番ってことですね」「こうやって何度も見ていると、もう見ただけで機能を思い出すようになってくる」
「そうそう、ところでmoney-rails#monetizeってメソッド、これが言いたくて作ったとしか思えない」「『誰がうまいこと言えと』的なやつですね」

class Product < ActiveRecord::Base

  monetize :price_cents

end

RailsでPostgreSQLのパーティショニングを使ってみる(RubyFlowより)


evilmartians.comより

# 同記事より
CREATE OR REPLACE FUNCTION orders_partitioned_view_insert_trigger_procedure() RETURNS TRIGGER AS $BODY$
  DECLARE
    partition TEXT;
    partition_country TEXT;
    partition_date TIMESTAMP;
  BEGIN
...

つっつきボイス: 「もうパーティショニングどうこうというより、PostgreSQLの機能がそもそも凄すぎてそっちに驚く」「『こんな機能があったのか』みたいなのが次から次に出てくるし」
「記事タイトルはCommand & Conquerのもじりかな」「あー、ゲームネタなのか(ゲーム音痴なので)」

⭐rails-sqli.org: RailsのSQLインジェクション手法を一覧できるサイト⭐


rails-sqli.orgより

今気づきましたが、Rails 5Rails 4Rails 3それぞれでSQLインジェクション手法をどっさり紹介しています。

# pluckを使ったインジェクション
params[:column] = "password FROM users--"
Order.pluck(params[:column])
Query
SELECT password FROM users-- FROM "orders"
Result
["Bobpass", "Jimpass", "Sarahpass", "Tinapass", "Tonypass", "supersecretpass"]

つっつきボイス: 「おお、これ( ・∀・)イイ!!: チェックに使える」「いろんなインジェクションがあるんだなー」「何にしろparamsに入力を直接突っ込む時点でアウトですな」「翻訳したい気もするけど翻訳するところがないw」「見ればわかりますからねー」

今週の⭐を進呈いたします。おめでとうございます。

barbeque: Dockerでジョブを実行する、クックパッド製ジョブキューgem

RubyWorld Conference 2017でも言及されていたようです。


つっつきボイス: 「クックパッドは多分ここ最近マイクロサービス化を進めているので、その中でバッチジョブもdocker containerの実行にすればより分散しやすくなるということかな」「Aaron Pattersonさんのインタビュー↓でもGitHubがマイクロサービス化を進めているという話がありましたね」「分散しないとRailsアプリ本体の起動が死ぬほど遅くなる」

[インタビュー] Aaron Patterson(前編): GitHubとRails、日本語学習、バーベキュー(翻訳)

RailsでのStripe.com決済処理をRSpecでテストする(RubyFlowより)


hackernoon.comより

# 同記事より
require 'rails_helper'
include ActiveJob::TestHelper
RSpec.describe Payments::InvoicePaymentSucceeded, type: :mailer do
  let(:plan) { @stripe_test_helper.create_plan(id: 'free', amount: 0) }
before(:each) do
    @admin = FactoryGirl.create(:user, email: 'awesome@dabomb.com')
    PaymentServices::Stripe::Subscription::CreationService.(
      user: @admin,
      account: @admin.account,
      plan: plan.id
    )
    @event = StripeMock.mock_webhook_event(
      'invoice.payment_succeeded',
      customer: @admin.account.subscription.stripe_customer_id,
      subscription: @admin.account.subscription.stripe_subscription_id
    )
  end
it 'job is created' do
    ActiveJob::Base.queue_adapter = :test
    expect do
      Payments::InvoicePaymentSucceeded.email(@event.id).deliver_later
    end.to have_enqueued_job.on_queue('mailers')
  end
it 'email is sent' do
    expect do
      perform_enqueued_jobs do
        Payments::InvoicePaymentSucceeded.email(@event.id).deliver_later
      end
    end.to change { ActionMailer::Base.deliveries.size }.by(1)
  end

つっつきボイス: 「Stripeは有名な決済サイトですね: こういうところにはテスト用のサンドボックス的環境が用意されているのが普通」
「そういえばPayPalのサンドボックスはよくできてるんですが、惜しいことにドキュメントがものすごく読みづらい」「私も以前試したけど結局よくわからんかった」


stripe.comより

Fat Free CRM: Railsで作られたOSSカスタマーリレーション管理(Awesome Rubyより)

www.fatfreecrm.comより


つっつきボイス: 「日本でこれをカスタマイズして使うのは考えにくいけど、ソースコードが結構きれいでよく書けている感じなので、実際に業務で動くアプリとしてソース読むとかテストコードの書き方や設計を学ぶのにいいかも」
「あまり大きくなさそうだし、git cloneしてIDEでじっくり読んでみようかな」「RedmineやGitLabだと巨大すぎて追うのが大変ですからね」「Rails 5.0.4か」

github.com/fatfreecrm/fat_free_crmより

flipper: 特定の機能を動的にオン/オフ/状態確認できるgem(RubyFlowより)

# github.com/jnunemaker/flipperより
require 'flipper'
require 'flipper/adapters/memory'

Flipper.configure do |config|
  config.default do
    # pick an adapter, this uses memory, any will do
    adapter = Flipper::Adapters::Memory.new

    # pass adapter to handy DSL instance
    Flipper.new(adapter)
  end
end

# 検索が有効かどうかをチェック
if Flipper.enabled?(:search)
  puts 'Search away!'
else
  puts 'No search for you!'
end

puts 'Enabling Search...'
Flipper.enable(:search)

# 検索が有効かどうかをチェック
if Flipper.enabled?(:search)
  puts 'Search away!'
else
  puts 'No search for you!'
end

有料版のFlipper::Cloudもあるようです。


つっつきボイス: 「クックパッドのchanko gemみたいなやつかな」


cookpad.github.io/chankoより

「ところで、flipperがなぜイルカなのかわかります?」「わかんね」「わかんね」「Officeイルカじゃなさそうだけど」「私が子供の頃『わんぱくフリッパー』っていう米国制作の番組が放映されてたんです(年即バレ)」

モデルになったとされるイルカは、不機嫌になるとすぐ芸の道具を全部ひっくり返す癖があったのでflipperというあだ名になったというのを何かで読んだ覚えがあります。

年に1度だけ起きるバグ(RubyFlowより)

とても短い記事です。

# 同記事より
event = test_organizer.create_published_event(starts_at: 25.hours.from_now)

つっつきボイス: 「年1バグとか普通にあるけど、これはどういうやつかな?以前のウォッチで扱ったActiveSupport::Durationでもなさそうだし」「25?」「あーサマータイムか」「サマータイムがある国の開発者(´・ω・)カワイソス」

週刊Railsウォッチ(20170120)Ruby 2.5.0 devリリース、古いMySQLのサポート終了、uniqメソッドが削除ほか

「そういえば米国ではsummer timeではなくDSTって書きますね: 自分もsummertimeというとスタンダードナンバーを連想します」

ジュニア開発者へのRails設計アドバイス(Awesome Rubyより)

以下は見出しから。

  • 純粋なRubyオブジェクトとデザインパターンを恐れず使うべし
  • (暗黙的でない)明示的なコードにすべし
  • アプリを水平分割すべし
  • 機能以外の本当の要件を満たす設計を
  • テストは徹底的に行うべし
  • 継承よりコンポジションを優先すべし
  • 制御フローを尊重すべし

つっつきボイス: 「うん、悪くなさそう: Railsに限らない話もいろいろある」「after_create/after_update/after_destroy/after_commitを避けろというのは大事: after_系フックを使うならちゃんと値を返すべきだし、自分自身を変えないこと」

reek: 「コードの匂い」を検出するgem(Awesome Rubyより)


github.com/troessner/reekより

gem install reekしてオレオレRailsアプリにかけてみたらこんな感じで出ました。ドキュメントのURLも出力してくれます。Rubyに絆創膏。

app/controllers/patterns_controller.rb -- 12 warnings:
  [41, 43]:DuplicateMethodCall: PatternsController#create calls 'format.html' 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]
  [80, 87]:DuplicateMethodCall: PatternsController#update calls '@pattern[:id]' 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]
  [80, 87]:DuplicateMethodCall: PatternsController#update calls 'edit_pattern_path(@pattern[:id])' 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]
  [80, 82]:DuplicateMethodCall: PatternsController#update calls 'format.html' 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]
  [7]:InstanceVariableAssumption: PatternsController assumes too much for instance variable '@pattern' [https://github.com/troessner/reek/blob/master/docs/Instance-Variable-Assumption.md]
  [35]:TooManyStatements: PatternsController#create has approx 11 statements
...

つっつきボイス:rubocopのcopにして欲しいー: rubocopと他のツールのwarningを調整するの大変だし」

activerecord-import: 一括インポートgem+バリデーション(Awesome Rubyより)

# wikiより
columns = [ :title, :author ]
values = [ ['Book1', 'FooManChu'], ['Book2', 'Bob Jones'] ]

# Importing without model validations
Book.import columns, values, :validate => false

# Import with model validations
Book.import columns, values, :validate => true

# when not specified :validate defaults to true
Book.import columns, values

つっつきボイス: 「前にもウォッチで触れたことありましたが一応」「activerecord-importはbulk insert gemとしては使いやすくて有能なやつですね: バリデーションもやってくれるし」「お、ちょうど今の案件に使えそうダナ」

dckerize: RailsアプリのDockerイメージを作るgem(RubyFlowより)

# 同リポジトリより
$ rails new myapp --database=postgresql
$ cd myapp
$ dckerize up myapp

# DBファイルを設定

$ docker-compose build
$ docker-compose up

つっつきボイス: 「とりあえず動かしてみたんですが、PostgreSQLのコンテナのところでつっかえてしまいました」「うーん、PostgreSQL 9.5.3のバージョンべた書きだったり、/var/lib/postgresqlに直接つっこんだりしてるしなー: 自分専用なんじゃ?」「そんな感じですね: 自分でdocker-compose書くのがよさそう」「Dockerやったことない人がお試しに動かすきっかけにはなるかも」

Ruby trunkより

提案: Enumerator#next?

class Enumerator
  def next?
    peek
    true
  rescue StopIteration
    false
  end
end

a = [1,2,3]
e = a.to_enum
p e.next?   #=> true
p e.next    #=> 1
p e.next?   #=> true
p e.next    #=> 2
p e.next?   #=> true
p e.next    #=> 3
p e.next?   #=> false
p e.next    #raises StopIteration

つっつきボイス: 「へー、next?って今までなかったのか」「採用されるかどうかはmatz次第みたいです」

2つのArrayの間にカンマがないとnilになる => 仕様どおり(却下)

[2, 2][3, 3] # => nil

つっつきボイス: 「これ、仕様どおりですよね」「Stackoverflowレベルの内容をRubyバグに投げるのは勇者」

Ruby

RubyGems 2.7.0がリリース

今見たらもう2.7.2になっています。

  • 2.7.0
    • Update vendored bundler-1.16.0. Pull request #2051 by Samuel Giddins.
    • Use Bundler for Gem.use_gemdeps. Pull request #1674 by Samuel Giddins.
    • Add command signin to gem CLI. Pull request #1944 by Shiva Bhusal.
    • Add Logout feature to CLI. Pull request #1938 by Shiva Bhusal.
  • 2.7.1
    • Fix gem update –system with RubyGems 2.7+. Pull request #2054 by Samuel Giddins.
  • 2.7.2
    • Added template files to vendoerd bundler. Pull request #2065 by SHIBATA Hiroshi.
    • Added workaround for non-git environment. Pull request #2066 by SHIBATA Hiroshi.

#2065と#2066、ちょうど昨日踏んでしまいましたが、gem update --systemで即修正完了でした。

Ruby 2.5で速度が改善された点(RubyFlowより)


rubyguides.comより

  • 式展開
  • String#prepend
  • Enumerableのメソッド
  • Range#minRange#max
  • String#scan

つっつきボイス: 「式展開の改善は目覚ましい」「他のは誤差っぽいかなー」「何にしろRuby 3×3に向けて着々と進んでいる感じですね」

メモリを意識したRubyプログラミング


gettalong.orgより

# 同記事より
2.4.2 > require 'objspace'
 => true
2.4.2 > ObjectSpace.memsize_of(nil)
 => 0
2.4.2 > ObjectSpace.memsize_of(true)
 => 0
2.4.2 > ObjectSpace.memsize_of(false)
 => 0
2.4.2 > ObjectSpace.memsize_of(2**62-1)
 => 0
2.4.2 > ObjectSpace.memsize_of(2**62)
 => 40

つっつきボイス: 「おー、オブジェクトサイズの変わる境界がいろいろ示されていて面白い」「文字列は23バイト目から変わる、と」「ここらへんはRubyのRVALUEみたいな内部構造に関わってるはず」「近々それ系の翻訳記事出します」「この記事見てて、k0kubunさんのこのツイートを思い出しました↓: 最速のメソッドを身体がつい選んでしまうとかもう常人じゃない感」

RubyとPythonで文字列分割対決(Awesome Rubyより)


chriszetter.comより

# ruby
"".split("-") #=> []
# python
"".split("-") #=> [""]

つっつきボイス: 「Rubyが空の[]を返すのはAWKに近いのか」「著者はPythonにstr.splitの挙動をAWKに近づけるよう提案したけど通らなかったらしいです」

商用で使われるRubyのバージョン分布

何となく年の瀬が近づいた感じがしてきました。


つっつきボイス: 「おー、1.8系はほぼ消滅か」「2.0以上でもう8割ですね」「Rubyが後方互換性を大事にしているおかげでみんな結構気軽にアップグレードしてる感ある」「frozen_string_literalのような変更にもちゃんと猶予期間を設けてますね」

k0kubunさんのyarv-mjitチューンアップ


つっつきボイス: 「やっぱりoptcarrotでやってます」「60fps超えてるー」

SQL

「Mastering PostgreSQL」が発売


masteringpostgresql.comより

著者のメルマガで知りました。


つっつきボイス: 「タイムセールに乗って即買いました」「そういえば好評につきタイムセールを48時間伸ばしたって通知メールに書いてありました」
「PostgreSQL 10の後に出てるからそちらもカバーはしているけど、PostgreSQLそのものを掘り下げる内容」
「この本はボリュームディスカウントもあって、2冊分の価格(179ドル)で50人まで買えますね: 社内で3人以上買うならこれの方がお得」

PostgreSQL 10の嬉しい点5つ(Postgres Weeklyより)


10clouds.comより

  • idカラムの指定がSQL準拠に
  • ネイティブのパーティショニング機能
  • 複数カラムのstatistics
  • 並列性向上
  • JSON/JSONBの全文検索
# 同記事より
# 9
CREATE TABLE foo (id SERIAL PRIMARY KEY, val1 INTEGER);

#10
CREATE TABLE foo (id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, val1 INTEGER);

つっつきボイス: 「おーやっぱすごい」「JSONの全文検索もうれしいけど、バッドプラクティスになりやすいので注意かな」
「ところでPostgreSQLのexplainって、MySQLのよりずっとずっと読みやすくて助かる」「全面同意します!」「PostgreSQLはindex作成中でもクエリかけられるし」

[Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)

JavaScript

JavaScriptのasyncとawaitがひと目でわかる動画(15秒)

社内Slackに投下してもらって知りました。


つっつきボイス: 「これマジでわかりやすい」「マジ」

5分でわかるJavaScriptのpromise


codeburst.ioより

Angular.js 5.0.0リリース


blog.angular.ioより

更新情報がいっぱいありすぎて書ききれない感じです。


つっつきボイス:CLDR対応は大きいかも」


cldr.unicode.orgより

TestCafe: Selenium要らずのWeb結合テストサービス(JavaScript Liveより)


devexpress.github.io/testcafeより

Node.js用です。


www.dedicatedcode.comより


つっつきボイス: 「この元記事の方、コードの配色がすごく読みにくい…」

Frappé Charts: GitHub風グラフ表示ライブラリ(Frontend Focusより)

依存関係なしで使えるそうです。

github.com/frappe/chartsより

CSS/HTML/フロントエンド

フロントエンドチェックリスト


codeburst.ioより

メタタグ/CSS/画像などのチェック項目リストです。


つっつきボイス: 「長い…ここまで増えたら自動化したい」

CSSだけでできる新しめのフォームデザイン(Frontend Focusより)


jonathan-harrell.comより

プレースホルダ文字を動的に移動するなどのテクニックが紹介されています。


jonathan-harrell.comより

FlexGrid: 有料のテーブル作成ライブラリ(Frontend Focusより)


grapecity.comより

非常に凝ったテーブルを作れるJSライブラリです。これも依存なしに使え、AngularとReactでも使えるそうです。


demos.wijmo.comより

その他

最も嫌われているプログラミング言語

stackoverflow.blogより

みっちりと書かれています。


stackoverflow.blogより


つっつきボイス: 「これも面白い記事」「Perlが圧勝」「CoffeeScript…(´・ω・)カワイソス」

番外

ダイヤルアップモデムの音と波形(HackerNewsより)

これはもうリンク先をご覧ください。


つっつきボイス: 「テレホタイム思い出した」「この音知らない人増えたでしょうね(年バレ)」「音は聞いたことあります」

最初のフック音が米国の電話のトーンだったので、大昔にどきどきしながら国際電話かけたときのことをつい思い出してしまいました。

足を鍛えると知能も鍛えられる?

鍛えるなら足というか下半身かなと思いました。

ニューラルネットワークをだます

この亀のフィギュアを銃と誤認させることに成功したそうです。


今週は以上です。

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

週刊Railsウォッチ(20171026)factory_girlが突然factory_botに改名、Ruby Prize最終候補者決定、PhantomJS廃止、FireFoxのFireBug終了ほか

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

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

Rails公式ニュース

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Hacker News

160928_1654_q6srdR

Viewing all 1380 articles
Browse latest View live