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

週刊Railsウォッチ: Devise 4.9のHotwire/Turbo統合に対応する、英国政府のViewComponentほか(20230314前編)

こんにちは、hachi8833です。Ruby 2.7のメンテナンス終了が近づいていますね。

参考: Ruby Maintenance Branches

週刊Railsウォッチについて

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

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

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

🔗 複数ジョブを一度にエンキューするperform_all_laterが追加

複数のジョブやジョブの配列を以下のように渡すことで、コールバックを実行せずにジョブをバルクエンキューする機能が追加される。

ActiveJob.perform_all_later(MyJob.new("hello", 42), MyJob.new("world", 0))
user_jobs = User.pluck(:id).map { |id| UserJob.new(user_id: id) }
ActiveJob.perform_all_later(user_jobs)

これにより、キューデータストアへのラウンドトリップ回数を大幅に削減できる。
新しいenqueue_allメソッドを実装していないキューアダプタでは、個別にジョブをエンキューする方法にフォールバックする。なお、Sidekiqアダプタはpush_bulkenqueue_allを実装している。

このメソッドは既存のenqueue.active_jobイベントを利用せず、enqueue_all.active_jobという新しいイベントを追加する。
Sander Verdonschot
同Changelogより


つっつきボイス:「これはActive Jobの改修ですね」「多数のマイクロジョブみたいなものを一度にエンキューする機能はいい👍: この改修ではSidekiqのアダプタにenqueue_allメソッドを追加してpush_bulkを呼んでいるけど、おそらくpush_bulkの中でRedisにJobを保存する部分でRedisのbulk put系命令を使うようになっているんでしょうね」

# activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb#L35
      def enqueue_all(jobs) # :nodoc:
        jobs.group_by(&:class).each do |job_class, same_class_jobs|
          same_class_jobs.group_by(&:queue_name).each do |queue, same_class_and_queue_jobs|
            immediate_jobs, scheduled_jobs = same_class_and_queue_jobs.partition { |job| job.scheduled_at.nil? }

            if immediate_jobs.any?
              Sidekiq::Client.push_bulk(
                "class" => JobWrapper,
                "wrapped" => job_class,
                "queue" => queue,
                "args" => immediate_jobs.map { |job| [job.serialize] },
              )
            end

            if scheduled_jobs.any?
              Sidekiq::Client.push_bulk(
                "class" => JobWrapper,
                "wrapped" => job_class,
                "queue" => queue,
                "args" => scheduled_jobs.map { |job| [job.serialize] },
                "at" => scheduled_jobs.map { |job| job.scheduled_at }
              )
            end
          end
        end
      end

「この翻訳記事↓でも書かれているように、Active Job経由だとActive Jobがサポートする機能しか使えないので、バックエンドで使っているSidekiqの機能をすべて使えるわけではありませんが、こうした改善によってより効率的な処理をサポートするための機能が少しずつ増えてきている感じですね」

Rails: SidekiqはActive Jobを経由せずに直接使おう(翻訳)

🔗 デフォルトのカラムシリアライザを定義可能になった

YAMLは何かと自分の足を撃ち抜きがちなので、他の何かで代替可能にしておくか、いっそシンプルにあらゆるシリアライズドカラムで明示的にシリアライザをユーザーに強制定義させるぐらいの方が望ましい。

このプルリクは#47422の縮小版で、デフォルトのシリアライザを設定可能にするもの。デフォルトのシリアライザを変更すべきかどうか(そしてデフォルトのシリアライザをどれにすべきか)についてはその後で議論可能。
同PRより


つっつきボイス:「割と変更が多いですね」「YAMLがカラムシリアライザのデフォルトなのは良くないという話はその通りだと思います」「この改修の前に#47422でYAMLをデフォルトから外してたんですね」

#47422ではデフォルトのカラムシリアライザを7.1からnilに変更している↓: 最終的にはデフォルトのカラムシリアライザをJSONに変更したいのかなと思ったけど、#47422のプルリクメッセージにはJSONも難しいだろうと書かれているので、7.1では先にYAMLをデフォルトから外してシリアライザを明示的に指定する形にしたということなんでしょうね」

# #47422より
# guides/source/configuring.md#L1272
#### `config.active_record.default_column_serializer`
The serializer implementation to use if none is explicitly specified for a given
column.

-`serialize` and `store` while allowing to use alternative serializer
-implementations, use `YAML` by default, but it's not a very efficient format
+Historically `serialize` and `store` while allowing to use alternative serializer
+implementations, would use `YAML` by default, but it's not a very efficient format
+and can be the source of security vulnerabilities if not carefully employed.

As such it is recommended to prefer stricter, more limited formats for database
serialization.

