Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20200525前編)2020年のRailsマストgem 19個、スライド『Fat Modelの倒し方』、AR mergeのrewhereオプションを変更ほか

こんにちは、hachi8833です。JavaScriptが25歳の誕生日を迎えたそうです🎉。10日そこそこで最初のプロトタイプを作ったとは😳。Rubyはちょっとだけ年上なんですね。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

今回のつっつき会は日中の社内勉強会枠で行いました。

Rails: 先週の改修(Rails公式ニュースより)

今週は以下のコミットリストから見繕いました。Changelogのdiffも見るのが効率よいとやっと気づきました😅。

Active Recordで署名付きidをサポート

署名付きidベースでのレコード検索サポートを追加した。署名付きidは不正防止の照合済みidのことで、expires_inで期限を設定したりpurposeでスコープを指定したりできる。これが特に有用なのは、パスワードリセットやメール認証などでレコードとやりとりできる署名付きidの所有者が欲しいが、特定の期限を設けたい場合。
同PRより大意

# 同PRより
signed_id = User.first.signed_id expires_in: 15.minutes, purpose: :password_reset

User.find_signed signed_id # => nil, since the purpose does not match

travel 16.minutes
User.find_signed signed_id # => nil, since the signed id has expired

travel_back
User.find_signed signed_id, purpose: :password_reset # => User.first

User.find_signed! "bad data" # => ActiveSupport::MessageVerifier::InvalidSignature

つっつきボイス:「DHH自らのプルリクです」「サインドid?」「purpose: :password_resetみたいに書けるのね」「いろんなidにマッピングされる、有効期限付きのidを発行できる機能がサポートされたように見える: インターフェイスが便利そう👍」

「この署名付きidって何でしょう?」「生のidを外部にさらさないために一時的に使えるidですね🧐」「有効期限を付けられるし、そんな感じかな」「そうそう😋」

「このfind_signed↓なんか、署名付きidを渡すと生のidが取れるし😋」「単に取り直してるだけという😆」

# activerecord/lib/active_record/signed_id.rb#42
      def find_signed(signed_id, purpose: nil)
        if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose))
          find_by id: id
        end
      end

「SHA256使ってる〜」「他の部分はこの署名付きidの管理周りでしょうね」「idや期限はどこに保存してるんだろう?🤔」「復号したら時刻が入っててアプリケーションでそれをチェックしたりして、って何も見ないで言ってますけど😆」「まあやろうと思えばできますね☺️」

参考: SHA-256(Secure Hash Algorithm 256-bit)とは - IT用語辞典 e-Words

「テストも1分で期限切れにしてtravelで2分進めたらnilが返るようになってる↓」

# activerecord/test/cases/signed_id_test.rb#32
  test "fail to find signed record within expiration date" do
    signed_id = @account.signed_id(expires_in: 1.minute)
    travel 2.minutes
    assert_nil Account.find_signed(signed_id)
  end

「このプルリク、いいね🎉がいっぱい付いてますね」「こういう一時的なid発行はユースケースとしてよくあるヤツ🧐」「今までだとgem使ったり自力で実装したりという感じだったんでしょうね」「こういう機能があることを知ってれば使うかも😋」「自力でやらずに済む😂」

「Deviseを使うとこういうのが入ってきますね: インターフェイスはちょっと違いますけど😆」「なるほど」「Deviseだと何とかtokenというカラムがあった覚えがあります」

each_paireach_valueにブロックを渡さない場合にEnumeratorを返すようになった

# actionpack/lib/action_controller/metal/strong_parameters.rb#L363
    def each_pair(&block)
+     return to_enum(__callee__) unless block_given?
      @parameters.each_pair do |key, value|
        yield [key, convert_hashes_to_parameters(key, value)]
      end
    end
  ...
    def each_value(&block)
+     return to_enum(:each_value) unless block_given?
      @parameters.each_pair do |key, value|
        yield convert_hashes_to_parameters(key, value)
      end
    end

つっつきボイス:「改修前はブロックを渡さないとエラーになってたんですね😳」「ハッシュの場合とインターフェイスを合わせたということですね👍」「なるほど!」「普通のハッシュならこうやって呼べますので☺️」「こういうのが直っていくのはいいですね〜😋」

