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

週刊Railsウォッチ: Railsコミュニティアンケート結果発表、書籍『Sustainable Web Development with Ruby on Rails』ほか(20220531)

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

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

🔗 Action Textでパーシャルが何度もレンダリングされる問題を修正

rich_textフィールドが更新されたときにコンテンツのレイアウトが何度もレンダリングされる問題を修正。
Jacob Herrington
同Changelogより


つっつきボイス:「Action Textの==を修正してますね」「2019年にも#37818が上がっているので割と前から起きていたみたい」「Action Textを使わなければ影響はなさそう」

# actiontext/lib/action_text/content.rb#L106
    def ==(other)
-     if other.is_a?(self.class)
+     if self.class == other.class
+       to_html == other.to_html
+     elsif other.is_a?(self.class)
        to_s == other.to_s
      end
    end

🔗 convert_to_model呼び出しをform_forからform_withに移動

convert_to_model呼び出しをform_forからform_withに移動する
form_forは今やform_withの形で実装されているので、このコミットではconvert_to_model呼び出しもform_forの実装から削除する。
同Changelogより

# actionview/lib/action_view/helpers/form_helper.rb#L433
      def form_for(record, options = {}, &block)
        raise ArgumentError, "Missing block" unless block_given?
        case record
        when String, Symbol
          model       = nil
          object_name = record
        else
-         model       = convert_to_model(record)
+         model       = record
# actionview/lib/action_view/helpers/form_helper.rb#L754
      def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
        options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)
        if model
          if url != false
            url ||= if format.nil?
              polymorphic_path(model, {})
            else
              polymorphic_path(model, format: format)
            end
          end

-         model   = _object_for_form_builder(model)
+         model   = convert_to_model(_object_for_form_builder(model))
          scope ||= model_name_from_record_or_class(model).param_key
        end

つっつきボイス:「form_forはRails 7にもまだ一応あるんですね」「convert_to_model呼び出しを移動したリファクタリングか」「form_withmodel:引数はもう要らないかもねというコメントに、やるなら別のプルリクにするかもとレスが付いていますね」

Rails 5.1〜7.1: 'form_with' APIドキュメント(翻訳)

🔗 バリデーションのinwithinでbeginless/endless rangeをサポート


つっつきボイス:「1つ目は、validates_length_ofでもinwithinの範囲を以下の..30のように書けるようになった」「Ruby 2.6のbeginless/endless range機能はRails 6.1から入ったんだったかな」

# 同Changelogより
validates_length_of :first_name, in: ..30

参考: 演算子式 (Ruby 3.1 リファレンスマニュアル)

Rails 7: 数値バリデーションを範囲(..)で指定できるinオプションが追加(翻訳)

「2つ目は、validates_inclusion_ofvalidates_exclusion_of..でも開始を省略できるようになったんですね」

# 同Changelogより
validates_inclusion_of :birth_date, in: -> { (..Date.today) }

参考: validates_inclusion_of -- ActiveModel::Validations::HelperMethods
参考: validates_exclusion_of -- ActiveModel::Validations::HelperMethods

「これは嬉しい改修でしょうか?」「<>とかで書くよりはわかりやすいのかも : 個人的な趣味としては{ (..Date.today) }の内側の丸かっこ()があまり好きじゃないぐらいかな」

「ところでこのin: -> { (..Date.today) }は、要するに未来の日付ではないという意味ですね」「たしかに誕生日に未来の日付は入力しませんよね」

「そういえば、以前英語圏の人の記事で<>よりbeforeやafterの方がわかりやすいと書かれてました↓」「ボクも<>が2つ以上出てくるとよく向きを間違えちゃいます」

Railsの技: 日付同士をbefore?とafter?で比較する(翻訳)

🔗 uniqueness指定のフィールドが変更されていない場合のバリデーションを回避

