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

週刊Railsウォッチ: ビューテンプレートに渡せるローカル変数をマジックコメントでチェック可能にほか(20220822前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

更新情報が2つ出ていたので、以下の中から取り上げていなかったものを見繕いました。

以下は次回に取り上げます。

🔗 テンプレートが受け取れるローカル変数をマジックコメントで定義可能になった

概要

このプルリクは、デフォルト値付きの必須引数をテンプレートで利用可能にするオプションを導入する。
今後この変更によって、初期化中にテンプレートをプリコンパイルする機能も利用可能になるだろう。

問題について

Railsのテンプレートには任意のローカル変数を受け取れる暗黙のAPIがあるため、テンプレートの依存関係の推測が難しくなることがある。しかも、背後のAction Viewのコードでローカル変数をなまじ柔軟に利用できてしまうため、実行時に渡されるローカル変数の一意の組み合わせごとにテンプレートをコンパイルしなければならなくなる。

提案されたソリューション

RailsConfで雑談した@tenderloveや@jhawthornや@byrootとともに、テンプレートのシグネチャをオプショナルのマジックコメントで利用可能にし、これらの引数をそのテンプレートのメソッドシグネチャとしてコンパイルするというアイデアを思い付いた。

これにより、将来はテンプレートを実行時ではなくアプリケーション起動時にプリコンパイル可能になる。詳しくは以下を参照。

変更前:

<%# issues/_card.html.erb %>
<% title = local_assigns[:title] || "Default title" %>
<% comment_count = local_assigns[:comment_count] || 0 %>
<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>

変更後:

<%# issues/_card.html.erb %>
<%# locals: (title: "Default title", comment_count: 0) %>
<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>

その他考慮すべき点

  • 引数にオブジェクトのイニシャライザを用いて1個のテンプレートごとに1個のオブジェクトをコンパイルする

これは私たちがGitHubのViewComponentで構築してきたこととだいたい同じ。これは自分たちのところでは非常に効果的だったが、1個のテンプレートを1個のオブジェクトにラップすると、利用頻度が高い場合にオブジェクトのアロケーションが多数発生するという欠点があり、フォームヘルパーで問題が発生した。

  • 暗黙のシグネチャコンパイル

理論上は、ASTをフルスキャンするとか新たな形の静的解析によってテンプレートのシグネチャを生成することも一応可能。しかし経験上これは困難であることが明らかだ。さらに、引数にデフォルト値を設定したり引数を必須にしたりすることはまだできない。
同PRより


つっつきボイス:「ビューのパーシャルに、以下のようにERBコメント形式でlocalsのシグネチャをマジックコメントとして書けるようになった↓、これは嬉しいかも」

<%# issues/_card.html.erb %>
<%# locals: (title: "Default title", comment_count: 0) %>
<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>

「これまでは、テンプレートで特定のローカル変数が渡されることが保証されない場合、コードでif defined?でチェックするとか、以下のサンプルコード↓みたいにtitle = local_assigns[:title] || "Default title"などと書かないといけなかったんですよ」「そうそう」「ビューの中にはそういうチェックロジックみたいなものをなるべく書きたくないので、渡していい引数をマジックコメントで書けばチェックしてくれるのは嬉しいですね👍」「これはいい」

<%# issues/_card.html.erb %>
<% title = local_assigns[:title] || "Default title" %>
<% comment_count = local_assigns[:comment_count] || 0 %>
<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>

「ところでlocalsって何でしょうか?」「ビューのrenderを呼ぶときに明示的にハッシュを渡せるオプションですね↓: 渡された側は普通のローカル変数として使える」

参考: Rails4 で render partial 部分テンプレートに変数を渡す(locals option)を使う時の注意点 - Qiita

「値をインスタンス変数で渡すとすべてのビューでグローバルに共有されてしまいますし、インスタンス変数を変更すると他の場所に影響する可能性も増えるので、なるべくインスタンス変数では渡したくない」「パーシャルにローカル変数として渡せば明示的になるし悪影響も防げるので、自分はあちこちで使われるパーシャルほど極力ローカル変数として値を渡すようにしていますね」「なるほど」「自分も普段からなるべくlocalsオプションで渡すようにしてます」

「パーシャル側にチェックコードを書きたくなかったので、これまではパーシャルにコメントの形でローカル変数の仕様を書いてました」「今回の改修でシグネチャをマジックコメントとして書くことで手軽にローカル変数をチェックできるようになったということですね」

参考: §3.4.4 ローカル変数を渡す -- レイアウトとレンダリング - Railsガイドより

🔗 find_or_create_byRecordNotUniqueエラーの場合にfindをリトライするようになった

  • find_or_create_byで一意性の制約に遭遇したときに2回目のfindを行うようになった。

これまでのfind_or_create_byは、適切な制約が設定されていない場合に、重複レコードを作成するかActiveRecord::RecordNotUniqueで失敗するかのどちらかという競合状態が常に発生していた。
そういうときのためにcreate_or_find_byが導入されたが、レコードが存在するケースがほとんどの場合はかなり無駄が多かった(INSERTはSELECTよりも多くのデータを送信しなければならず、データベースの負荷も増える)。一部のデータベースでは主キーがインクリメントされてしまうという望ましくないことも起きる。

つまり、レコードがほぼ確実に存在することが期待される状況なら、createActiveRecord::RecordNotUniqueで失敗したときはfindをリトライすれば競合状態にはならない。ただし、これはそのテーブルに適切な一意性の制約が設定されていることが前提。設定されていない場合はfind_or_create_byで引き続き重複レコードが作成されてしまう。

Jean Boussier, Alex Kitchens
同Changelogより


つっつきボイス:「find_or_create_byの競合状態を防ぐために、必要な場合だけfindを2回実行するようにしたみたい」「2つの処理をまとめて行うメソッドはいろいろ注意が必要ですね」

参考: Rails API find_or_create_by -- ActiveRecord::Relation

🔗 CHECK制約の削除でif_existsオプションが利用可能になる

  • CHECK制約の削除でif_existsオプションが利用可能になる
    remove_check_constraintif_existsを渡せるようになった。trueの場合はCHECK制約が存在しなくてもエラーを発生しない。
    Margaret Parsa and Aditya Bhutani
    同Changelogより

つっつきボイス:「remove_check_constraintif_existsオプションをつけられるようになった: やりたいことはわかる」「if_existsをつけても大丈夫とわかっていれば念のため書いておくということができますね」

remove_check_constraint :products, name: "price_check", if_exists: true

参考: Rails API remove_check_constraint -- ActiveRecord::ConnectionAdapters::SchemaStatements

🔗 マイグレーションにdrop_enumコマンドが追加(PostgreSQLのみ)

  • PostgreSQL向けのdrop_enumコマンドがマイグレーションに追加
    これはcreate_enumとちょうど逆の動作。enumを削除する前に、そのenumに依存するカラムを削除しておくこと。
    Alex Ghiculescu
    同Changelogより

つっつきボイス:「PostgreSQL専用の機能」「以下の記事にはdrop_enumはないと書かれていたんですが、ついにできたんですね」

Rails 7: PostgreSQLのカスタムenum型が使いやすくなった(翻訳)

# activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L492
+     def drop_enum(name, *args)
+       options = args.extract_options!
+       query = <<~SQL
+         DROP TYPE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(name)};
+       SQL
+       exec_query(query)
+     end

