- Ruby / Rails関連
週刊Railsウォッチ: GitLabがRailsにこだわる理由、Rails7アップグレードのハマりどころほか(20220620前編)
こんにちは、hachi8833です。
インボイス制度の施行のために、年商一千万円を超える全ての法人の所在地、個人事業主の屋号と氏名が、csv で全面的に公開されることになりました。
Web APIで名前の一部だけ返す仕様で良かったはずなのに、国内の全事業者リストを公開するとはね。 https://t.co/bDR77U5ZBZ
— 藤井 太洋, Taiyo Fujii (@t_trace) June 14, 2022
つっつきボイス:「年商一千万円を超える全ての法人と個人事業主が対象、マジで?」「年商は売上のことですね」「あくまで登録リストが公開されるだけで額までは公開されていませんけど」「そういえば高額納税者リストは廃止されてましたね↓」「公開する理由がよくわからないな〜」「APIで個別に公開しても一気にリストを取得する人はいそうですけどね」
追いかけボイス:「公開されてないと登録事業者を税務署が紐付け可能な形で確認できなくなって適格請求書が発行できないので、インボイス制度の施行上は公開が必要ですね(用途は違いますが法人番号のようなものだと思っています)」「一応確認する限り、個人事業者の場合は希望しなければ住所や屋号は非公表にできるらしいです」
🔗Rails: 先週の改修(Rails公式ニュースより)
なお、7.1.0のマイルストーンのissueが残り1つになっていました。
参考: 7.1.0 Milestone
つっつきボイス:「マイルストーンはissue残りの目安程度のものですし、今のところ7.1.0のリリース予定はアナウンスされていないので、まだ先でしょうね」「そういえば7-1-stableブランチもまだできていませんでした」
🔗 トランザクション内に同一モデルのインスタンスが複数ある場合にどのインスタンスからコールバックを呼び出すかを変更
- トランザクション内で指定のレコードを保存するときは最新のインスタンスでトランザクションコールバックを実行する
1つのトランザクション内で複数のActive Recordインスタンスが同じレコードを変更する場合、そのうちの1つだけが
after_commit
やafter_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_transaction
がtrue
の場合は、インスタンスのステートが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リレーションのreset
でcache_version
がリセットするよう修正
- PR: Update AR Relation method to reset cache_version by austenmadden · Pull Request #45342 · rails/rails
現在は、リレーションオブジェクトで
#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_null
とchange_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引数にtrue
かfalse
しか渡せないようにし、それ以外のものを渡したらエラーを発生することで防げると思う。このプルリクでは、Rails 7.1以降に作成されるマイグレーションを対象にこれを実装している。
同PRより
つっつきボイス:「いつもchange_column
使ってたのでchange_column_null
というメソッドがあるって知らなかった」「見たことあったかも」「Rubyはnil
とfalse
以外はデフォルトでtrue
と評価するので、明示的にtrue
とfalse
しか渡せないように修正した、なるほど」
参考: change_column_null
-- ActiveRecord::ConnectionAdapters::SchemaStatements
参考: change_column
-- ActiveRecord::ConnectionAdapters::SchemaStatements
参考: Truthy (真値) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
🔗 #field_name
ヘルパー呼び出しでobject_name
引数にnil
を渡せるようになった
- PR: Support calls to
#field_name
with nilobject_name
by seanpdoyle · Pull Request #45366 · rails/rails
ActionView::Helpers::FormTagHelper#field_name calls
呼び出しで、fields
やfields_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_name
のobject_name
引数にnil
を渡せるようにしたことで上のfields do
〜end
ブロックのように短く書けるようになった」「お〜、フィールド名を書かなくていいんですね」「fields_for
だと省略できないのか」「コントローラ側でオブジェクトを既に展開済みでfields_for
に名前を渡す必要もないようなときに簡潔に書けるのはいい👍」
参考: field_name
-- ActionView::Helpers::FormTagHelper
参考: fields_for
-- ActionView::Helpers::FormHelper
🔗Rails
🔗 GitLabがRailsにこだわる理由
Why We're Sticking with Ruby on Rails at GitLab https://t.co/MK2FSJvoic @sytses @gitlab #Sponsored #rubyonrails
— The New Stack (@thenewstack) June 8, 2022
つっつきボイス:「ニュースサイトに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が自分のプロジェクトの構成に近いかどうかもチェックしておきたい」「普段からRailsの改修やアップグレードを追いかけている人が身近にいれば相談できるけど、そうでないときは要注意ですね」
🔗 「プロを目指す人のためのRuby入門」のテストコードをRSpecに書き換える
どうもこんにちは。チェリー本のテストコードをRSpecに置き換える記事もあります。第1版が対象なので一部第2版に当てはまらない内容もありますが、8割ぐらいは適用可能なはずです😃
【動画付き】「プロを目指す人のためのRuby入門」のテストコードをRSpecに書き換える https://t.co/b8I5VMUH7h
— Junichi Ito (伊藤淳一) (@jnchito) June 15, 2022
つっつきボイス:「jnchitoさんの動画付き記事」「そうそう、チェリー本のテストコードはminitestなんですよ」
🔗 その他Rails
Today I completed 7 years working at @ShopifyEng. In those 7 years I was able to build one of the best open source focused Ruby team from 1 person to ~50. I’m proud of what this team is being able to contribute back to the community so far. More years to come! ♥️
— Rafael França 🇧🇷 (@rafaelfranca) June 16, 2022
つっつきボイス:「Matzのリツィートで知りました」「Railsコアコミッターの@rafaelfrancaさんだ」「7年の間にチームが1人から50人に増えたのって凄い」「最初はどこの組織も1人から始まりますけどね」
「ところで、会社の規模が大きくなると求められるスキルが変わってきますよね」「人数が少ないうちはジェネラリストが重宝されるけど、人数が増えてくるとだんだんスペシャリストが求められるようになってきたり」「会社の規模が変わって、会社から求められるスキルと自分のやりたいことがマッチングしなくなってきたら会社を離れるというのは選択肢として普通にありだと思います」
前編は以上です。
バックナンバー(2022年度第2四半期)
週刊Railsウォッチ: Rubyの実行モデル解説記事、shale gem、HTTP/3がRFC 9114にほか(20220614後編)
- 20220613前編 Hotwireをアプリ構築で学ぶ、Active RecordのDurationとPostgreSQL intervalデータ型ほか
- 20220607後編 Shopifyのlanguage server ruby-lsp、PostgreSQL 15 Beta 1リリースほか
- 20220606前編 BasecampのHotwireページネーション、Query Object、Lograge gemほか
- 20220531 Railsコミュニティアンケート結果発表、書籍『Sustainable Web Development with Ruby on Rails』ほか
- 20220524後編 Railsコアチームとコミッターに新メンバー、ruby-buildでのRust YJITサポートほか
- 20220523前編 Hotwireの用途解説記事、RubyKaigi 2022プロポーザル募集開始ほか
- 20220517後編 rubygemsに「scoped gems」の提案、RSpecのブロック構文ほか
- 20220516前編 Active Modelで属性のパターンマッチをサポート、猫でもわかるHotwire入門ほか
- 20220511後編 Ruby 3.2.0devにRust版YJITがマージ、Docker Compose V2ほか
- 20220510前編 Active RecordにPromiseと非同期集計メソッドがマージ、climate_control gemほか
- 20220419後編 RubyのGCコンパクション改修、jemalloc、ReDoSの自動検出修正ほか
- 20220418前編 RailsConf 2022が5月17〜19日開催、認可機能解説記事ほか
- 20220412後編 HashieでRubyのハッシュを強化、最近のRubyコア解説記事ほ
- 20220411前編 Turbo Railsチュートリアル、Active Recordの「Leaky Abstraction」を削減ほか
- 20220406後編 RBS関連記事、Ruby formatterプロジェクト、Google Cloud Runほか
- 20220404前編 Ruby 3.2.0 Preview 1リリース、Rails向けDocker環境ジェネレータ、scientist gemほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)