従来はActive Recordのレコードを保存するときにuniqueness: trueが指定されているバリデーションを属性ごとにチェックするための余分なクエリが、属性が変更されていない場合にも実行されていた。
データベースにUNIQUEインデックスがある場合は、このバリデーションが永続化済みレコードで失敗することはありえないので、安全にスキップ可能。
fatkodima
同Changelogより


つっつきボイス:「パフォーマンスがよくなりそうな改修」「uniquenessバリデーションはデータベースクエリが発生して遅くなるので、無駄なクエリを投げないのはいい👍」「不要なクエリを投げないようにする改修はこの間もありましたね(ウォッチ20220516)」

「割とコードやテストの追加量が多いかも」「この種の改修は、ほぼ起きなさそうなレアケースの対応も含めてちゃんとやるのは割と難しいと思うので、テストでしっかり押さえておかないといけないでしょうね」「なるほど」

🔗 ActiveSupport::Cache::Store#fetch_multiforceオプションをサポート

# activesupport/lib/active_support/cache.rb#L495
      def fetch_multi(*names)
        raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?
        options = names.extract_options!
        options = merged_options(options)

        instrument :read_multi, names, options do |payload|
-         reads   = read_multi_entries(names, **options)
+         if options[:force]
+           reads = {}
+         else
+           reads = read_multi_entries(names, **options)
+         end
+
          writes  = {}
          ordered = names.index_with do |name|
            reads.fetch(name) { writes[name] = yield(name) }
          end
          payload[:hits] = reads.keys
          payload[:super_operation] = :fetch_multi
          write_multi(writes, options)
          ordered
        end
      end

つっつきボイス:「forceオプションをキャッシュストアに渡せるようにしたらしい」「forceはメモリ上のキャッシュを使わずに、保存済みのキャッシュストアから強制的に読み直すということでしょうか?」「そうだと思います」

「複数キーの取り出しでキャッシュ有効期限内のキーと失効したキーが混じっているときに、forceで有効期限を無視して取り直すのかな?」「テストコード↓を見るとexpires_inに60や100を指定しているので、そのようなユースケースを想定していそうですね」

# 同PRより
  def test_fetch_multi_race_condition_protection
    time = Time.now
    key = SecureRandom.uuid
    other_key = SecureRandom.uuid
    @cache.write(key, "foo", expires_in: 60)
    @cache.write(other_key, "bar", expires_in: 100)
    Time.stub(:now, time + 71) do
      result = @cache.fetch_multi(key, other_key, race_condition_ttl: 10) do
        assert_nil @cache.read(key)
        assert_equal "bar", @cache.read(other_key)
        "baz"
      end
      assert_equal({ key => "baz", other_key => "bar" }, result)
    end
  end

🔗Rails

🔗 書籍『Sustainable Web Development with Ruby on Rails』


つっつきボイス:「ツイートでも引用されている以下の記事でこの本を知ってポチりました」「この本話題になってますね」「紙の書籍の定価は$49.95ですが、国によってディスカウントがあって、Amazon.co.jpでKindleだと1,880円で買えました」「お、リーズナブルな値段」

参考: 『Sustainable Web Development with Ruby on Rails』はRails使ってるなら絶対面白いと思う
参考: Sustainable Web Development with Ruby on Rails -- 公式サイト

「そういえば@r7kamuraさんも早速読んでブログに書いていますね↓」

参考: 『Sustainable Web Development with Ruby on Rails』を読んだ

「まだ半分ぐらいしか読んでませんが、英文も思った以上に読みやすくて、Railsチュートリアルを終えた人やRails開発1年目ぐらいの人によさそうかなと思いました」

その後読み終わりました。

🔗 Serviceクラスよもやま

「同書を巡って日本語記事でも話題になっているServiceクラスは、以下の記事で知ってたコマンドパターン的なService Objectと少し違う感じでした: クラスメソッドにせずにnewして使うとか、汎用的なcallを使わずに具体的なメソッド名にするとか」「コマンドパターン的なService Objectでもそういう書き方をすることはありますよ: ただ個人的にはそうした詳細部分の違いはあまり重要ではない気もします」

Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)

