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

週刊Railsウォッチ: Rails 7.1がJavaScriptでBunサポートを追加ほか(20230926前編)

こんにちは、hachi8833です。Rails 7.1beta1の全Changelog、あと少しで読み終われそうです。

Rails 7.1 Beta 1がリリースされました

週刊Railsウォッチについて

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

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

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

🔗 生成される.dockerignoreに.envファイルを追加

重要なトークンを含む可能性のある.envファイルがあるかどうかをチェックせずに済むようになる。
同PRより

# railties/lib/rails/generators/rails/app/templates/dockerignore.tt#L9
+# Ignore all environment files (except templates).
+/.env*
+!/.env*.erb

同Changelogより


つっつきボイス:「DHHのシンプルなプルリクです」「たしかに.envは.dockerignoreにも含めておくべき」「!/.env*.erbとなっているのはテンプレート(.erb)ファイルを除外しないためみたい」「/.env*だけで十分な気もしますけどね」

参考: .dockerignore ファイル -- Dockerfile リファレンス — Docker-docs-ja 24.0 ドキュメント

🔗 関連付けでprimary_key:オプションを常に優先するようになった

このプルリクは、関連付けられるモデルにquery_constraintsが設定されている場合に、primary_key:オプションが無視される問題を修正する。修正後は、常にprimary_key:オプションが優先されるようになり、primary_key:オプションが存在しない場合のみquery_constraintsassociation_primary_keyの値が決定される。

これは技術的にはRails 7.1 beta版のバグなので、CHANGELOGエントリとコミットが7-1-stableブランチに(このブランチがあれば)バックポートされることを想定している。これを次のリリースでバグ修正として含める方法や、CHANGELOGエントリが必要かどうかについて知らせて欲しい。
同PRより


つっつきボイス:「Rails 7.1の複合主キー関連のバグ修正ですね: association_primary_keyで主キーを決定するときに、修正前はquery_constraintsが効いてしまっていたのを修正後はprimary_keyオプションを先にチェックするようになった↓」

# activerecord/lib/active_record/reflection.rb#L864
      def association_primary_key(klass = nil)
-       if !polymorphic? && ((klass || self.klass).has_query_constraints? || options[:query_constraints])
-         (klass || self.klass).composite_query_constraints_list
-       elsif primary_key = options[:primary_key]
+       if primary_key = options[:primary_key]
          @association_primary_key ||= -primary_key.to_s
+       elsif !polymorphic? && ((klass || self.klass).has_query_constraints? || options[:query_constraints])
          (klass || self.klass).composite_query_constraints_list
+       elsif (klass || self.klass).composite_primary_key?
          # If klass has composite primary key of shape [:<tenant_key>, :id], infer primary_key as :id
          primary_key = (klass || self.klass).primary_key
          primary_key.include?("id") ? "id" : primary_key
        else
          primary_key(klass || self.klass)
        end
      end

🔗 Active Recordのトランザクションをinstrumentationで計測できるようになった

Active Recordのトランザクションでinstrumentationを利用可能になった。

トランザクションイベントをサブスクライブすることでトラッキングやinstrumentationで利用できるようになる。イベントペイロードには、コネクションの他にタイミングの詳細詳細情報も含まれる。

ActiveSupport::Notifications.subscribe("transaction.active_record") do |event|
  puts "Transaction event occurred!"
  connection = event.payload[:connection]
  puts "Connection: #{connection.inspect}"
end

Daniel Colson, Ian Candy
同CHANGELOGより


つっつきボイス:「transaction.active_recordというフックがinstrumentationに追加されたそうです」

参考: Active Support の Instrumentation 機能 - Railsガイド

「追加されたフックを見ると、handlestartedpayloadが取れるらしい↓」「ちなみにこのフックは7.1のガイドにはまだ追加されていませんでした」

    class TransactionInstrumenter
      def initialize(payload = {})
        @handle = nil
        @started = false
        @payload = payload
      end

      def start
        return if @started
        @started = true

        @handle = ActiveSupport::Notifications.instrumenter.build_handle("transaction.active_record", @payload)
        @handle.start
      end

      def finish
        return unless @started
        @started = false

        @handle.finish
      end
    end

「instrumentationは自分で追加することも可能だし、Datadogなどの監視サービスを使うのももちろんありですが、公式に追加されたことでトランザクションでこういう項目を調べやすくなったのはいいですね👍」

参考: クラウド時代のサーバー監視&分析サービス | Datadog

🔗 rails newでBunを指定できるようになった

rails new --javascriptでBunをサポートするようになった。

rails new my_new_app --javascript=bun

Jason Meller
同CHANGELOGより


関連プルリク

このプルリクは、以下の関連プルリクがすべてマージ・リリースされてからマージすること。さもないと--javascript bunが期待通り動かない。

