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

週刊Railsウォッチ: GitLabがRailsにこだわる理由、Rails7アップグレードのハマりどころほか(20220620前編)

こんにちは、hachi8833です。


つっつきボイス:「年商一千万円を超える全ての法人と個人事業主が対象、マジで?」「年商は売上のことですね」「あくまで登録リストが公開されるだけで額までは公開されていませんけど」「そういえば高額納税者リストは廃止されてましたね↓」「公開する理由がよくわからないな〜」「APIで個別に公開しても一気にリストを取得する人はいそうですけどね」

参考: 高額納税者公示制度 - Wikipedia

追いかけボイス:「公開されてないと登録事業者を税務署が紐付け可能な形で確認できなくなって適格請求書が発行できないので、インボイス制度の施行上は公開が必要ですね(用途は違いますが法人番号のようなものだと思っています)」「一応確認する限り、個人事業者の場合は希望しなければ住所や屋号は非公表にできるらしいです」

週刊Railsウォッチについて

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

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

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

なお、7.1.0のマイルストーンのissueが残り1つになっていました。

参考: 7.1.0 Milestone

つっつきボイス:「マイルストーンはissue残りの目安程度のものですし、今のところ7.1.0のリリース予定はアナウンスされていないので、まだ先でしょうね」「そういえば7-1-stableブランチもまだできていませんでした」

🔗 トランザクション内に同一モデルのインスタンスが複数ある場合にどのインスタンスからコールバックを呼び出すかを変更

  • トランザクション内で指定のレコードを保存するときは最新のインスタンスでトランザクションコールバックを実行する

1つのトランザクション内で複数のActive Recordインスタンスが同じレコードを変更する場合、そのうちの1つだけがafter_commitafter_rollbackを実行する。
Railsでどのインスタンスがコールバックを受け取るかを指定できるよう、config.active_record.run_commit_callbacks_on_first_saved_instances_in_transactionコンフィグが追加された。フレームワークはデフォルトで新しいロジックを使うよう変更された。

config.active_record.run_commit_callbacks_on_first_saved_instances_in_transactiontrueの場合は、インスタンスのステートがstaleしていても(=古くなっても)、最初に保存したインスタンスでトランザクションコールバックが実行される。
これがfalseの場合は7.1からフレームワークのデフォルトになるが、トランザクションコールバックはステートが最新のインスタンスで実行される。インスタンスは以下のように選択される。

  • 一般に、トランザクションコールバックは最新のインスタンスで実行され、トランザクション内で指定のレコードを保存する。
  • ただし例外が2つある。
    • トランザクション内でレコードを作成して別のインスタンスで更新すると、after_create_commitは2番目のインスタンスで実行される。これは、インスタンスのステートに基づいてナイーブに実行されるafter_update_commitコールバックの代わりとなる。
    • レコードがトランザクション内で削除されると、after_destroy_commitコールバックは最後に削除されたインスタンスで実行される。これは、たとえstaleしたインスタンスがその後更新を行ったとしても同様で、この更新はどの行にも影響しない。

Cameron Bothner and Mitch Vollebregt
同Changelogより


つっつきボイス:「以下のようにトランザクション内で別々のインスタンスが同じものを参照していて、さらにコールバックが絡んだときの挙動を問題にしているようですね: 例外はあるけど、原則として最新のインスタンスでコールバックを呼ぶようになった」「修正前の挙動が直感に反していたので修正したいということみたい」

# 同PRより: 既存の挙動
Product.transaction do
  Product.find(id).update!(title: "T-Shirt")
  Product.find(id).update!(description: "A cool T-shirt")
end
# トランザクションがコミットされると、`after_update_commit`は
# 最初のインスタンスでトリガーされ、ここには新しいdescriptionが入らない

Product.transaction do
  Product.find(id).update!(title: "T-Shirt")
  Product.find(id).destroy!
end
# トランザクションがコミットされると、productが削除されていても
# `after_update_commit`がトリガーされる

参考: Active Record コールバック - Railsガイド

