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

週刊Railsウォッチ: Active Recordのattribute更新系メソッド比較リスト、Railsリファクタリングガイドほか(20220425前編)

こんにちは、hachi8833です。RuboCopが10歳になりました🎉

週刊Railsウォッチについて

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

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

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

今回の改修はかなり件数が少なかったので範囲を広げて見繕いました。小粒揃いです。

🔗 config.enable_reloadingを追加

config.cache_classesを設定するたびに、この設定が正しいかどうかいちいち考え込んでしまう。無駄に考えさせる設定。
一方、config.reload = trueという記述を見れば、アプリケーションがリロードされるであろうことは考えるまでもなく一瞬でわかる。
人は「リロードは有効なのか無効なのか?」と機能に沿って考えるものだ。しかもRubyプログラムでは、クラスが「キャッシュされる」という言い方はしない。必要なのは、カスタムのリロード機能を表す語だ。
このリファクタリングについて解説する。

unless config.cache_classes && config.eager_load    ⇔ (unlessは「if !」と同じ)
    if !(config.cache_classes && config.eager_load) ⇔ (!(A && B) は !A || !Bと同じ) 
    if !config.cache_classes || !config.eager_load  ⇔ (!config.cache_classes は config.reload と同じ)
    if config.reload || !config.eager_load

config.cache_classesはずっと前からあるので、今さら非推奨化するのは実用的ではないと思う。それよりも、新しいアプリを生成するとこの新しいconfig.reloadを使うようにし、これが現在の推奨フラグであることをドキュメントに書いて推奨しつつ、config.cache_classesを非推奨化せずに引き続きサポートすることを提案したい。Rails 8になったら非推奨サイクルを開始してもいいかもしれないが、この点は議論の余地がある。
同PRより


つっつきボイス:「config.cache_classesというコンフィグの意味がわかりにくいので、config.enable_reloadingというコンフィグを追加したらしい」「!config.cache_classesと同等なんですね」「たしかにconfig.enable_reloadingの方がいい名前👍」

より直感的な名前にするために、config.enable_reloading!config.cache_classesと同じ)が定義された。今後はconfig.enable_reloadingconfig.reloading_enabled?が推奨されるが、後方互換性のためconfig.cache_classesも引き続きサポートする。
Xavier Noria
同Changelogより

🔗 config.enable_dependency_loadingが非推奨化

config.enable_dependency_loadingを非推奨化。このフラグは(Zeitwerk以前の)classicオートローダーの制約を正すためのものだが現在は無効。この非推奨化メッセージを修正するために、この参照を削除して欲しい。
Xavier Noria
同PRより

# railties/lib/rails/application/configuration.rb#L300
+     ENABLE_DEPENDENCY_LOADING_WARNING = <<~MSG
+       This flag addressed a limitation of the `classic` autoloader and has no effect nowadays.
+       To fix this deprecation, please just delete the reference.
+     MSG
+     private_constant :ENABLE_DEPENDENCY_LOADING_WARNING
+
+     def enable_dependency_loading
+       ActiveSupport::Deprecation.warn(ENABLE_DEPENDENCY_LOADING_WARNING)
+       @enable_dependency_loading
+     end
+
+     def enable_dependency_loading=(value)
+       ActiveSupport::Deprecation.warn(ENABLE_DEPENDENCY_LOADING_WARNING)
+       @enable_dependency_loading = value
+     end

つっつきボイス:「上に続いてZeitwerkの@fxnさんによるコミットです」「これもRailsのオートロード関連ですね」

参考: Classic から Zeitwerk への移行 - Railsガイド

🔗 ドキュメント: host_configurationhost_authorizationに修正

# guides/source/configuring.md#L509
You can exclude certain requests from Host Authorization checks by setting
-`config.host_configuration.exclude`:
+`config.host_authorization.exclude`:

