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

週刊Railsウォッチ: Active Storageのしくみを詳しく解説するDiscussion投稿ほか(20231017前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

公式更新情報に追いついたので、mainブランチのコミットログから見繕いました。多くはドキュメント関連でした。また、7.1.2と7.2.0のマイルストーンもできていました。

参考: 7.1.2 Milestone
参考: 7.2.0 Milestone

🔗 HashWithIndifferentAccess#to_procを実装

従来は、HashWithIndifferentAccessオブジェクトに対して#to_procを呼び出すと、Hashクラスから継承された #to_procメソッドが使われていたが、これは文字列キーとシンボルキーを問わず値にアクセスする機能が使えなかった。

修正: #48770
同PRより


つっつきボイス:「Rails 7.1.1で一段落したのか今回は小粒の改修が多い感じです」「おや、HashWithIndifferentAccess#to_procがインスタンスメソッドとして実装されたのね: 自分はあまり使ってない機能ですが」「今までだとHash#to_procが呼ばれてたんですね」「#to_procしたものにアクセスするとキーが文字列の場合とシンボルの場合で結果が違ってしまっていたのか」

# activesupport/test/hash_with_indifferent_access_test.rb#968
# @strings = { "a" => 1, "b" => 2 }が設定済み
  def test_indifferent_to_proc
    @strings = @strings.with_indifferent_access
    proc = @strings.to_proc

    assert_equal 1, proc["a"]
    assert_equal 1, proc[:a]
    assert_nil proc[:no_such]
  end

参考: Rails API ActiveSupport::HashWithIndifferentAccess


後で上のテストコードを少し変えて手元のRails 7.1.1で動かしてみると、修正前は以下のようになりました。

» proc['a']
#>1
» proc[:a]
#>nil

🔗 send(primary_key)呼び出しをidに置き換えた

修正: #49408

primary_keyをレコードにsendして主キーの値を取得する理由はない。idメソッドなら正確に行えるし、抽象化としてもより優れている。また、これによって複合主キーの場合の問題も暗黙で修正される。
同PRより


つっつきボイス:「7.1の複合主キー関連でread_attribute(:id)が主キーを返すのが非推奨化されたときも取沙汰されましたけど(ウォッチ20230906)、こういうのは主キーではなくidで取るのが正当ですね」「これはRails内部のコード修正だからユーザー側には影響しないやつですね」

# activerecord/lib/active_record/persistence.rb#L1272
    def _raise_record_not_destroyed
      @_association_destroy_exception ||= nil
      key = self.class.primary_key
-     raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy #{self.class} with #{key}=#{send(key)}", self)
+     raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy #{self.class} with #{key}=#{id}", self)

参考: Active Record の複合主キー - Railsガイド

🔗 scaffoldで生成したシステムテストでdatetimetimeを正しく扱うよう修正

動機/背景

このコミットより前は、timeまたはdatetime属性を含むシステムテストをscaffoldで生成すると、updateのテストで失敗していた。キャプチャされたスクリーンショットには、以下のようなクライアントサイドのバリデーションエラーが表示されていた。

Please enter a valid value. The two nearest valid values are 10:45:33 AM
and 10:46:33 AM.

Please enter a valid value. The two nearest valid values are 09/27/2023,
10:45:33 AM and 09/27/2023, 10:46:33 AM.

詳細

このプルリクでは、システムテストのscaffoldを更新し、timedatetimeの値が条件付きで文字列に変換して正しく入力され、テストがパスするように修正する。
同PRより


つっつきボイス:「scaffoldで生成したシステムテストがいきなり失敗したらびっくりする」「今さらですが、システムテストにもジェネレータがあるんですね」

# railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb#L66
+
+       def datetime?(name)
+         attribute = attributes.find { |attr| attr.name == name }
+         attribute&.type == :datetime
+       end
+
+       def time?(name)
+         attribute = attributes.find { |attr| attr.name == name }
+         attribute&.type == :time
+       end

参考: §6 システムテスト -- Rails テスティングガイド - Railsガイド

# Railsガイドより
$ bin/rails generate system_test users
      invoke test_unit
      create test/system/users_test.rb

🔗 ActiveSupport::BroadcastLoggerのメソッドにブロックも渡せるようになった

#49417の続き。

ブロードキャストロガーで呼び出されるメソッドにブロックを渡したら、すべての登録済みのロガーにブロックをforwardするべき。たとえば、Rails.logger.tagged { }を呼び出したら、現在は渡したブロックを実行せずに新しいタグ付きロガーを返している。
同PRより


つっつきボイス:「Rails 7.1に駆け込みでマージされたBroadcastLoggerの続きですね(ウォッチ20231011)」「method_missingに手を加えてブロックも受け取れるようにしたんですね↓: ロガーをカスタマイズしたりしてもブロックがforwardされるようになった感じかな」

# activesupport/lib/active_support/broadcast_logger.rb#L206
-     def method_missing(name, *args)
+     def method_missing(name, *args, &block)
        loggers = @broadcasts.select { |logger| logger.respond_to?(name) }

        if loggers.none?
-         super(name, *args)
+         super(name, *args, &block)
        elsif loggers.one?
-         loggers.first.send(name, *args)
+         loggers.first.send(name, *args, &block)
        else
-         loggers.map { |logger| logger.send(name, *args) }
+         loggers.map { |logger| logger.send(name, *args, &block) }
        end
      end

+     def respond_to_missing?(method, include_all)
+       @broadcasts.any? { |logger| logger.respond_to?(method, include_all) }
+     end

参考: Rails API ActiveSupport::BroadcastLogger
参考: BasicObject#method_missing (Ruby 3.2 リファレンスマニュアル)

🔗 ドキュメントの修正2件


つっつきボイス:「2つまとめました: APIドキュメントのRDocフォーマットでmarkdownのバッククォートを書く人があとを絶たないらしく、1つ目の#49407の他に7.1でも何度かRDocの++記法に修正されていました」「RDocってバッククォート使えないのか」「RDoc書いたことなかった」「こういうのはlinterで自動修正できそうな気もしますけどね」「たしかに」

# activerecord/lib/active_record/associations.rb#L158
-       #   When the value is set the Array size must match associated model's primary key or `query_constraints` size.
+       #   When the value is set the Array size must match associated model's primary key or +query_constraints+ size.

参考: §1 RDoc -- API ドキュメント作成ガイドライン - Railsガイド
参考: class RDoc::MarkupReference - rdoc 6.5.0 Documentation

「2つ目の#49412はドキュメントのコードブロックに多数修正が入りました」「コードブロックの言語が違っているのとかいろいろ修正されてる」「これはrubocop-mdによる自動修正なのか」「そういえば少し前にRailsで導入されていましたね(ウォッチ20230405)」

# guides/source/action_cable_overview.md#L850
-```ruby
+```js
# guides/source/contributing_to_ruby_on_rails.md#L600
def load_defaults(target_version)
  case target_version.to_s
  when "7.1"
-   ...
+   # ...
    if respond_to?(:active_job)
      active_job.existing_behavior = false
    end
-   ...
+   # ...
  end
end
# guides/source/active_record_multiple_databases.md#L320
-class MyCookieResolver << ActiveRecord::Middleware::DatabaseSelector::Resolver
+class MyCookieResolver < ActiveRecord::Middleware::DatabaseSelector::Resolver

「rubocop-mdでチェックする対象を追加した、なるほど↓」

# .rubocop.yml#L20
    - 'activestorage/test/dummy/**/*'
    - 'actiontext/test/dummy/**/*'
    - 'tools/rail_inspector/test/fixtures/*'
+   - guides/source/debugging_rails_applications.md
+   - guides/source/active_support_instrumentation.md

rubocop/rubocop-md - GitHub

🔗Rails

🔗 Phlexを使ってみて(Ruby Weeklyより)


つっつきボイス:「以前話題にしたPhlexというViewComponent的なコンポーネントを使ってみた記事だそうです(ウォッチ20221011)」「そういえばPhlexありましたね: コードの雰囲気はSlimを思わせるものがありそう↓」「言われてみればHTMLタグがメソッド名になっているところとか似てるかも」「アイデアとしては昔からあったんですね」「DSLの一種だと思えばいいので、Phlexの書き方にはそんなに違和感はないかな」

# 同記事より
class ArticlePartial < Phlex::HTML
  include Ui::Typography

  def initialize(article)
    @article = article
  end

  def template
    article(class: "article") do
      heading1(@article.title)
      subtitle(@article.motto)

      div(class: "content") { @article.content }
    end
  end
end

phlex-ruby/phlex - GitHub

参考: ドメイン固有言語(DSL) - Wikipedia

参考: slim/README.jp.md at main · slim-template/slim

# https://github.com/slim-template/slim/blob/main/README.jp.mdより
doctype html
html
  head
    title Slim Examples
    meta name="keywords" content="template language"
    meta name="author" content=author
    link rel="icon" type="image/png" href=file_path("favicon.png")
    javascript:
      alert('Slim supports embedded javascript!')

  body
    h1 Markup examples

    #content
      p This example shows you how a basic Slim file looks.

    == yield

    - if items.any?
      table#items
        - for item in items
          tr
            td.name = item.name
            td.price = item.price
    - else
      p No items found. Please add some inventory.
        Thank you!

    div id="footer"
      == render 'footer'
      | Copyright &copy; #{@year} #{@author}

実践ViewComponent(1): 現代的なRailsフロントエンド構築の心得(翻訳)

🔗 Active Storageのしくみを詳しく解説するDiscussion投稿(Ruby on Rails Discussionsより)


つっつきボイス:「たしかX(元Twitter)で見かけたような気がするんですが、discuss.rubyonrails.orgのこの投稿が絶賛されていました

「おぉ〜、これはとてもよく書けている記事なのでは: Active Storageのここまで詳しい解説ってないので、今は知りたかったら自分で実装を追いかけるしかないヤツですね」「分量もすごい」

後で見出しだけ取り出して翻訳してみました。投稿ではActive StorageをASTと略しています。


1. はじめに
2. Active Storageを理解する
2.1. 基本的なユースケースをサンプルにする
2.2. Active Storageで作成されるテーブルやカラム
2.2.1. active_storage_blobs
2.2.2. active_storage_attachments
2.2.3. active_storage_variant_records

2.3. 内部のしくみ
2.3.1. 画像を添付すると何が行われるか
2.3.2. 画像を表示すると何が行われるか
2.3.3. ファイルを「リダイレクトモード」「プロキシモード」「パブリックモード」で配信すると何が行われるか
2.3.4. ダイレクトアップロードのフローを解説
2.3.5. PNGのフォールバックのしくみ
2.3.6. AnalyzeJobのしくみ

2.4. これらが自分のアプリと一体どう関係するか
2.4.1. 添付ファイルのあるレコードを大量に表示するときはN+1クエリに十分注意すること
2.4.2. クライアントが遅くならないために、ファイルアップロードは常にダイレクトアップロードを使うこと
2.4.3. プロキシモードを使う場合は、クライアントが遅くならないためにサーバーとユーザーの間にNginxやCloudflareを配置すること
2.4.4. オンデマンドのバリアント生成機能は素晴らしいが、気をつけないとアプリ全体が詰まってしまう可能性がある
3. production環境で画像を配信する場合の注意事項や愚痴など

「discussに投稿するボリュームと内容じゃないですよね: 公式にRailsガイドに含めてもおかしくないのでは」「Railsガイドに載せてはどうかというレスもついてますね」「翻訳したいけどSNS投稿だとやりにくそう...」

参考: Active Storage の概要 - Railsガイド


前編は以上です。

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

週刊Railsウォッチ: Rails 7.1.0リリース、YARPがprismにリネームほか(20231011)

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

Rails公式ニュース

Ruby Weekly

Ruby on Rails Discussions


CONTACT

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