「コールバックの挙動は割と厄介ですけど、トランザクション内にインスタンスが複数あるのがさらに厄介」「同じレコードに対して複数回処理をするなら、普通は上の例のようにfindし直して新しいインスタンスを作ったりせずに、取得済みのインスタンスを流用すると思うのでこうしたケースは踏まないと思いますが、何かの拍子でこの問題を踏んでた可能性はありそう」「find(id)を2回書いたりしませんよね」

🔗 テーブル名の長さに上限を設定

テーブル名が長い場合、主キーインデックス名を生成するときにPostgreSQLはテーブル名をさらに切り詰めてから_pkeyサフィックスを追加することで63バイト以内に収める。
既にActive Recordには別の名前制約に用いる定数があり、以下のようなさまざまな場所でインデックス名をチェックしている(ただしすべてを網羅しているわけではない)。

# https://github.com/rails/rails/blob/9e0320790142185c1724c3d6655debea43fc23e2/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L341
validate_index_length!(table_name, new_name)

テーブル名や外部キーなどの名前(少なくともテーブル名)については、古いマイグレーションでの後方互換性を考慮しつつ、別の別のプルリクで同様の実装をするとよいのではないか。
@@nvasilevski
同PRより


つっつきボイス:「テーブル名の長さはたまに厄介なことになる: 中間テーブルとかでRails wayな命名をするとやたら長いテーブル名になってしまって、複合インデックスをRailsに生成させたりすると、長すぎるので生成できないって怒られたりしますね」「そういうときは自分で名前を付けるしかなくてションボリする」「テーブルの主キーに自動付与されるindex名のサフィックスが_pkeyになるけど、その文字列を含めて63バイト以内というのは文字数制限的に厳しいかなと思うこともありますね」

# activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb#L1589
        def validate_table_length!(table_name)
          if table_name.length > table_name_length
            raise ArgumentError, "Table name '#{table_name}' is too long; the limit is #{table_name_length} characters"
          end
        end

🔗 SQLiteのdatabase.ymlにデフォルトで:strictオプションを追加


つっつきボイス:「:strictにすると、二重引用符で囲んだ文字列リテラルをSQLiteで無効にするのね」

SQLite3Adapterでstrict文字列モードを有効にすると、二重引用符で囲まれた文字列リテラルが無効になる。
SQLiteの二重引用符で囲まれた文字列リテラルにはいくつか妙な癖がある。
二重引用符で囲まれた文字列リテラルは、最初は識別子名として認識しようと試みるが、存在しない場合は文字列リテラルとして認識する。このせいで、名前をタイポしても気づけない。
たとえば、存在しないカラムのインデックスすら作成できてしまう。詳しくはSQLiteドキュメントを参照。
この振る舞いを止めたい場合は、以下で無効にできる。

# config/application.rb
config.active_record.sqlite3_adapter_strict_strings_by_default = false

修正: #27782
fatkodima, Jean Boussier
同Changelogより

参考: SQLiteのクォートにまつわる奇妙な仕様 | 徳丸浩の日記

「SQLiteの型の扱いは他のRDBMSといろいろ違っていた覚えがあるけど、普段あまりSQLiteを使ってなくてすぐに思い出せない」「自分も普段は使ってないな〜」「たまにシェルスクリプトでSQLが必要になったときにSQLiteファイルを中間データとして使うぐらいで、環境にlibsqlite3などがデフォルトで入っていないことも多いんですが、SQLiteそのものはいいものだと思いますし、現代ではOSのファイルキャッシュがよく効くのでSQLiteを使う後押しにもなりますね」

参考: libsqlite3
参考: SQLite - Wikipedia

🔗 Active Recordリレーションのresetcache_versionがリセットするよう修正

現在は、リレーションオブジェクトで#resetが呼び出されてもcache_versionインスタンス変数はリセットされない。これは、データが正しいのにリレーションが古いcache_versionを報告して混乱やバグの元になる。リレーションの他のステートと共にこのインスタンス変数もリセットすれば修正できる。
同PRより