# Exclude requests for the /healthcheck/ path from host checking
-Rails.application.config.host_configuration = {
+Rails.application.config.host_authorization = {
  exclude: ->(request) { request.path =~ /healthcheck/ }
}

When a request comes to an unauthorized host, a default Rack application
will run and respond with `403 Forbidden`. This can be customized by setting
-`config.host_configuration.response_app`. For example:
+`config.host_authorization.response_app`. For example:

-Rails.application.config.host_configuration = {
+Rails.application.config.host_authorization = {
  response_app: -> env do
    [400, { "Content-Type" => "text/plain" }, ["Bad Request"]]
  end
}

つっつきボイス:「英語版のRails Guidesの設定パラメータが間違っていたのが修正されたのか」「host_configurationというコンフィグは見たことないですね」「早速Railsガイドにも修正プルリクを投げました↓」

参考: configuring.mdを修正 by hachi8833 · Pull Request #1233 · yasslab/railsguides.jp -- マージ済み

🔗 ドキュメント: primary_keyに関する不要な記述を削除

# guides/source/active_record_migrations.md#L354
By default, `create_table` will create a primary key called `id`. You can change
-the name of the primary key with the `:primary_key` option (don't forget to
-update the corresponding model) or, if you don't want a primary key at all, you
-can pass the option `id: false`. If you need to pass database specific options
-you can place an SQL fragment in the `:options` option. For example:
+the name of the primary key with the `:primary_key` option  or, if you don't
+want a primary key at all, you can pass the option `id: false`. If you need to
+pass database specific options you can place an SQL fragment in the `:options`
+option. For example:

つっつきボイス:「これもドキュメント修正で、@pockeさんが英語と合わせて日本語↓も修正してくださいました🙇」「ありがたい🙏」

参考: Remove unnecessary note for primary_key in AR migration guide by pocke · Pull Request #1232 · yasslab/railsguides.jp

🔗 issue: マルチプルDBでnon-primary DBのマイグレーションのステータスがdownでもActiveRecord::PendingMigrationErrorが発生しない

再現手順
マルチプルDBでprimary以外のDBマイグレーションのステータスがdownでもActiveRecord::PendingMigrationErrorが発生しない。

# config/database.yml
development:
  backbone:
    <<: *default
    database: backbone_development
  library:
    <<: *default
    database: library_development
    migrations_paths: db/library_migrate

以下を実行してhttp://localhost:3000/users か http://localhost:3000/books を開く。

bin/rails g scaffold User title
bin/rails g scaffold Book title --db library
bin/rails db:create db:migrate
bin/rails s
# 別のコンソール
bin/rails g migration add_published_at_to_books published_at:datetime --db library

期待される動作
add_published_at_to_booksマイグレーションのステータスがdownなのでActiveRecord::PendingMigrationErrorが発生する。

$ bin/rails db:migrate:status

database: backbone_development
 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20191022014341  Create users

database: library_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20191022023152  Create books
  down    20191022023755  Add published at to books
$ rails db:abort_if_pending_migrations
You have 1 pending migration:
  20191022023755 AddPublishedAtToBooks
Run `rails db:migrate` to update your database then try again.

実際の動作
エラーが発生しない。
システム構成
Rails 6.0.0
Ruby 2.6.5
同issueより


つっつきボイス:「あ〜、たしかにnon-primaryのDBでマイグレーションがpendingなのにPendingMigrationErrorエラーが出ないのはつらい」「まだ修正プルリクは出ていないみたい」「ちなみに以下のマイルストーンで見つけたissueです↓」

参考: 7.0.2 Milestone

「このissueは発生の条件が少し厳しくて、マルチプルデータベースでprimaryとnon-primaryの両方を同じRailsリポジトリで管理しているときに発生するっぽい」「なるほど」「マルチプルデータベースを本格的に使っていないとなかなか踏まないバグかも」

参考: Active Record で複数のデータベース利用 - Railsガイド

「DBのロールバックって普段あまりやらないかも」「マイグレーションがどのぐらいちゃんと書かれているか調べないとロールバックするのは怖いです」「ロールバックする可能性がありそうな箇所なら時間をかけてでも検証しますけどね」

🔗Rails

🔗 GitHubが行ったRailsのパフォーマンス改善(Ruby Weeklyより)


つっつきボイス:「へ〜、rack.after_replyなんてのがあるのね」「rack.after_replyは元々Pumaの拡張で、リクエストが完了したあとでコールバックを呼び出す機能らしい」

puma/puma - GitHub

「GitHubはWebサーバーにPumaではなくUnicornを使ってると書かれてる」「そういえばGitHubはUnicornをマルチスレッド・マルチプロセスで使っているという話を昔見たような気がしますけど、今もUnicornを使ってるんですね」「Pumaのrack.after_replyを使いたいけどUnicornには実装されていなかったので、Unicornにパッチを投げたそうです」「その気持わかる」「GitHubは技術ブログを着実に更新していていいですね👍」

defunkt/unicorn - GitHub

# 同記事より(改行を削減)
class AfterResponse
  def initialize(env)
    env[“rack.after_reply”] ||= []
    env[“rack.after_reply”] << -> do
      self.call
    end

    @to_perform = []

  end

  # Calls each callable defined via #perform
  def call
    @to_perform.each do |block|
      begin
        block.call(self)
      rescue Object => e
        Rails.logger.error(e)
      end
    end
  end

  # Adds given block to the array of callables that will
  # be called after the user has received the response.
  def perform(name, &block)
    @to_perform << block
  end
end

🔗 Rails 7版: Active Recordのさまざまなattribute更新系メソッドの比較リスト(Ruby Weeklyより)


つっつきボイス:「お、定番のActive Record attribute更新系メソッドの挙動比較表」「コールバックを呼ぶものや呼ばないものかあったりしますね」


同記事より

upsert_allはタイムスタンプがセットされないみたいな点は注意が必要」「そうそう、コールバックとタイムスタンプ更新は連動するので、コールバックが呼ばれないメソッドではActiveRecord::Timestampが呼ばれない」「たしかに」「この表を丸覚えするよりは、挙動を理解する方が覚えやすいでしょうね: いい情報👍」

「Rails 6以前向けの記事リンクも記載されていました↓」「こういう記事はこまめに更新しないといけないのが大変」「たまに記事が間違ってたり古かったりするとハマりますよね」

assign_attributesattributes=のエイリアスなのね」

参考: ActiveModel::AttributeAssignment

「そういえばQiitaにも以前似たような記事があった気がします」「そうそう、これですね↓」

参考: ActiveRecord の attribute 更新方法まとめ - Qiita


そういえばupdate_attributesupdate_attributes!はRails 6.1でそれぞれupdateupdate!に移行したのでした↓。

参考: Deprecate update_attributes and update_attributes! by elebow · Pull Request #31998 · rails/rails
参考: Rails 6 deprecates update_attributes and update_attributes! | Saeloun Blog

🔗 Railsリファクタリングガイド


つっつきボイス:「リファクタリング記事は定番ですね」「慣れていない人は読んでおくといいと思います👍」

「『リファクタリング時にspecも一緒に作る』、その通り!」「そうそう」「何をするにもspecを揃えてから作業すべき」

「テストがないコードは、そもそもテストしにくかったりできなかったりすることもよくある」「その場合は、まずテストを書いて正しく動くようにコードを改修するところから始めないといけない」「ああ、それつらいヤツです」

「この記事を書いた方はフリーランスのエンジニアなのか」「フリーランスの場合、プロパーとしてチームに常駐する人とはアプローチが異なってくる面もありますね: プロジェクトのメンバーや特性などでも変わってきますが」

「フリーランスの場合、最初の1か月である程度の成果を出すことを求められることもよくあると思いますけど、足りないテストを増やすというタスクは、そういうときに少しずつでも成果を出せるのが嬉しい」「少なくともテストを増やすことで既存のコードが悪くなる心配はないですよね」「既存プロジェクトへの途中参加案件で直近取り組める手頃なサイズのタスクが無い場合に、進捗を刻んで開発の流れに乗っていくためにまずテストを書くことを提案することもあります」

🔗 環境変数でテストが壊れやすくなる問題(Ruby Weeklyより)


つっつきボイス:「環境変数で挙動が変わる部分のテストを書いたら思わぬ挙動をしたという記事だそうです」「近年は環境変数でアプリの挙動を変えることが増えていますけど、どこまで環境変数でやるのかは悩ましいところ」「そうそう」

「記事では、問題を起こす環境変数をメソッドで渡すように変えることで、環境変数によって挙動が変わらないようにしたんですね」

「たとえば外部APIに依存するアプリで、APIをスタブモードに切り替えるのに環境変数を使ったりしますよね」「APIが有料だったり、顧客のプライベートなネットワークでしかアクセスできないような場合は、そうやって環境変数で切り替わるようにしないとローカルで開発できない」「ついこの間まさにそういうコードを書きました」「おそらくダミーのAPIサーバーを立ててそちらにアクセスする方がいいんでしょうけど、工数が増える」「そうなんですよ」「環境変数で挙動が変わる部分はテストを書いておきたい」

「ちなみに今やっているプロジェクトの環境変数は、開発環境向けのものなので数は抑えているんですが、それでもかなり多い」「そうなりますよね」


追いかけボイス: 「RSpecのbefore do の中でENV['hoge'] = 'piyo'のようにENVを設定してしまうと、ENVの値はafterで元の値に復元されたりunsetされたりしないので、テストの実行順序が変わると落ちるようになってしまうことがあります: これを回避するにはbeforeで環境変数を一時退避してafterで戻すなどの対応が必要になりますが、マルチスレッドが絡むとさらにおかしくなることもあります」

🔗 Webpackerからimportmap-railsへの移行

つっつきボイス:「Webpackerからimportmap-rails、大変そう」「疲れさまです」「ぼくも同じような記事書いたんでした↓」「こういう知見はありがたい👍」

「大規模なRailsアプリでimportmapを使っているところはまだ少なそう」「Webpackerはやめたけどwebpackは残しているところも多いんじゃないかな」「webpackを剥がすのは大変だと思います」

Rails 7: importmap-rails gem README(翻訳)

🔗 その他Rails

つっつきボイス:「古いRubyを使わないといけないときとかでつらいヤツだ」「compat_openssl的なものは出ないんだろうか」「そういうのがないとRuby以外の言語でも困りそうですし、公式でなくても誰か作ると思いたい」「自分たちがUbuntu 22.04にアップデートするのはもう少し先になると思いますが、あって欲しい」

参考: Ubuntu 22.04 その3 - OpenSSL 3.0への移行計画 - kledgeb

「Dockerを使うときにUbuntuのバージョン指定をlatestにしている人は要注意ですね」「う、latest普通に使っちゃってます😅」「latestにするとアップデートで死ぬのでバッドプラクティスです、マジで」「そうでした、ちゃんとバージョン指定しなきゃ...」「この分だとしばらくの間あちこちで悲鳴が上がるかも」

参考: Ubuntu - Official Image | Docker Hub -- latestは既に22.04になっています


前編は以上です。

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

週刊Railsウォッチ: RubyのGCコンパクション改修、jemalloc、ReDoSの自動検出修正ほか(20220419後編)

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines


CONTACT

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