なお、ボーナスとしてexecJSを更新した(#127)ので、execJSでもBunをランタイムとして使えるようになった。

Bunについて

Bunは、node.jsランタイム、yarnパッケージマネージャー、esbuildバンドラーに対する有望な新しい代替手段。Bunの主な特徴は速さにあり、多くの場合node.jsや関連製品よりも数倍高速。

素のRailsプロジェクトのほとんどが、ほんの少しだけJSを散りばめたいと考えている(しかしJSエコシステムがimport-mapsよりもう少しましになる方が嬉しい場合もある)。Bunはこうした用途に非常に適しており、新しいRailsプロジェクトでも手軽に採用できる。

Stimulus、Turbo、Tailwindなどの共通のデフォルトを利用する新規Railsプロジェクトとの相性が実によいので、こうしたアプリを構築する人々向けに、Bunを第一級市民にすることを提案する。Bunでこれらのプロジェクトを手早く立ち上げられるようにして、Yarn/Nodeの残り物を外科手術で取り除く時間をかけずに済むようにすべき。

BunとTailwindの実行例

このプルリクがマージされれば、rails new --javascript bun --css tailwind --database sqlite3のようなオプションで新規Railsプロジェクトを立ち上げれば、「すべて問題なく」動くようになるはず。
同PRより


つっつきボイス:「最近人気が高まっているというBunが追加されました」「Bunはパッケージ管理以外にもバンドルやランタイムなどを単体でひととおりサポートしているんですって」「速いという評判」「bunを辞書で見るといくつか意味があるけど、アイコンからするとこの場合は"束髪"かな」

参考: Bun — A fast all-in-one JavaScript runtime

oven-sh/bun - GitHub

「試しに7.1beta1でrails new --javascript bun --css tailwind --database sqlite3をやってみたところ、rails newは成功しましたが、周辺gemでまだマージされていないものがあるので、scaffoldしたページではまだJavaScriptが動きませんでした」

「業務で使うことを考えると、速さよりも今後長期間サポートされるかどうかの方を気にしたい」「結局そこがポイントですよね」


なお、Bunはjsbundling-rails環境での利用が前提なので、当然ながらimportmap-railsとは共存できません。

jsbundling-rails README(翻訳)

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

🔗 マイグレーションヘルパーで複合主キーをサポート

マイグレーションヘルパーで複合主キーをサポート。

# "carts"テーブルの主キーが"(shop_id, user_id)"だとする

add_foreign_key(:orders, :carts, primary_key: [:shop_id, :user_id])

remove_foreign_key(:orders, :carts, primary_key: [:shop_id, :user_id])
foreign_key_exists?(:orders, :carts, primary_key: [:shop_id, :user_id])

fatkodima
同CHANGELOGより


つっつきボイス:「Rails 7.1で複合主キーをサポートするなら当然マイグレーションでもサポートが必要ですね: primary_key:に複合主キーを配列の形で渡せるようになった👍」「これないと困るヤツ」「7.1beta1が出たこの時期でこういう機能が入ってくるぐらいなので、複合主キーはリリース後も使いながら完成度を高めていく形で進めることになるでしょうね」

🔗 マイグレーションのadd_check_constraintif_not_existsオプションを渡せるようになった

マイグレーションにCHECK制約を追加するときのif_not_existsオプションをサポート。

add_check_constraint :posts, "post_type IN ('blog', 'comment', 'share')", if_not_exists: true

Cody Cutrer
同CHANGELOGより


つっつきボイス:「以下のようなマイグレーションでバリデーションをオフにすると、1回目はいいけど2回目だとCHECK制約がもうできているのでエラーになってしまう: if_not_exists: trueを指定すると、CHECK制約が既に存在する場合でもadd_check_constraintがエラーにならなくなる、なるほど」「デフォルトはif_not_exists: falseなんですね」

add_check_constraintメソッドに新しいif_not_existsオプションを渡せるようになった。
このオプションをtrueに設定すると、CHECK制約が既に存在している場合でもエラーが発生しなくなる。
さらに、remove_check_constraintadd_check_constraintにこのオプションが設定されていれば、マイグレーションの取り消し(ロールバック)でif_existsおよび if_not_existsオプションの振る舞いも入れ替わる。これにより、冪等かつトランザクショナルでないマイグレーションを手軽に作れるようになる。

動機/背景

自分たちは以下のようなマイグレーションを書くことが多い。

class MyMigration < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def change
    add_check_constraint(:table, "expression", name: "mychk", validate: false)
    validate_constraint(:table, "mychk")
  end
end

しかし、バリデーションが失敗したときでもマイグレーションを気軽に再実行できるようにしたい。この操作はトランザクショナルではないので、そのためには冪等でなければならない。つまり、add_check_constraintif_not_existsオプションが必要となる。
また、トランザクショナルでないマイグレーション内で正しくロールバックするには、ロールバックでremove_check_constraintを呼び出すときにif_exists: trueを設定する必要もある。
なお、CHECK制約の削除中にロールバック処理がデータベースから切断されると、その状態でマイグレーションが「完了」と記録される可能性がある(少々わざとらしい例だが、同じマイグレーション内でCHECK制約を追加してから削除する場合などに起きうる)。

同PRより

🔗 暗号化属性の平文データサポートをsupport_unencrypted_dataオプションで属性ごとに無効化できるようになった

属性ごとに設定されるsupport_unencrypted_dataオプションを暗号化に追加。

特定の暗号化済み属性についてのみ、暗号化されていない文のサポートをsupport_unencrypted_dataでオフにできるようになった。
このオプションは、ActiveRecord::Encryption.config.support_unencrypted_data == trueが設定済みの場合にのみ有効。

class User < ActiveRecord::Base
  encrypts :name, deterministic: true, support_unencrypted_data: false
  encrypts :email, deterministic: true
end

Alex Ghiculescu
同CHANGELOGより


つっつきボイス:「なるほど、Railsの暗号化機能は、暗号化したい属性に既存の平文(暗号化されていない文)が既にある場合に読み出せるようにもできるんですが、その平文サポートを属性単位で無効にできるようになったということですね」「コンフィグのencryption.support_unencrypted_dataで平文のサポートをグローバルに許可している場合にのみ使えるとあるけど、これ自体オプショナルだからデフォルトではオフ」「平文をサポートしない属性はエラーにするのではなく平文を検索できなくなるのね↓」

  test "If support_unencrypted_data is opted out at the attribute level, cannot find unencrypted data" do
    UnencryptedBook.create! name: "Dune"
    assert_nil EncryptedBookWithUnencryptedDataOptedOut.find_by(name: "Dune") # core
    assert_nil EncryptedBookWithUnencryptedDataOptedOut.where("id > 0").find_by(name: "Dune") # relation
  end

参考: § 3.5 暗号化されていないデータのサポート -- Active Record と暗号化 - Railsガイド

ActiveRecord::Encryption.config.support_unencrypted_data == trueに設定されている場合、以下のように書けるようになる。

class User < ActiveRecord::Base
  encrypts :name, deterministic: true, support_unencrypted_data: false
  encrypts :email, deterministic: true
end

この場合、emailカラムだけに平文データのサポートを許可する(なおコンフィグでextend_queries: trueが設定されていれば、emailクエリでのみ拡張クエリが使える)。
この機能が欲しくなる理由についての裏話は#49072のコメントを参照
同PRより

🔗 名前が衝突する属性名を生成するとエラーを発生するようになった

属性名が衝突する危険がある場合にエラーをraiseするようになった。

以下のコマンドは、savehashが既にActive Recordで定義済みなのでエラーになる。

bin/rails generate model Post save
bin/rails generate model Post hash

Petrik de Heus
同CHANGELOGより


つっつきボイス:「危険な名前、たしかに」「この使っちゃいけない名前リストってどこにあるんでしょう?」(しばらく探す)「これみたい↓: なるほど、instance_methodsでリストを取り出しているんですね」

# activerecord/lib/active_record/attribute_methods.rb#L31
    class << self
      def dangerous_attribute_methods # :nodoc:
        @dangerous_attribute_methods ||= (
          Base.instance_methods +
          Base.private_instance_methods -
          Base.superclass.instance_methods -
          Base.superclass.private_instance_methods
        ).map { |m| -m.to_s }.to_set.freeze
      end
    end

参考: Module#instance_methods (Ruby 3.2 リファレンスマニュアル)

🔗 ドキュメント関連

🔗 複合主キーガイドが新たに追加


つっつきボイス:「新機能である複合主キーのガイドは欲しくなるヤツ👍: 今後ブラッシュアップされるんでしょうね」

参考: rails/guides/source/active_record_composite_primary_keys.md at main · rails/rails

🔗 テストガイドにビューパーシャルのテスト方法を追加

つっつきボイス:「こちらはテスティングガイドにパーシャルのテスト方法が追加されていました↓」「render partial:ができるんだから、パーシャルのテストのドキュメントがあってもいいですね👍」

参考: rails/guides/source/testing.md at main · rails/rails

「なお、最近の更新ではありませんが、エラーレポートガイドも7.1で新しく追加されています↓」

参考: rails/guides/source/error_reporting.md at main · rails/rails


前編は以上です。

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

週刊Railsウォッチ: Turbo 8のTypeScriptがJavaScriptに置き換わるほか(20230914後編)

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

Rails公式ニュース

Ruby Weekly


CONTACT

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