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

週刊Railsウォッチ: Active Modelのパターンマッチングがいったん取り消し、Ruby技術者認定試験が10月3日から3.xに対応ほか(20220719)

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

今週は月曜日が祝日のため短縮版でお送りします。

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

今回は以下の中から取り上げていなかったものを中心に見繕いました。

🔗 in_batchesを最適化

従来のin_batches編注: 原文each_batchを修正)の実装では、特定のidをpluckしてから、それらをWHERE IN (...ids...)で用いてバッチを生成していた。
このリストはすぐ大きくなりがちで(デフォルトは1000件)、そうなるとクエリの効率が低下したりログや監視ツールのSQLクエリが切り詰められたりすることがある。きっと誰しも画面サイズの半分を占める怪しいクエリを見たことがあるはず。
このプルリクは、クエリをrange(WHERE id >= num1 AND id < num2)でイテレートすることでクエリの効率を高めてコンパクトにする実装を行う。

User.in_batches(of: 3) do |relation|
  puts relation.to_sql; nil
end; nil
--改修前
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3)
SELECT "users".* FROM "users" WHERE "users"."id" IN (4, 5, 6)
SELECT "users".* FROM "users" WHERE "users"."id" IN (7, 8, 9)
SELECT "users".* FROM "users" WHERE "users"."id" IN (10, 11, 12)
SELECT "users".* FROM "users" WHERE "users"."id" IN (13, 14)
--改修後
SELECT "users".* FROM "users" WHERE "users"."id" < 4 ORDER BY "users"."id" ASC LIMIT 3
SELECT "users".* FROM "users" WHERE "users"."id" >= 4 AND "users"."id" < 7 ORDER BY "users"."id" ASC LIMIT 3
SELECT "users".* FROM "users" WHERE "users"."id" >= 7 AND "users"."id" < 10 ORDER BY "users"."id" ASC LIMIT 3
SELECT "users".* FROM "users" WHERE "users"."id" >= 10 AND "users"."id" < 13 ORDER BY "users"."id" ASC LIMIT 3
SELECT "users".* FROM "users" WHERE "users"."id" >= 13 ORDER BY "users"."id" ASC LIMIT 3

この手法は自分のgemで既に使っていて非常にうまくいっている。
このパッチを実装した後で、これと似たような#42695があることに気がついたが、そこまでうまく動いていない様子("改修前"のようなクエリがまだ生成される)。個人的には自分の実装はよりシンプルにできたと思っている。そういった流れでこのプルリクをオープンした。
同PRより


つっつきボイス:「先週も見たような気がするプルリクだけど、先週のはin_batchesuse_ranges: trueを指定できる改善でしたね(ウォッチ20220711): こちらは従来のin_batchesfind_in_batchesがSQLのINでidのリストを渡す方式だったのをWHERE id >= num1 AND id < num2のようなrangeに変えている」「先週と同じ方がこのプルリクも出してるんですね」

参考: Rails API in_batches -- ActiveRecord::Batches
参考: Rails API find_in_batches -- ActiveRecord::Batches

🔗 app:update実行時に機能がconfig/application.rbで無効になっているかをチェックするようになった


つっつきボイス:「RailsでActive MailboxやAction Textのような機能をrequireしていないならapp:updateの対象にする必要はない、たしかに」「ですよね」

🔗 in_batchesにブロックを渡さずに降順を指定できるようになった

#30590の続き。
#30590ではバッチを降順にする機能が導入されたが、in_batchesにブロックを渡さずに同じことをする機能がサポートされていなかった(おそらくコレクションの省略がサポートされていなかったためと思われる)。

  • in_batchesにブロックを渡す場合: 問題なく動作する
# Post Pluck (0.1ms)  SELECT "posts"."id" FROM "posts" ORDER BY "posts"."id" DESC LIMIT ?  [["LIMIT", 1000]]
Post.in_batches(order: :desc) {}
  • in_batchesにブロックを渡さない場合
# 変更前: DESCを期待したがASCになる
# Post Pluck (0.1ms)  SELECT "posts"."id" FROM "posts" ORDER BY "posts"."id" ASC LIMIT ?  [["LIMIT", 1000]]
Post.in_batches(order: :desc).each {}
# 変更後
# Post Pluck (0.1ms)  SELECT "posts"."id" FROM "posts" ORDER BY "posts"."id" DESC LIMIT ?  [["LIMIT", 1000]]
Post.in_batches(order: :desc).each {}

同PRより


つっつきボイス:「これもin_batchesの改善」「ブロックを渡さないとorder: :descが効かなかったのを効くようにしたんですね」「今までASCになっちゃってたのか」「気づかなかったら事故りかねないヤツ」

🔗 空のデータベースが存在していてもdb:prepareでスキーマを読み込み可能にした

  • db:prepareタスクを更新して、初期化されていないデータベースが存在する場合にスキーマを読み込み、マイグレーションの後でスキーマをダンプするようにした。
    Ben Sheldon
    同PRより
# activerecord/lib/active_record/tasks/database_tasks.rb#L188
      def prepare_all
        seed = false
        each_current_configuration(env) do |db_config|
          ActiveRecord::Base.establish_connection(db_config)

          begin