irb(main):001:0> ActionController::Parameters.new(foo: "bar").each_pair
=> #<Enumerator: <ActionController::Parameters {"foo"=>"bar"} permitted: false>:each_pair>
irb(main):002:0> ActionController::Parameters.new(foo: "bar").each_value
=> #<Enumerator: <ActionController::Parameters {"foo"=>"bar"} permitted: false>:each_value>

「この間社内でもちょっと話題になったんですけど、RailsのActive Recordとかって、インターフェイスはたしかにEnumerableと同じなのに、実際にはEnumerableモジュールをインクルードしてないものがちょくちょくあったりするんですよね😅」「あ〜わかります😆」「それと似た感じで、Enumerableモジュールはインクルードしてないけど、あたかもしているかのような挙動に近づけたんでしょうね☺️」

raise_on_missing_translations設定をビューとコントローラで統一

# actionview/lib/action_view/railtie.rb#L43
    config.after_initialize do |app|
      ActiveSupport.on_load(:action_view) do
        app.config.action_view.each do |k, v|
+         if k == :raise_on_missing_translations
+           ActiveSupport::Deprecation.warn \
+             "action_view.raise_on_missing_translations is deprecated and will be removed in Rails 6.2. " \
+             "Set i18n.raise_on_missing_translations instead. " \
+             "Note that this new setting also affects how missing translations are handled in controllers."
+         end
          send "#{k}=", v
        end
      end
    end

つっつきボイス:「今までconfig.action_viewにあったi18n(国際化)設定をconfig.i18nにまとめたのね↓」「あ、設定の話なのか😳」

# actiontext/test/dummy/config/environments/development.rb#L57
  # Raises error for missing translations
- # config.action_view.raise_on_missing_translations = true
+ # config.i18n.raise_on_missing_translations = true

「訳文をフェッチして取れなかった場合の振る舞いなんでしょうね☺️」「まあi18nの設定をビューとコントローラで別々にすることはまずないから、まとめちゃってもいいと思いま〜す😋」「普通に考えて、コントローラではi18nエラーを出さないけどビューでは出すなんてしませんし😆」「一緒にしたいですよね😆」

:rewhereオプションとmergeを組み合わせたときの挙動を変更

mergeのオプション:rewhereで、mergeされる側の条件をそのまま置換するようサポートされた。
Changelogより


つっつきボイス:「rewhereって何これ?🤣」「スゴい名前🤣」「へ〜、rewhere自体は前からあったみたい」「あ、そうなんですね😅」「名前からしてreorderと同じ頃にrewhereが実装されたのかも🤔」「この改修は、Active Recordのmergerewhereを使ったときの挙動をあるべき姿に戻したということでしょうね☺️」

「まあunscopeよりはrewhereの方が直感的ではありますし😋」「たしかに😋」「条件が既に設定されてしまっているwhereとかorderとかをやり直したいときって割とよくありますけど、unscope.whereって書くよりはrewhereって書く方が早いし😆」「whereを付け直したいときあるといえばある😆」

「ま、自分はActive Recordのインスタンスを取り直す方がいいんじゃないかと思いますけど、どうしても付け直したい人がいるんでしょうし🤣」「😆」

関連PR: #39236
relation.mergeメソッドは、mergeされる側の条件を置き換えることもあるが、relation.rewhereを使わない場合はmergeする側とされる側の条件が両方残ることもある。
このままでは、mergeされる側の条件が置き換えられるのかどうかをmergeの結果で予測するのがきわめて難しい。
既にある方法のひとつは、mergeする側のリレーションでrelation.rewhereを使うことだが、これもmergeでリレーションが使われるかどうかを事前に知るのが難しい(mergeのワンタイムリレーションを除く)。
この問題を修正するために、merge:rewhereオプションで、mergeされる側の条件をそのまま置換することをサポートするよう提案したい。
このオプションは、rewhereでないリレーションがrewhereリレーションとして振る舞うようになる。
同PRより大意

david_and_mary = Author.where(id: david.id..mary.id)

# マージする側とされる側の両方に競合条件が存在する
david_and_mary.merge(Author.where(id: bob)) # => []