「PostgreSQLでDROP TYPEができるということはCREATE TYPEもできるということ?」「あ、ありますね↓」「ほんとだ、PostgreSQLでは新しいデータ型を定義できるのか」

参考: CREATE TYPE -- PostgreSQL 14.0文書
参考: DROP TYPE -- PostgreSQL 14.0文書

🔗 routes --unusedで冗長なルーティングを検出

ルーティングの作成には時間がかかり、Railsアプリのルーティングが多すぎると起動が時とともに遅くなることがある。このスクリプトは、作成されているが実際は無効なルーティングを検出するのに使える。このスクリプトで検出したルーティングを削除すると、アプリの高速化や不要なコードの削除に役立てられる。
例:

> bin/rails routes --unused

Found 2 unused routes:

Prefix Verb URI Pattern    Controller#Action
   one GET  /one(.:format) action#one
   two GET  /two(.:format) action#two

これをRailsに取り込む価値があるか、それともgemにすべきか判断に迷っている。この恩恵を得られるアプリは多そうだし、使っているのはprivate APIなので、ここに投げるのが一番適していると思う。いかがだろうか?
余談だが、このスクリプトを書いたきっかけは、とある大規模アプリに未使用ルーティングがあることに気づいたことだった。このスクリプトのおかげで無効なルーティングを100件以上発見でき、現在削除を進めているところ。このスクリプトによって多くの不要なコードが明らかになったので、他のアプリでも使えるようにするとよいと思う。
同PRより