「設計の好みの話で言うと、自分はService Objectがクラスメソッドなのは特に気にならない方です: 逆にインスタンス化してステートを持たせたり使い回されたりするのが好きではないので、クラスメソッドを呼んだらそれでおしまいという形の方が好み」「自分はコンテキストとして使いたいので、逆にクラスメソッドにするのがあまり好きではない派かも」「自分はそれをさせたくない気持ちなんですよ」「なるほど、それもわかります」

「細かな設計はつまるところ実装者の好み次第なので、明らかに問題があるとか既存の設計と調和しないということがない限り、コードレビューでは通すことが多いと思います」「そうそう、結局は好みというか宗派ですよね」「もちろん決済周りのような非常にクリティカルな部分の設計だったらレビューで指摘することもあると思いますが、たとえばService Objectが既にプロジェクトで使われているなら同じように書く方が望ましいぐらいの気持ちなので、このあたりの設計に強いこだわりはないですね」

🔗 Railsコミュニティアンケート2022年度版の結果発表


つっつきボイス:「以前取り上げた2022年度版アンケート(ウォッチ20220228)の結果が発表されていました」

「CIでGitHub Actionsが2022年でランク外から突然トップに上がっているのが興味深い: GitHub Actionsで足りるなら連携も楽だし、たしかに便利」「Circle CIが2位でGitLabが3位か」「BPSでメインとして使っているGitLabがトップ3に入っているのを見たら、GitLabはマイナーじゃないと思えて安心しました」

「CDNはCloudFrontを使っているという回答の方が、コストの安いCloudflareより多いところを見ると、アンケートの母集団にはエンタープライズ系の回答者が多そうな気がしますね」

「デプロイ周期がほぼ毎日という回答数が上昇しているのも興味深い」「めったにデプロイしないという回答数はめっきり減っていますね」

「新人にRailsの学習と構築を2022年も推奨するかはYesが94%」「あくまでこの母集団での結果ですけどね」

🔗 DeviseとTurbo


つっつきボイス:「DeviseのRails 7対応ってどうなっているんだろうと思って探してみると、Rails 7ではTurboをオフにしようというプルリクがオープンされているのが目に付きました」「Deviseを使う人はSessionsControllerを継承してカスタマイズする人が多いと思いますし、仮にTurboを使うとしてもログイン画面までSPA的にしなくてもよさそうに思えるので、実際に使うときは自力でログイン画面のTurboをオフにして部分更新しないようにしている人が多いかもしれませんね」「なるほど」

「ついでに気づいたんですが、Jeremy Evans作のrodaをベースにしたrodauth-railsは↓Rails 7に対応しているものの、こちらもTurboをオフにする必要があるそうです」

janko/rodauth-rails - GitHub

「Turboみたいなものは認証と相性が良くないんでしょうか?」「Turboというよりasync/await的な非同期処理と相性が良くないというのはあるでしょうね: ログインセッションの状態を更新するリクエストとログイン後にリクエストする前提のリクエストを同時に非同期で実行すると、認証されていることを前提とするリクエストが認証されてなくてエラーになったりする」「あ、そうか」

「その意味では、Webアプリの認証は非同期ではなく同期的にやりたいものですね: 認証をTurboで非同期に行おうとすると、同じページ内で認証後のセッションcookieありのリクエストとセッションcookieなしのリクエストが混じって扱いにくそうなので、Deviseの標準では対応しにくいかも」「たしかに」「ログアウトについても同じようなことが考えられますね」

参考: 非同期関数 - JavaScript | MDN

🔗 Railsリポジトリにあるrequest.js


