- Ruby / Rails関連
 
週刊Railsウォッチ: Turbo Railsのチュートリアル、Active Recordの「Leaky Abstraction」を削減ほか(20220411前編)
こんにちは、hachi8833です。17歳の誕生日おめでとうございます🎉
Today we're celebrating 17 years of Git! 🎉
How has Git changed your life? pic.twitter.com/urNVDk1Q6U
— 🦊 GitLab (@gitlab) April 7, 2022
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は以下のコミット差分より見繕いました。
🔗 ActionController::ParametersのキーにSymbolとStringのみを許可
ActionController::ParametersではキーにSymbolとStringのみを許可するようにする。
これは#44813の実験。
同PRより
# actionpack/lib/action_controller/metal/strong_parameters.rb#L271
    def initialize(parameters = {}, logging_context = {})
+     raise InvalidParameterKey unless parameters.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
      @parameters = parameters.with_indifferent_access
      @logging_context = logging_context
      @permitted = self.class.permit_all_parameters
    end
参考: Rails API ActionController::Parameters
つっつきボイス:「SymbolとString以外のものをキーにできないようにするということは、これまでは任意のオブジェクトを渡せてしまっていたということなんでしょうね」「その発想はなかったかも」「禁止されていないと使う人はどうしても出てきてしまいますよね」「RubyではハッシュのキーにSymbolとStringしか使わないのが普通ですけど、仕様上は任意のオブジェクトをキーにできる↓」
参考: class Hash (Ruby 3.1 リファレンスマニュアル)
ハッシュテーブル(連想配列とも呼ぶ)のクラスです。ハッシュは任意の種類のオブジェクト(キー)から任意の種類のオブジェクト(値)への関連づけを行うことができます。
https://docs.ruby-lang.org/ja/latest/class/Hash.htmlより
🔗 CSRFトークンをセッションストレージ以外の任意の場所に保存できるようになった
- PR: Allow CSRF tokens to be stored outside of session. by simbasdad · Pull Request #44283 · rails/rails
 
つっつきボイス:「csrf_token_storage_strategyというコンフィグが追加されて、CSRFトークンをセッションの外にオプションで保存できるようになったんですね」「どういうときに欲しくなるんでしょうか?」「サマリー↓にも、認証されてないユーザーのセッションが大量に作成されて、しかも中身はCSRFトークンだけという状態を避けたいときに便利とありますね」「なるほど、これはありがち」「フォームを作成した時点でCSRFトークンが必ず作成されるのでセッションストアがゴミだらけになるヤツ」「セッションをcookieに保存するなら問題ないですね」「ユースケースによって欲しくなる設定👍」
概要
CSRFトークンをカスタムの置き場所に保存できるlambdaを使える設定オプションを新たに追加。これはセッションをcookieに保存しない場合に有用。たとえば、Redisセッションストアを使う場合、認証されていないユーザー用のセッションが数百万件作成され、しかもそのセッションにはCSRFトークンしか含まれていないという状態になると、キャッシュがスラッシングして常に古いセッションが退避される可能性がある。
この新しい設定パラメータを使うと、CSRFトークンをセッションの外(暗号化済みcookieなど)に保存可能になり、認証済みユーザーのセッションのみをセッションストアに保存しておけるようになる。
同PRより
参考: §3.1 CSRFへの対応策 -- Rails セキュリティガイド - Railsガイド
🔗 ActionController::Parameters#valuesでネステッドハッシュの値をActionController::Parametersにキャスト
ActionController::Parameters#valuesがネステッドハッシュの値をActionController::Parametersにキャストするようにした。また、8af86c9で導入された非推奨警告もこのコミットで修正した。
同PRより
つっつきボイス:「これもActionController::Parametersの改修」「ネステッドハッシュもparamsで使えるようになったということかな」「このテストがわかりやすいかも↓: ActionController::Parametersの中にさらにActionController::Parametersがあっても正しく展開できるようになったんですね」
# actionpack/test/controller/parameters/accessors_test.rb#L314
  test "values returns an array of the values of the params" do
    params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
    assert_equal ["Chicago", "Illinois"], params.values
    params = ActionController::Parameters.new(city: "Chicago", state: "Illinois", person: ActionController::Parameters.new(first_name: "David"))
    assert_equal ["Chicago", "Illinois", ActionController::Parameters.new(first_name: "David")], params.values
  end