つっつきボイス:「#resetでキャッシュバージョンがリセットされていなかったのが修正されたらしい」「実際のステートと見かけ上のステートが食い違ってたんですか」

# activerecord/lib/active_record/relation.rb#L709
    def reset
      @future_result&.cancel
      @future_result = nil
      @delegate_to_klass = false
      @to_sql = @arel = @loaded = @should_eager_load = nil
      @offsets = @take = nil
      @cache_keys = nil
+     @cache_versions = nil
      @records = nil
      self
    end

参考: reset -- ActiveRecord::Relation

🔗 (PostgreSQLのみ)EXCLUDE制約のサポートが追加

これは、#31323と同様にActive RecordのマイグレーションやスキーマダンプでEXCLUDE制約をサポートする。

add_exclusion_constraint :invoices, "daterange(start_date, end_date) WITH &&", using: :gist, name: "invoices_date_overlap"
remove_exclusion_constraint :invoices, name: "invoices_date_overlap"

同PRより


つっつきボイス:「お、ぽすぐれのEXCLUDE制約が使えるようになった」「マイグレーションなどで使えるんですね」「さすがに更新多いな」「今までも生SQLを書けばできるし、それで十分という考え方もありだと思いますけど、PostgreSQLの機能を積極的に使えるように更新されるのはいい👍」

参考: PostgreSQL Documentation 14 CREATE TABLE -- EXCLUDE

🔗 change_column_nullにブーリアン以外の値を渡すとエラーになるように修正

現在は、change_column_nullにブーリアン以外の値を渡すとtruthyとして扱われてカラムがnullableになる。この動作はおそらく嬉しくないだろう。自分もこれまで何度か見かけたが、たとえばchange_column_nullchange_column_defaultのシグネチャは同じだと仮定する人がいた。

change_column_default(:posts, :state, from: nil, to: "draft")
change_column_null(:posts, :state, from: true, to: false)

このマイグレーションを読むと、カラムのデフォルトは"draft"になってnullを受け付けなくなると思うのが普通だろう。しかし実際には"null"の{ from: true, to: false }引数はtruthyなのでカラムはnullを受け付けるようになる。マイグレーションの結果を注意深く見なければ見落とすかもしれない。
これは、change_column_nullの第3引数にtruefalseしか渡せないようにし、それ以外のものを渡したらエラーを発生することで防げると思う。このプルリクでは、Rails 7.1以降に作成されるマイグレーションを対象にこれを実装している。
同PRより


つっつきボイス:「いつもchange_column使ってたのでchange_column_nullというメソッドがあるって知らなかった」「見たことあったかも」「Rubyはnilfalse以外はデフォルトでtrueと評価するので、明示的にtruefalseしか渡せないように修正した、なるほど」

参考: change_column_null -- ActiveRecord::ConnectionAdapters::SchemaStatements
参考: change_column -- ActiveRecord::ConnectionAdapters::SchemaStatements
参考: Truthy (真値) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

🔗 #field_nameヘルパー呼び出しでobject_name引数にnilを渡せるようになった

ActionView::Helpers::FormTagHelper#field_name calls呼び出しで、fieldsfields_forヘルパーで構築されたインスタンスのobject_name引数がnilになる可能性もある。たとえば、以下はundefined method empty?' for nil:NilClass'例外になる。

<%= fields do |f| %>
  <%= f.field_name :body %>
<% end %

これらの呼び出しをガードするために、このメソッドのString#empty?呼び出しをObject#blank?に置き換えた(NilClass#empty?は定義されていないため)。
同PRより


つっつきボイス:「ビューのタグヘルパーの修正か」「field_nameobject_name引数にnilを渡せるようにしたことで上のfields doendブロックのように短く書けるようになった」「お〜、フィールド名を書かなくていいんですね」「fields_forだと省略できないのか」「コントローラ側でオブジェクトを既に展開済みでfields_forに名前を渡す必要もないようなときに簡潔に書けるのはいい👍」

参考: field_name -- ActionView::Helpers::FormTagHelper
参考: fields_for -- ActionView::Helpers::FormHelper