# マージされる側の条件がrewhereによって置き換えられる
david_and_mary.merge(Author.rewhere(id: bob)) # => [bob]

# マージされる側の条件がrewhereオプションによって置き換えられる
david_and_mary.merge(Author.where(id: bob), rewhere: true) # => [bob]

where(id: david.id..mary.id)した状態でwhere(id: bob)をマージすると、bobがない結果セットからさらに絞り込む形になるので結果は空になると」「それが本来望ましい動作だから、出るはずのないbobがwhere(id: bob)で取れたら確かにびっくりしますし😳」「でrewhere(id: bob)だとやり直せるからbobが取れると」

「一番下にrewhere: trueっていうオプションがある」「このオプションが必要になるときって一体😆」「この辺の挙動を理解してないとrewhere: trueって付けるのを思いつかなさそう😆」

「このscope.unscope!しているあたり↓が改修ポイントかな」「breaking changesにならない範囲で修正したんでしょうね😋」

# activerecord/lib/active_record/relation/query_methods.rb#L704
    def rewhere(conditions)
-     attrs = []
      scope = spawn
-
      where_clause = scope.build_where_clause(conditions)
-     where_clause.each_attribute do |attr|
-       attrs << attr
-     end

-     scope.unscope!(where: attrs)
+     scope.unscope!(where: where_clause.extract_attributes)
      scope.where_clause += where_clause
      scope
    end

rewhere、自分はあんまり使わないかな〜: そもそもrewhereを使わないといけないようなコードの書き方しませんし🤣」「それやったらスコープのライフサイクル長過ぎ〜🤣」

ENVのBACKTRACEオプションでバックトレースの削除をオフにできる


つっつきボイス:「Railsフレームワークのバックトレースを消せるようになったのかと思ったら、バックトレースのクリーンアップを環境変数で止められるようになったのね」「二重否定っぽくてややこしい😆」「これはなかなかいい機能😋」

「最初ドキュメントにハウツーを追加したのかなと思ったら、よく見ると下にコードも追加されてました↓😅」「BACKTRACE=1という書式がちょい微妙😆」「backtrace_cleanerにもともとremove_silencersというのがあって、それをオンにできるようになったと」「デフォルトではバックトレースを削除すると😆」「ほとんど隠し機能😆」

# railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt#L6
-# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
-# Rails.backtrace_cleaner.remove_silencers!
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
+# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
+Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]

フレームワークのコードや埋もれたissueをデバッグするときは、一時的にバックトレースの削除をオフにしておきたくなることが多い。これを環境変数で設定できれば楽だ。
同PRより大意

Rails

2020年度のマストgem 19


つっつきボイス:「今年のマストgemどんなんかな?」「RuboCopは入ってませんが、たぶんあって当然なのかなと😆」

「bootstrap-emailがある」「これおいしいヤツでしょうか?😋」「HTMLメールはBootstrapのスタイルを付けられないので、それを効かせられるようにするやつなのかなと😆」「HTMLメールのスタイルは普通styleに入れないといけないですし☺️」

「先週話題にしたlockboxも入ってますね↓(ウォッチ20200519)」

「rolifyって初めて見たかも↓」「いわゆるユーザーの認可(authorization)とか権限周りを管理するgemですね」「cancancanが隣にあるし😆」「has_role?するヤツ」

# 同リポジトリより
user.has_role?(:moderator, @forum)
#=> false # ユーザーが別のフォーラムのモデレータである場合

「Faradayもいらっしゃる↓」「Faraday今も現役なのね〜」「更新もされてますね」

「まあこれまでとそんなに大きく違うわけではなさそうかな」「bootstrap-emailがマストかどうかはさておき😆」「メール使わなければ要らないですし😆」「突飛なgemはなさそう😋」「Railsもそれだけ枯れてきたというか成熟したんでしょうね☺️」

interactor-rails: RailsでInteractorパターン

# 同リポジトリより
class AuthenticateUser
  include Interactor

  def call
    # 何かする
  end
end

つっつきボイス:「interactor-railsは取り上げたことがありそうでなかったので」「interactor gemをRailsですぐ使えるようにした感じみたい」「interactorはさっきのマスト19 gemにも載ってましたし😆」「どうせRailsで使うことが多いでしょうから統合したんでしょうし😆」「使いたいならどうぞ〜😋」