+Unfortunately there isn't really any suitable defaults available in Ruby's standard
+library. `JSON` could work as a format, but the `json` gems will cast unsupported
+types to strings which may lead to bugs.
+
+The default value depends on the `config.load_defaults` target version:
+
| Starting with version | The default value is |
| --------------------- | -------------------- |
| (original)            | `YAML`               |
+| 7.1                   | `nil`                |

「今回の#47463の改修は、従来の書き方にdeprecation warningを出して7.1のbreaking changeに備えつつ、デフォルトをカスタマイズ可能にしたようですね: serializecoder:というキーワード引数を追加して、デフォルトのカラムシリアライザをconfig.active_record.default_column_serializerコンフィグでカスタマイズ可能にする」「ところでYAMLの場合は元々yaml:オプションも渡せるようになってたんですね」

# activerecord/lib/active_record/attribute_methods/serialization.rb#L156
        #   class User < ActiveRecord::Base
-       #     serialize :preferences, yaml: { permitted_classes: [Symbol, Time] }
+       #     serialize :preferences, coder: YAML, yaml: { permitted_classes: [Symbol, Time] }
        #   end

🔗 deliver_laterのジョブキュー名をメーラーごとにカスタマイズ可能になった

deliver_later_queue_nameは既にActionMailer::Baseでカスタマイズ可能。その値がすべてのサブクラスに継承される。クラスの継承可能な属性を代わりに使うことでサブクラスでオーバーライド可能にする。

これは、コンカレンシー制御で名前付きクエリに名前を使っている場合に便利。たとえば、多数のワーカーで処理中のキューは大量のメールを同時配信可能だが、それが望ましくない場合もあるだろう。多数のメールの同時配信数が多過ぎると、メールプロバイダで速度を制限されたりブロックされたりする可能性がある。これを防止する方法のひとつは、ワーカー数を減らすか1個にすること。
このプルリクは、デフォルトの配信ジョブが利用するキューを以下のようにメーラーごとに設定可能にすることで、これをもっと手軽に行えるようにする。

class AnnouncementsMailer < ApplicationMailer
  self.deliver_later_queue_name = :throttled_mailer
vend

参考: Allow configuration of ActionMailer queue name #18587 (comment)
同PRより


つっつきボイス:「今まではdeliver_laterのジョブキュー名をconfig.action_mailer.deliver_later_queue_nameで設定していたのでサブクラスごとに変えられなかったんですね: 使いたい機能👍」

参考: § 3.13.15 config.action_mailer.deliver_later_queue_name
-- Rails アプリケーションを設定する - Railsガイド

🔗 database.ymlのshared設定を改善

動機/背景
従来のsharedコンフィグハッシュは、database.ymlで定義されているすべてのデータベース設定とマージされるようになっていた。

詳細
このコミットは、sharedハッシュを以下のように3階層に対応させ、設定名が一致する場合にのみ環境間で設定を共有できるようにした。

shared:
  one:
    migrations_path: "db/one"
  two:
    migrations_path: "db/two"

development:
 one:
    adapter: sqlite3
 two:
    adapter: sqlite3

production:
  one:
    adapter: mysql2
  two:
    adapter: mysql2

これによって、development環境とproduction環境の両方で、設定onetwomigration_pathsが正しく設定されるようになる。

追加情報
修正: #47367

cc @eileencodes
cc @matthewd#28896のコメントでこれを思いついていたので)
同PRより


つっつきボイス:「そもそもRailsにはRails::Application#config_forにYAMLを読み込む機能が実装されているんですが、ここにsharedをルート要素のキーとするエントリがあると、全環境にデフォルトでその設定をmergeするという機能があります」

参考: Rails API config_for -- Rails::Application

「database.ymlも同じ思想で、sharedキーを持っていると全環境の共通設定として書けるんですが、これが3階層以上(各環境内の設定値としては2階層以上)になると、個別環境で設定を書くとmergeではなく上書きされてしまうという問題があったようです」
「これは、database.ymlの処理はRails::Application#config_forの処理とは別のコードとして実装されていた(#database_configuration)ことに起因していて、これまではRails::Application#config_forとdatabase.ymlの読み込み仕様において、sharedキーの扱いが異なる状態だったようです」

参考: database_configuration -- Rails::Application::Configuration

「このプルリクではこれを解消して、database.ymlのsharedについても階層のある設定を上書きではなくmergeするようにすることで解決した、ということのように見えます」


「なお、なぜこのsharedのようなキーが存在するかについては、恐らくはYAMLパーサーであるpsychのデフォルトのsafe_loadがYAMLエイリアス機能(&defaultsなどで使う)が無効化されてしまったことが背景にあるのではないかと思われます」

ruby/psych - GitHub

「環境別の設定は共通項目も多いので、エイリアス機能がないと大量の同じ行をコピペすることになってしまうため不便です: そのため、YAMLエイリアスは使わないけどsharedという特別なキーを導入し、config読み込み機能側で特別な処理をさせることでセキュリティと利便性のバランスを取ったのだろうと想像しています(出典までは探れなかったので間違っていたらごめんなさい)」