-           # Skipped when no database
-           migrate
-
-           if ActiveRecord.dump_schema_after_migration
-             dump_schema(db_config, ActiveRecord.schema_format)
-           end
+           database_initialized = ActiveRecord::SchemaMigration.table_exists?
          rescue ActiveRecord::NoDatabaseError
            create(db_config)
+           retry
+         end

+         unless database_initialized
            if File.exist?(schema_dump_path(db_config))
              load_schema(
                db_config,
                ActiveRecord.schema_format,
                nil
              )
-           else
-             migrate
            end
-
            seed = true
          end
+
+         migrate
+         dump_schema(db_config) if ActiveRecord.dump_schema_after_migration
        end

つっつきボイス:「この機能が欲しいのはわかる」「空のデータベースがあってもdb:prepareしたいときがあるんでしょうか?」「プルリクにもあるように、HerokuなどのPaaS環境を使う場合には事前に空のDBだけが作られていて、db:create相当の操作をRails側から行えないケースがあるので、そういうときに欲しい機能でしょうね」

🔗 credentialの改良2つ


つっつきボイス:「2つのプルリクはどちらもcredential関連でした」

「1つ目は、credentialのカスタムテンプレートをアプリに置けるようになったんですね: 通常だとcredentialにはsecret_key_baseぐらいしか含まれなかったと思うけど、そのアプリで使うcredential項目が他にもいろいろある場合はテンプレート化したい気持ちはわかる」「プルリクにもあるように、オープンソースのRailsアプリのセットアップがやりやすくなりそうですね」

このコミットは、アプリ内でカスタムcredentialテンプレートのサポートを追加する。credentialファイルが存在しない場合はrails credentials:editでlib/templates/rails/credentials/credentials.yml.ttでcredentialファイルの生成を試みてからデフォルトのテンプレートにフォールバックする。
これにより、たとえばオープンソースのRailsアプリ(リポジトリにcredentialファイルが含まれていない)にcredentialのテンプレートを含められるようになり、そのアプリをインストールするユーザーがcredentials:editを実行するとカスタムの記入済みcredentialファイルを得られるようになる。
#45544より

「2つ目はそれに関連して、credentialを生成するときにsecret_key_baseを常に含めるようにしたようですね」

現在はCredentialsGeneratorによってconfig/credentials.yml.encが生成されるときに利便性のためにsecret_key_baseが含まれる。しかしconfig/credentials/#{environment}.yml.encは別のジェネレータ(EncryptedFileGenerator)が生成するのでsecret_key_baseが含まれなかった。
このコミットはCredentialsGeneratorをよりジェネレータ的に改修してrails credentials:editでconfig/credentials.yml.encとconfig/credentials/#{environment}.yml.encを両方生成するようにすることで、常にどちらにもsecret_key_baseが含まれるようになる。
#45543より

🔗 Active Modelのパターンマッチングのマージがいったん取り消された


つっつきボイス:「今週のRailsのChangelogをチェックしていて、珍しく削除されている部分があったので見てみると、みんな大好きActive Modelのパターンマッチング(ウォッチ20220516)がいったん取り消されていました」「ありゃ〜残念」「パターンマッチングの書き方はあまりに便利なので、一度リリースされたら相当乱用されそうではある」「言われてみれば、一度使われてしまったら機能の修正が必要なときに大変そう」

「パターンマッチを使うときれいに書けるケースが多いと思うので、きっと広く使われるでしょうね」「強力な機能だけに慎重にやるのがいいのかも」「パターンマッチングでやんちゃされる前に議論を尽くしておくのはいい方向: 英断だと思います👍」

#45553のコメントを見ると戻り値周りなんかが懸念されているらしい↓」「150%って凄い」

このあたりを考えるうえでのコンテキストを@kddnewtonに提供しておく。
このAPIを正しいものにするチャンスは1度きりしかなく、ひとたび使えるようになったら変更しようがない。非推奨サイクルで戻り値を変更することもできないし、Ruby組み込み機能なのでリネームや置き換えサイクルも適用できない。
そういうわけで、特定の振る舞いを固める前に150%正しい値を返せるようにしておく必要がある。
自分は、この機能で期待される動作についてはまだコミュニティで共通認識を得られる段階ではないと思うし、私たちがこの機能を選択することでこの感情を定義できる立場にいるわけでもないと思う。率直に言えば、私たちの立場を利用してRubyの新しい言語機能を成功させたい気持ちはあるものの、一度決定したAPIを変えられないことを考慮すると、早期に飛びつくのはデメリットが大きいと思う。
具体的には、この機能で_read_attribute#[]などを、あるいはsendを使うべきなのだろうか?モデルのアクセサをオーバーライドする機能は、ドキュメント化されているpublic APIに含まれている。オーバーライドを尊重する実装とオーバーライドをバイパスする実装のどちらもあり、それだけで検討が難しくなるが、#45553で提案されている属性と関連付けのミックスとマッチは、いずれにしろ成り立たなさそうに思える。
https://github.com/rails/rails/pull/45553#issuecomment-1179568855(by matthewd)より