interactor gemは以下の翻訳記事にも載っています。

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

銀座Railsスライド『Fat Modelの倒し方』


つっつきボイス:「この間の銀座Railsのスライドですね」「ファットモデルはRailsの『滑らない話』の定番😆」「後で読もっと😋」

「そういえば神速さんはなるべくgemを使わない主義って聞いた覚えありますね」「その辺も含めて、やっぱり設計周りは人によってさまざまだな〜って思いながら聞いてましたし、こういう設計周りの話はいろんな人のを聞いておくのがいいと思います👍」「設計で『これしかない』って思い込んだらコワいですね😅」


以下はウォッチ公開後に見つけたツイートです。

Awesome Code: リポジトリのコードをオートフォーマットするサービス


同サイトより


つっつきボイス:「GitHubだけじゃなくてBitBucketやGitLabにも対応してるようです」「リポジトリCIのフックに挟めるとかそういう感じですね😋」「おいくらかな?」「オープンソースは無料で、デフォルトブランチのみのStartupプランだと月10ドルか〜💰」「サービスでやる方が楽ならやってもいいかも」

「その下の紹介記事によると、Awesome CodeはRuboCopやrufoなどをいいとこ取りして、面倒な部分を考えずにやれるようです」「こういうlintの設定ってプロジェクトによって違いますけどね😅」「そういや最近rufo使ってない😆」「自分も外しました😆」「その辺の設定をIDEのlintとなかなか同じにできないのが面倒なんですよね😢、全員が同じIDE使っていればそれでやれるんですけど」「たしかに」

書籍『The Rails 6 Way』


つっつきボイス:「銀座Railsでこの本の話題が出ていたので」「Rails 6のRails Wayね」「と思ったら執筆の進捗20%でした😆」「はよ出さないとRailsが6.1になっちゃう😆」「頑張って欲しいです😂」「でもRails 6はマルチDB周りとかが固まってなかったりしますし」「Active StorageはRails 6より前からですけど、そこそこ定着した感ありますね☺️」


目次より:

  • 強力・スケーラブルでRESTに沿ったバックエンドサービスを構築する
  • Action Controllerを用いる複雑なフローのプログラミング
  • Active Recordによるモデル/リレーションシップ/操作の表現、およびActive Recordの高度なテクニック
  • マイグレーションでデータベーススキーマをスムーズに進化させる
  • Action Viewでフロントエンドを構築
  • アセットパイプラインやWebpackerによるアセットの構築
  • キャッシュやTurbolinksによるパフォーマンスやスケーラビリティの最適化
  • hamlテンプレートで生産性を向上させる
  • SQLインジェクション/XSS/XSRFなどの攻撃からシステムを守る
  • Action MailerやAction Mailboxによるメール統合
  • Action CableでWebSocketsベースのリアルタイムなブラウザ動作を有効にする
  • バックグラウンド処理でレスポンスを改善する
  • JSONをやりとりするAPI専用バックエンドプロジェクトを構築する
  • Active Storageでファイルをクラウドに保存する
  • Action Textで洗練されたWYSIWYGを追加
  • マルチプルデータベースの活用

その他Rails


つっつきボイス:「1つ目はTruffleRubyのCIテストが全部通ったそうです」「あ、Rubygemsのテストね😆」「TruffleRubyでRailsが通ったのかと思った😆」

つっつき後にTruffleRuby 20.1.0がリリースされました。

「2つ目はRailsでRSpecやる人にはお馴染みの『Everyday Rails』が10周年だそうです🎉」「🎉」「そういえば昨日大学の講義で自己紹介するときに自分もRailsやって10年経ってるって気づきましたし😆」「10年!」「もうPHPフレームワークより長かった😆」


「以下はついさっき見つけたjnchitoさんのチュートリアル動画です↓(30分)」


前編は以上です。

おたより発掘

バックナンバー(2020年度第2四半期)

週刊Railsウォッチ(20200518前編)スライド『令和時代のRails運用』、Ruby 3.0のキーワード引数変更リスケ、Action CableのCLIほか

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

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

Rails公式ニュース


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。