つっつきボイス:「お便りいただきました😂」「お、rails/request.jsというのが一応標準であるんですね」「これでAjaxリクエスト時にCSRFトークンを含められるそうです」「ページのmetaタグにあるCSRFを取り出して使うのかな: 知ってたら使うかも👍」

rails/request.js - GitHub

🔗 Railsでカウンタキャッシュを使う前に試したい4つの方法(Ruby Weeklyより)


つっつきボイス:「記事は見出しどおりの内容かな: Rails標準のcounter_cacheはだいぶ昔からありますが、あくまで自前で実装するよりは便利という程度の機能なので、これをベストプラクティスと信じ込まない方がいいと思います」「なるほど」

参考: §4.1.2.3 :counter_cache -- Active Record の関連付け - Railsガイド

Rails向け高機能カウンタキャッシュ gem「counter_culture」README(翻訳)

🔗Ruby

🔗 bundler-alive: Gemfile.lockのgemをチェック

kyoshidajp/bundler-alive - GitHub


つっつきボイス:「ruby-jp Slackで見かけたgemです」「なるほど、Gemfile.lockに記載されているgemがリポジトリから取得可能かどうかをチェックするんですね、欲しくなる気持ちわかる👍」

「gemが消えていることってたまにありますね」「あるバージョン以前のmimemagic gemが諸事情で消されたことは以前も話題にしましたね(ウォッチ20210329)」

参考: mimemagicの最新動向 - HackMD

🔗 Rubyのfilter_map


つっつきボイス:「短い記事です」「Ruby 2.7から入ったfilter_mapだと簡潔に書ける、そうそう」「RuboCopでもfilter_map使えって言われるぐらいRubyらしい書き方」

# 同記事より
>> [1, 2, 3, 4, 5, 6].filter_map { |i| i * 2 if i.even? }
#=> [4, 8, 12]

参考: Enumerable#filter_map (Ruby 3.1 リファレンスマニュアル)
参考: Performance/MapCompact -- Performance :: RuboCop Docs

「kazzさんといえば以下の記事ですが、きっとfilter_map好きですよね?」「まだあまり使ってないけど好き❤️」

Ruby: eachよりもmapなどのコレクションを積極的に使おう(社内勉強会)

🔗 Rubyのdig

参考: Hash#dig (Ruby 3.1 リファレンスマニュアル)
参考: Array#dig (Ruby 3.1 リファレンスマニュアル)
参考: Struct#dig (Ruby 3.1 リファレンスマニュアル)


つっつきボイス:「上のissueはまだオープン中ですが、エラーをraiseするバージョンのdigを提案しています」「Hash#digとかはキーが見つからなくてもエラーを出さないところが便利なので、dig!的な別メソッドみたいなオプションとして追加されるならいいかな」

🔗 その他Ruby

つっつきボイス:「小ネタですが、上のRubyコントリビューションガイドをruby/debugでおなじみの@_st0012さんが他の方と共同でがっつり書き直したそうです」「こういうのは最新になっていることが重要ですね👍」

参考: contributing - Documentation for Ruby 3.2


「変数名に?を付けたくなる気持ち、ちょっとわかるかも」「自分は思わないなぁ」

「今のRubyは識別子に?が付いていたら必ずメソッド名だとわかるから、?が付いているのに変数だと戸惑いそう」「Rubyの識別子は1種類じゃないということなんでしょうか?」「そう考えてみると、?がメソッド名には使えるのに変数名には使えない歴史的な理由があるのかも」「探せばありそうですね」

「ここは想像ですが、変数の方が使える文字が多いのは、たとえば代入の=演算子は変数名の直後にスペースなしで置けないけどメソッド名の直後には置けるからかもしれませんね」

参考: 字句構造 (Ruby 3.1 リファレンスマニュアル)


今回は以上です。

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

週刊Railsウォッチ: Railsコアチームとコミッターに新メンバー、ruby-buildでのRust YJITサポートほか(20220524後編)

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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