つっつきボイス:「これはきっとみんなが欲しかった機能🎉」「使ってないルーティングってどこまで検出できるんだろう?」「url_forとかで生成されるものあたりは検出できそうな気はする」「コントローラやビューで実装されていないものを探して知らせてくれる感じかな」

「ところで、このコマンドはrubocop-railsあたりにあってもいいかもしれませんね」「たしかに」「もちろんRails本体にあってもいいと思います」

rubocop/rubocop-rails - GitHub

🔗 番外: Changelogの書式をチェックするlinterを追加

# .github/workflows/lint.yml#L8
jobs:
+ changelog-formatting:
+   name: Check CHANGELOGs formatting
+   runs-on: ubuntu-latest
+   steps:
+   - uses: actions/checkout@v3
+     with:
+       repository: skipkayhil/rails-bin
+       ref: 44270430c14385fd7db002b47f0819af5d824352
+   - uses: ruby/setup-ruby@v1
+     with:
+       ruby-version: 3.1
+       bundler-cache: true
+   - uses: actions/checkout@v3
+     with:
+       path: rails
+   - run: bin/check-changelogs ./rails
...

つっつきボイス:「Changelogの書式が揃ってくれるとRailsリリース記事が書きやすくなるので個人的に嬉しい機能です」「DependabotのようにChangelogの内容を抽出して知らせるCI機能なども多いので、Changelogをプログラムで解析可能にしておくのは大事ですね👍」

参考: Keeping your supply chain secure with Dependabot - GitHub Enterprise Server 3.4 Docs

🔗Rails

🔗 Turbo関連の更新(Rails公式ニュースより)


つっつきボイス:「Railsの更新情報のうちTurboに関連するものをこちらにまとめました」「Turbo v7.2.0-beta.2はいろいろ変更や改修が行われてますね」「Basecampが必要とする機能から順に実装されているんでしょうね」「Railsとバージョン番号が揃ってたらいいのに」

🔗 Hotwireを「プログレッシブエンハンスメント」で理解する

参考: プログレッシブエンハンスメント - Wikipedia


つっつきボイス:「プログレッシブエンハンスメントでHotwireを理解するという記事で、今翻訳中です」「そういえば万葉さんも引き続きHotwireを推していますね↓」

「今いるRailsエンジニアだけで作業するのであれば、HotwireやTurboはとてもいいと思います」「ですね」「強いて言うならですが、今の時点では事例が少なくてHotwireやTurboを使えるエンジニアを新たに探しにくいのと、Storybookみたいなフロントエンドの便利ツールがHotwireにはまだ少なそうなことぐらいかな🤔」

🔗 図解Redis


同記事より


つっつきボイス:「絵心のある人がRedisを図解付きで解説しているのがいいですね👍」「他にもデータベース基礎の図解入り記事とかいろいろ書いているようです↓」

参考: Things You Should Know About Databases


同記事より

🔗 その他Rails


つっつきボイス:「今日はすろっくさんお休みですが、記事が出てました」「Railsをメジャーアップデートするときは、普通の手順を普通に進めるのが一番」

「永和システムマネジメントさんはRubyKaigiで3人も登壇するの凄い」「gem_rbs_collectionの話題があるんですって↓」「RuboCopにサーバーモードが追加されたんですね」「サーバーモードは、Railsのspring gemと同じような感じで大量のcopをプリロードすることでCIでの実行を高速化するのかなと想像してみました」

ruby/gem_rbs_collection - GitHub


前編は以上です。

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

週刊Railsウォッチ: RubyのGVLトレーサーgvl-tracing、casting gemでオブジェクトに振る舞いを追加ほか(20220802後編)

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

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

Rails公式ニュース


CONTACT

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