「valuesメソッドもこう変わった↓」
# actionpack/lib/action_controller/metal/strong_parameters.rb#L398
+   # Returns a new array of the values of the parameters.
+   def values
+     to_enum(:each_value).to_a
+   end
「ところで、ActionController::ParametersはRubyのHashクラスを継承していないんですよ」「形だけ見るととてもハッシュっぽいのに違うんですね」
参考: Rails API ActionController::Parameters
「たしかEnumerableですらないはず」「意外でした」「以前ハッシュのつもりで使おうとしたら使えないメソッドがあったときに気づきました: Railsのmainブランチで見るとActionController::Parametersは素のクラスになっている↓」「to_hやto_hashなども自前実装なんですね」
# https://github.com/rails/rails/blob/main/actionpack/lib/action_controller/metal/strong_parameters.rb#L142
  class Parameters
    cattr_accessor :permit_all_parameters, instance_accessor: false, default: false
    cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
    # 略
    def to_h(&block)
      if permitted?
        convert_parameters_to_hashes(@parameters, :to_h, &block)
      else
        raise UnfilteredParameters
      end
    end
   # 略
    def to_hash
      to_h.to_hash
    end
   # 略
  end
🔗 legacy_connection_handlingを削除
つっつきボイス:「Rails 6.1から互換性のためにあったlegacy_connection_handlingがついに削除された」「名前からしてそんな感じ」「Rails 6.1でコネクションプール周りが大きく変わったので、おそらくgemの都合などでこれまで残されていたんでしょうね」
参考: §2.1 データベース単位のコネクション切り替え -- Ruby on Rails 6.1 リリースノート - Railsガイド
Rails 6.1でデータベース単位のコネクション切り替え機能が使えるようになりました(#40370。6.0の場合は、ロールを
readingに切り替えるとすべてのデータベースコネクションもreadingロールに切り替わりました。6.1からは、Railsの設定でlegacy_connection_handlingをfalseに指定しておけば、対応する抽象クラスでconnected_toを呼び出すことでデータベースへのコネクションを切り替えられます。
railsguides.jpより
参考: Implement granular role and shard swapping by eileencodes · Pull Request #40370 · rails/rails -- legacy_connection_handlingはこのプルリクで入りました
🔗 db:schemaのダンプや読み込みのスキーマフォーマットを環境変数で指定できるようになった
rails db:structure:dumpやrails db:structure:loadが非推奨になったので、SQLフォーマットとRubyフォーマットの両方でシンプルにダンプする方法がなかった。
このプルリクは#39470のレビューでの@jeremyのアドバイスを実装したので、以下のように環境変数で指定できるようになった。
SCHEMA_FORMAT=sql rake db:schema:dump
同PRより
つっつきボイス:「SCHEMA_FORMAT環境変数でスキーマのフォーマットを指定できるようになった」「今までもコンフィグでSQLかRubyかを設定できたけど↓、この環境変数で上書きすれば柔軟に変更できますね」「CIでスキーマフォーマットを一時的に変更したいときとかに便利そう: これはいい話👍」
参考: §3.7.10 config.active_record.schema_format -- Rails アプリケーションを設定する - Railsガイド
「Railsの環境変数が使えるのはもちろん便利だけど、環境変数が増えてくるとだんだん管理が複雑になったりしますよね」「あるあるですね」
🔗Rails
🔗 hotrails.dev: Turbo Railsのチュートリアル
If you want to learn about Turbo Rails/Hotwire, I just finished writing my open-source Turbo Rails tutorial!
We will learn about Turbo Drive, Turbo Frames, Turbo Streams, and more! All those new concepts are illustrated with sketches!https://t.co/PG1m6t9pKA pic.twitter.com/wJvOm2dPc2
— Alexandre Ruban (@alexandre_ruban) March 18, 2022
つっつきボイス:「Matzがリツイートしていたのを見て知りました」「なるほど、Turbo Railsのチュートリアルサイトですか」「章立ても充実してる感じですね↓」「トップページの下にある『What people are saying』にチュートリアルの喜びの声が集められてますね」「手を動かしながらひととおり読み進めればTurbo Railsの基本的なユースケースに対応できそう👍」
- 1: Turbo Rails tutorial introduction
 - 2: Organizing CSS files in Ruby on Rails
 - 3: Turbo Drive
 - 4: Turbo Frames and Turbo Stream templates
 - 5: Real-time updates with Turbo Streams
 - 6: Turbo Streams and security
 - 7: Flash messages with Hotwire
 - 8: Two ways to handle empty states with Hotwire
 - 9: Another CRUD controller with Turbo Rails
 - 10: Nested Turbo Frames
 - 11: Adding a quote total with Turbo Frames
 
「トップページからたどれるQuote editor↓をTurboで作ってみましょうという趣向のようです」「この種のSPA的な機能を試すときって、とりあえずToDoやRemember The Milk的なものを作ったりしますよね」「そうそう」
参考: Remember The Milk: オンラインでTodoやタスクを管理しよう
🔗 Active Recordの「Leaky Abstraction」を削減する(Ruby Weeklyより)
つっつきボイス:「thoughtbotの記事だ」「記事は、クエリの詳細をコントローラに書いてしまうと後で修正箇所が増えてつらくなるよという話↓」「この記事ではそれをleaky abstractionと呼んでいるんですね」
# 同記事より
class AuthorsController < ApplicationController
  def index
    @published_authors = Person.distinct.joins(:posts).where(posts: { published: true })
  end
end
「記事での解決法はスコープ的なself.publishedクラスメソッドを作る: これもよく使われます↓」
# 同記事より
class Post < ApplicationRecord
  # other methods
  def self.published
    where("published_at < ?", Time.current)
  end
end
「これでpublishedの意味も適切に抽象化されるし、Post.publishedのようにわかりやすく書ける↓」
# 同記事より
class PostsController < ApplicationController
  def index
-   @newest_posts = Post.where(published: true).order(created_at: :desc).limit(10)
+   @newest_posts = Post.published.order(created_at: :desc).limit(10)
  end
end
class AuthorsController < ApplicationController
  def index
-   @published_authors = Person.distinct.joins(:posts).where(posts: { published: true })
+   @published_authors = Person.distinct.joins(:posts).merge(Post.published)
  end
end
「ところで、このpublished_atのように多義的に使われるカラムは、ここでやっているpublishedのようになるべくビジネスロジックに沿ったメソッドで抽象化しておかないと後でつらくなりがち」「使う場所ごとにpublished_atの意味を使い分けるのはたしかにつらそう...」
unpublished(published_atがnilに設定される)published(published_atには現在またはそれ以前のタイムスタンプが設定される)enqueued(published_atには未来のタイムスタンプが設定される)
同記事より
「ただ、こういうのはどこまでやるかというさじ加減がなかなか難しいんですよ: コントローラの1箇所でしか使わないなら抽象化なしでも容認できることもあるけど、あちこちで使われるようになってくると読むのがしんどくなる」「たしかに」
参考: Leaky abstraction - Wikipedia
参考: Leaky Abstraction について - 新山
🔗 『【2022年版】HerokuでRailsの画像が表示されないときの適切な対処法(と間違った対処法)』
この件、「"config.assets.compile = true"にしたら直りました!って書いてる記事のなんと多いことよ😭」と以前から嘆いておりました。これで少しは改善されるかな〜?https://t.co/FfCOJ5r48E
— Junichi Ito (伊藤淳一) (@jnchito) February 13, 2022
つっつきボイス:「jnchitoさんが以前から繰り返し指摘していた問題を解決すべく検索でトップに出る記事を書いたそうです」「ウォッチでも取り上げたことありますね」「そうそう、Herokuで画像が表示されないからといってconfig.assets.compile = trueをproductionで設定してはいけない」「ネットでこれをやってしまっている記事、あちこちにありますよね」
「原因についても丁寧に解説されている」「image_tagの画像名に拡張子付け忘れるの、ありがち」「development環境だと拡張子なしでも通るのにproductionだと効かないのって罠ですよね」
# 同記事より
-<%= image_tag "bread" %>
+<%= image_tag "bread.jpg" %>
「jpg拡張子をjpegと書いてしまうのもあるある」「4文字の拡張子ってなかなか見かけないですよね」
参考: 拡張子 - Wikipedia
「初歩的な問題ではあるけど、おそらくRailsの環境が以前より多様化してきたこともあって、アセット周りでときどき原因がすぐにわからないような問題に出くわすことがありますね」「記事にもあるstylesheet_pack_tagとかもそう」「Herokuの問題なのかどうかの切り分けとかも割と面倒だし、いろいろやっているうちになぜか動くようになってしまうことすらある」「そうそう」「初心者だとなおさらググって目についた方法に飛びつきがち」「そういう人たちを助けるための記事👍」
🔗 Herokuよもやま
「ちなみに、この記事で取り上げているのはRailsに特化したHeroku環境なので特殊だけど、今のHerokuはDockerコンテナでデプロイもできるので、そちらを使っていれば普通にDockerコンテナでやれるでしょうね」「HerokuのDockerデプロイ、まだやったことなかった」
参考: Docker によるデプロイ | Heroku Dev Center
「ところで、昨今はユーザーエクスペリエンスを重視する文化が広まったことで、たとえばFirebase+フロントエンドフレームワークのようなセットアップをよく見かけるようになってきましたね」「たしかにそんな感じ」「でも個人的には、そういうのでない業務システムのラピッドプロトタイピングなら、今でもRails+Herokuのようなセットアップでやるのが一番手っ取り早いと思います」「ふむふむ」「特に業務システムだと、データベースを手堅く使えるのは圧倒的なメリット」「たしかに」
参考: Firebase
🔗 その他Rails
つっつきボイス:「そうそう、OmniAuthとの関連でcriticalなセキュリティ修正が出てた: BPSのGitLabサーバーではOmniAuthは使っていないけど、週末にアップデートしておかないと(その後完了しました)」
前編は以上です。
バックナンバー(2022年度第2四半期)
週刊Railsウォッチ: RBS関連記事、Ruby formatterプロジェクト、Google Cloud Runほか(20220406後編)
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
      



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