🔗Rails

🔗 GitLabがRailsにこだわる理由


つっつきボイス:「ニュースサイトにGitLab CEOのSid Sijbrandijさんが寄稿した記事で、YassLabの安川さんに教えていただきました」「GitLabがRailsを採用してモジュラーなモノリスを設計として選んだ理由をCEO自ら解説している感じですね👍」


「GitHubはマイクロソフトに買収されてから他の競合サービスをさらに引き離しましたけど、GitLabもものすごく頑張っているし実際に大きく成功しているのが凄い」「GitLabをセルフホスティングしてリポジトリを一般公開している人達が少ないので一見そこまで使われていない様に見えるかもしれませんが、privateなリポジトリサーバーとしては企業等でかなり使われていると思うので、ユーザー数は日本でもかなりいるんじゃないかな」「BPS社内もメインのリポジトリはGitLabサーバーですね」

参考: GitLab は単一のアプリケーションとして提供される DevOps プラットフォームであり | GitLab


「記事の途中でJavaやPHPと比較しているあたりは議論を呼びそう」「たぶん昔のPHPとJavaが念頭にあるんじゃないかな?」「昔のPHP 4の頃は相当きつかったけど、今のPHPはstrictモードで書けばそうそう乱雑にはならないと思いますけどね」「今もそういう書き方をしている人がいるとつらい」

参考: PHPにおけるstrictモードを使った厳密な型宣言

「自分がPHPを勉強した頃にクラスが導入されましたけど、当初はメソッドをクラスにまとめた程度の構文で、コンストラクタすらなかった」「今のPHPはコンストラクタやデストラクタもあるし型ヒントも渡せるようになりましたね」

参考: PHP: コンストラクタとデストラクタ - Manual
参考: PHP: 型宣言 - Manual

🔗 RailsのHotwireで確認ダイアログをTurboでカスタマイズする(Ruby Weeklyより)


つっつきボイス:「Turboでカスタム確認ダイアログを表示するというシンプルなscreencastですね」

🔗 Rails7へのアップグレードのハマりどころ


つっつきボイス:「BPS社内でもRails 7アップグレードをやってるプロジェクトがありますね」「アップグレードのどこでハマるかは、プロジェクトでどの機能を使っているかで大きく変わりますね: たとえばActive Storageを使っているプロジェクトは、バージョンアップに伴うActive Storageのスキーマ変更でハマります」「たしかに」「実際上の記事はWebpackerを使っていないので、そこではハマっていない」「もし使ってたらWebpackerを引っ剥がすとかShakapackerに乗り換えるとかしないといけなくなるでしょうね」

Rails: Webpacker v5からShakapacker v6へのアップグレードガイド(翻訳)

「アップグレード記事を読むときは、その記事で扱っているRailsが自分のプロジェクトの構成に近いかどうかもチェックしておきたい」「普段からRailsの改修やアップグレードを追いかけている人が身近にいれば相談できるけど、そうでないときは要注意ですね」

🔗 「プロを目指す人のためのRuby入門」のテストコードをRSpecに書き換える

つっつきボイス:「jnchitoさんの動画付き記事」「そうそう、チェリー本のテストコードはminitestなんですよ」

🔗 その他Rails

つっつきボイス:「Matzのリツィートで知りました」「Railsコアコミッターの@rafaelfrancaさんだ」「7年の間にチームが1人から50人に増えたのって凄い」「最初はどこの組織も1人から始まりますけどね」


「ところで、会社の規模が大きくなると求められるスキルが変わってきますよね」「人数が少ないうちはジェネラリストが重宝されるけど、人数が増えてくるとだんだんスペシャリストが求められるようになってきたり」「会社の規模が変わって、会社から求められるスキルと自分のやりたいことがマッチングしなくなってきたら会社を離れるというのは選択肢として普通にありだと思います」


前編は以上です。

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

週刊Railsウォッチ: Rubyの実行モデル解説記事、shale gem、HTTP/3がRFC 9114にほか(20220614後編)

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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