「別プルリクの#45070(現在オープン)にあるこのコードみたいなことをやりたくなるんでしょうね↓: Active ModelでパターンマッチできるようになればActive Recordでもパターンマッチできるように実装されるでしょうけど、通常のattributes APIであれば特に問題のないようなコードでも、Active Recordとパターンマッチが結合したときに何か面倒が起きそうな予感はする」「たしかに」

# #45070より
class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

post = Post.new(title: "Welcome!", comments: [Comment.new(body: "Thanks!")])
post => { title: "Welcome!", comments: [Comment[body:]] }
body # => "Thanks!"

「ところで今回パターンマッチがrevertされたのは、そろそろRails 7.1のリリースを検討し始めているからかも」「今のところ7-1-stableブランチやv7.1.0タグは見当たりませんけど、検討していそうですね」


以下の翻訳記事にも、マージがいったん取り消された件を追記しました↓。

Rails 7: Active Modelにパターンマッチングが追加(翻訳)

🔗Rails

🔗 rails-template-inspector: ブラウザ画面クリックでRailsソースコードを直接開ける

aki77/rails-template-inspector - GitHub


つっつきボイス:「ぼくのツイートだ😆」「なるほど、RailsのビューにJavaScriptコードを追加する形でやっているんですね↓」「これは便利そう」「便利ですよ〜」

<!DOCTYPE html>
<html>
<head>
<!-- ... -->
</head>
<body>

<%= yield %>

<% if Rails.env.development? %>
  <script type="module" src="https://cdn.skypack.dev/@aki77/rails-template-inspector@^0.3.0"></script>
  <rails-inspector url-prefix="vscode://file" root="<%= Rails.root %>" combo-key="command-shift-v"></rails-inspector>
<% end %>
</body>
</html>

「最近Storybookなどを仕事でやむを得ず触っているんですけど、今どきのフロントエンドはこういう風に画面をクリックしてソースコードを開くみたいな機能がとても充実していることを実感しましたね」「お〜」「こういう便利機能がもっとRailsにあっていいと思う👍」

参考: Storybook: UI component explorer for frontend developers


「なお、上の記事をjnchitoさんが頑張って英語版も出したそうです↓」「英語圏でも広く使われるようになったらさらに改良が進みそう」

🔗 Deserialization on Rails


つっつきボイス:「お〜、Railsのデシリアライズという切り口でセキュリティ上の注意点などをまとめた記事ですね: 目の付け所がうまい」「みっちり書かれていて凄いですね」「デシリアライズは気をつけたい点が多いので、読んでおきたい記事👍」

この記事は、先週のRailsセキュリティ修正について調べていて見つけました↓。

Railsセキュリティ修正7.0.3.1、6.1.6.1、6.0.5.1、5.2.8.1がリリースされました

🔗 「Hotwireかんたん入門」

つっつきボイス:「万葉さんがHotwire宣言したとおりにHotwireの入門記事を書いてくれた🎉」

🔗 スライド『クックパッドマートの失敗したデータ設計 Before / After 大放出』


つっつきボイス:「これはいいスライド👍」

「営業日と定休日の扱い↓あたりは経験者がいれば避けられた可能性はあるかもしれないけど、よく起きる問題なんですよね」「そうそう、定休日はまず規則的になってくれない」「おおむね規則性はあっても、きっと例外的なことが起きる」

🔗 その他Rails

つっつきボイス:「Rubyの経済効果、1位はAirbnb、2位はShopifyか」「言われてみればAirbnbはRubyだった」


「jnchitoさんが7/27(水)に開催する"リーダブルなテストコードについて考えよう"の参加者が既に500人超えです」「リーダブルなテストコードのように答えが1つではない問題はプロジェクトや会社によっても変わってきますけど、こんなふうにどんどん発表してもらって答えが煮詰められていくといいですね👍」

以下はつっつき後に見つけたツイートです↓。

🔗Ruby

🔗 Ruby技術者認定試験がRuby 3.xへの対応を発表


つっつきボイス:「そうそう、Ruby技術者認定試験が10月からついに改訂されますね🎉」「3.xに対応するなら勉強がてら受けてみようかな〜」「それならパターンマッチ覚えないと」「う😅」

「2.xの旧試験は2022/09/30に配信終了するんですって」「お別れの季節ですね👋」

🔗 Rubyの定数探索スタイル(Ruby Weeklyより)


つっつきボイス:「このどらちのスタイルにするか問題、ありますよね↓」「自分はafter派かも」「自分は途中に何も置かないならbefore派かな: 定数のネストが必要以上に深いのは好きじゃないので」

# 同記事より
# Before
class Product::Operation::Create
  # ...
end

# After
module Product
  module Operation
    class Create
      # ...
    end
  end
end

🔗 その他Ruby

つっつきボイス:「Rails Girls Japanが今年もRubyKaigiへの参加を支援してくれているんですね」「ありがたい🙏」


今週は以上です。

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

週刊Railsウォッチ: RubyのGCが高速化、RuboCopのストレスを減らす4つの方法、Defensive CSSほか(20220712後編)

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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