🔗 新規アプリのテンプレートにconfig.hostsconfig.host_authorizationがコメントアウト状態で追加されるようになった

5c830a8/upエンドポイントが追加されてロードバランサーやアップタイムモニタで便利に使えるようになったが、たまにDNSリバインディングに邪魔されることがある。hostshost_authorization設定で対応できることを示しておけばスムーズに対応しやすいだろう。
同PRより


つっつきボイス:「設定を探さずに対応できるようになるのは便利👍」

# railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt#L103
  # DNSリバインディングやその他の`Host`ヘッダー攻撃からの保護を有効にする設定:
  # config.hosts = [
  #   "example.com",     # Allow requests from example.com
  #   /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
  # ]
  # デフォルトのヘルスチェックエンドポイントでDNSリバインディング保護をスキップする設定:
  # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end

参考: §3.5.1 ActionDispatch::HostAuthorization -- Rails アプリケーションを設定する - Railsガイド

🔗 Rails Application Templatesガイド


つっつきボイス:「改修そのものはRails Guides Webサイトの目次追加だけど、Rails Application Templatesガイドの作成が進行中なんですね」「なるほど」

# guides/source/documents.yaml#L289
+   -
+     name: Rails Application Templates
+     url: rails_application_templates.html
+     description: >
+       Application templates are simple Ruby files containing DSL for adding
+       gems, initializers, etc. to your freshly created Rails project or an
+       existing Rails project.
+     work_in_progress: true

「Edgeガイドで進行中のガイドが読めますね↓」

参考: Rails Application Templates — Ruby on Rails Guides

🔗 番外: コピーライト表記の年号を削除

Railsも、curlみたいにライセンスから日付を削除すべき。何かに役立つわけでもないし、毎年この日付を更新するためにプルリクを作成しないといけない。
例: #46866#44039#40991#34831#31606#27523、とまだまだ続く(これで多くのプログラマーが何年間助かるだろうか...)😂


つっつきボイス:「著作権表記の年号ってそういえば法的な意味がよくわからないですね」「ネットにもいろいろ情報はあるけど、不安であれば著作権法に詳しい弁護士などの専門家に相談してみるべきでしょうね」

🔗Rails

🔗 Devise 4.9をインストールしてRails 7.0 (Hotwire/Turbo)に対応する


つっつきボイス:「jnchitoさんがDevise 4.9でのHotwire/Turbo対応を少し前から調べていて記事を公開していたのですが、4.9の正式リリースに合わせて記述を更新したそうです」「これはありがたい!」「元々以下の呼びかけ↓に応えた記事だそうです」

「ちなみにjnchitoさんは4.9へのアップグレードガイドも翻訳しています↓」

参考: 【日本語訳】How To: Upgrade to Devise 4.9.0 [Hotwire Turbo integration]

🔗 Active Record関連付けのループはfind_eachで(Ruby Weeklyより)


つっつきボイス:「これは定番ですね: primary_keyであるidカラム昇順以外でのORDER BYが効かない点には気をつける必要がありますが(追記 2023/03/16: #30590descも指定できるようになっています)、基本的にeachよりもfind_eachで回す方がいい」

# 同記事より
post.comments.each do |comment|
  # Do stuff with each comment: enqueue a job
end

post.comments.find_each do |comment|
  # Do stuff with each comment: enqueue a job
end

参考: Rails API find_each -- ActiveRecord::Batches

🔗 英国政府が公開しているViewComponentベースのデザインシステム


つっつきボイス:「以下のViewComponent公式ドキュメントで知りました↓」「公的機関がデザインシステムを公開するのは理にかなってますね: ViewComponentコンポーネントの実装まで公開するのは英国政府のWebサイトが公式にRailsを採用しているからできることですが」

参考: Resources | ViewComponent

つっつき後に、日本のデジタル庁もデザインシステムを公開していることを教わりました↓

参考: デザインシステム|デジタル庁

英国政府によるRailsアプリケーションテストの新標準(翻訳)

🔗 その他Rails

つっつきボイス:「Railsガイドでマージ後に自動デプロイできるようになったので、コントリビュートの反映がやりやすくなりました」

「GitHub Actionsは本当に便利ですよね: organizationの切り方を間違えると大変ですが」「というと?」「GitHub Actionsをプロダクトごとにorganizationを分けて無料枠で運用していると、無料枠ではデフォルトのキャッシュ容量に上限があるので、大きなものをデプロイするとキャッシュが足りなくなって困ったことがあります」「ありゃ〜」

参考: Actions | GitHub


前編は以上です。

バックナンバー(2023年度第1四半期)

週刊Railsウォッチ: Ruby30周年記念イベント、37signalsのデプロイツールmrskほか(20230308後編)

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

Rails公式ニュース

Ruby Weekly


CONTACT

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