こんにちは、hachi8833です。つっつき会を行った11/09はRubyWorld Conference 2023の初日でした。盛況&無事終了おめでとうございます🎉
#RubyWorld Conference 2023にご参加いただいた皆様ありがとうございました!
来場者数:延べ780名(1日目:474名、2日目:360名) pic.twitter.com/Lk4E2QvWQ4— RubyWorld Conference (@RubyWorldConf) November 10, 2023
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 MariaDBでRETURNING
をサポート
このプルリクは、フィードバックに対処した#49315をrebaseしたもの。
この変更は、MySQLやMariaDBのuuid
データ型をサポートする自分の取り組み(#49752コメント)で必要となる。
INSERT
の後で動的なデータベースデフォルト設定が反映されることをテストするテストケースを追加した。insert_all
などのメソッドでも:returning
をサポートしているが、これらは既にテストされている。rails/activerecord/test/cases/insert_all_test.rb
# Line 90 in [820fbd9](https://github.com/rails/rails/commit/820fbd9102a5488f3bfcc91ec35324b7076cc83e) skip unless supports_insert_returning?
クローズされるissue: #49315
cc: @nvasilevski、@adrianna-chang-shopify
同PRより
つっつきボイス:「INSERT
文のRETURNING
はPostgreSQLでは昔からサポートされていますね」「最近だとSQLite3のRETURNING
もRailsでサポートされましたね(#49290)」
「基本的な質問ですけど、RETURNING
は何ができるんでしょうか?」「SELECT
文が結果を返すのと似た感じで、INSERT INTO
文で返したい戻り値を指定できるオプションですね: INSERT INTO
文のRETURNING
オプションがとても便利なのは、idのような自動生成される結果も取れること」「あ、自動生成される値も返せるんですか」「仮にActive Recordでたとえると、もしこの機能がなければ、レコードセットをcreate
してから、生成されたidも含む結果を取りたい時にもう一度select
しなければならなくなりますけど、RETURNING
がサポートされていれば、確定したid値を含む結果を取得するところまでできるので、それを受け取って何かしたりできるのが、めちゃくちゃ便利、しかもトランザクションを別途張らなくていい」「なるほど、RailsがMariaDBでもその機能をサポートしたということなんですね」
参考: INSERT -- PostgreSQL 15 ドキュメント
RETURNING
句を指定すると、INSERT
は実際に挿入された(あるいはON CONFLICT DO UPDATE
句によって更新された)各行に基づいて計算された値を返すようになります。 これは、通番のシーケンス番号など、デフォルトで与えられた値を取り出す時に主に便利です。 しかし、そのテーブルの列を使用した任意の式を指定することができます。RETURNING
リストの構文はSELECT
の出力リストと同一です。 挿入または更新に成功した行だけが返されます。 例えば、行がロックされていて、ON CONFLICT DO UPDATE ... WHERE
句の conditionが満たされなかったために更新されなかった行は返されません。
INSERT -- PostgreSQL 15 ドキュメントより
「そうそう、この記事にもありますけど、MySQLだとLAST_INSERT_ID()
というそれ用の関数でやったりしていましたね↓」
参考: MySQLではinsertで生成されるIDが帰ってこない問題へのアプローチ #Ruby - Qiita
後で探すと、MariaDBのINSERT
にはRETURNING
オプションの説明がありましたが、MySQL 8.2にあるのはJSON型で型を指定するRETURNING
オプションだけのようです。
参考: INSERT...RETURNING - MariaDB Knowledge Base
参考: MySQL :: MySQL 8.2 Reference Manual :: 12.17.3 Functions That Search JSON Values
🔗 attr_internal_define
メソッドを最適化
@
プレフィックスは常に削除されるので、必要ではない場合もある。後方互換性の理由から、当面この
@
プレフィックスを処理するが、積極的に削除して非推奨警告を表示するようにする。
同PRより
つっつきボイス:「公式更新情報にはattr_internal_naming_format=(format)
の内部が最適化されたとありますね」「どこかで見かけたような気がするメソッド名」「内部用のメソッドだし、インスタンス変数の使われ方もわからないけど、attr_*
でアクセサを作るようにしたことで@
を書かなくても済むように最適化したということなのかな🤔」
# activesupport/lib/active_support/core_ext/module/attr_internal.rb#L40
+ private
def attr_internal_define(attr_name, type)
- internal_name = attr_internal_ivar_name(attr_name).delete_prefix("@")
+ internal_name = Module.attr_internal_naming_format % attr_name
# use native attr_* methods as they are faster on some Ruby implementations
public_send("attr_#{type}", internal_name)
attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
alias_method attr_name, internal_name
remove_method internal_name
end
🔗 Active Storageのhas_one_attached
やhas_many_attached
にservice
オプションをprocとして渡せるようになった
動機/背景
このプルリクを作成した理由は、
has_one_attached
メソッドとhas_many_attached
メソッドのservice
キーワード引数が値としてシンボルしか受け取れないため。 このプルリクによって、クラスレベルのコンテキストでservice
を定義できるようになる。しかし自分たちの要件の1つとして、ユーザーの地域に基づいてファイルをアップロードしたいというのがある(地域によっていくつかの規制があるため)。そういうわけで、このプルリクでは、
service
をインスタンスレベルのコンテキストで定義可能にするため、service
をProcとしても受け取れるように変更する。詳細
このプルリクは、
has_one_attached
メソッドとhas_many_attached
メソッドのロジックを以下のように変更する。
service
キーワード引数にシンボルとprocのどちらでも渡せるようにする- シンボルの場合、サービスの設定は起動時にバリデーションされるが、procの場合は実行時にバリデーションされる
同PRより
つっつきボイス:「has_one_attached
やhas_many_attached
メソッドのservice
オプションにprocも渡せるようにした↓」「Active Storageで添付ファイルの保存先を今まではservice: :s3
みたいにシンボルしか指定できなかったのね」「プルリクを上げた人たちは地域によってアップロード先のストレージを使い分けたいらしい」「そういうニーズもあるんですね」
# activestorage/test/models/attached/many_test.rb#L775
test "attaching a new blob from an uploaded file with a service defined at runtime" do
extra_attached = Class.new(User) do
def self.name; superclass.name; end
has_many_attached :signatures, service: ->(user) { "disk_#{user.mirror_region}" }
def mirror_region
:mirror_2
end
end
@user = @user.becomes(extra_attached)
@user.signatures.attach fixture_file_upload("cropped.pdf")
assert_equal :disk_mirror_2, @user.signatures.first.service.name
end
🔗 trilogy_adapterとmysql2_adapterのquote_string
メソッドをabstract_mysql_adapter.rbに移動
このメソッド(
quote_string
)の実装は、trilogyアダプタとmysql2アダプタで完全に同じなので、抽象mysqlアダプタで明示的に共有することでメリットを得られる。これはコスメティックな変更に近いが、行うべき重要な変更だと思う。多くのアプリケーションが
mysql2
からtrilogy
に移行することが予想されており、問題が発生したらtrilogyアダプタとmysql2アダプタで違いを調査することになる可能性がある。完全に同じメソッドがtrilogyアダプタとmysql2アダプタにあってもデバッグで役に立たないし、共通クラスに置くことで振る舞いが同じことが明確になる。PostgreSQL
公平のために述べておくと、postgresqlアダプタにも完全に同じ実装がある。しかし、潜在的に使われる可能性のあるアダプタに適した実装ではないと思うので、これも抽象アダプタに移動してよいものか自信がない。
rails/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
# Lines 74 to 78 in [403447d](https://github.com/rails/rails/commit/403447d06165b0e9a13f0942f11b97e2f9d426b8) def quote_string(s) # :nodoc: with_raw_connection(allow_retry: true, materialize_transactions: false) do |connection| connection.escape(s) end end
自分がコードを正しく追えていれば、上を抽象アダプタに移動すると
Quoting#quote_string
は使われなくなるだろう。rails/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
# Lines 88 to 90 in [403447d](https://github.com/rails/rails/commit/403447d06165b0e9a13f0942f11b97e2f9d426b8) def quote_string(s) s.gsub("\\", '\&\&').gsub("'", "''") # ' (for ruby-mode) end
同PRより
つっつきボイス:「trilogyアダプタとmysql2アダプタにまったく同じメソッドがあるなら抽象クラスに引っ越させようという話、わかる」「リファクタリングですね」「trilogy_adapterとmysql2_adapterがあって、さらにabstract_mysql_adapter.rbがあるのが、理にはかなっているけどちょっと面白い構成」「base_mysql_adapterではなかった」
🔗 アップデートされたTrixがSprocketsでうまく動かない問題を修正
動機/背景
Trixが1.3.1から2.0.4に更新されたとき(fab1b52)、UMDバンドル(UMDバンドルで使われるベンダリングされた1.3.1ファイル)ではなく、2.0.4のESM(ESモジュール)バンドルが使われるようになった。これにより、SprocketsでTrixを使おうとすると問題が生じた。理由は、ESMバンドルでは変数がファイルスコープであるかのように宣言されるが、Sprocketsではグローバルスコープであるとみなされるため。
詳細
このコミットでは、TrixのESMバンドルをUMDバンドルに置き換える(これで2.0.4から2.0.7にアップグレードされる)ことで問題を修正する。
また、Rakeタスクも追加された。これは以前a42863fでImportmap::Packager
を用いる自動ベンダリング向けにガイドのRakeタスクに追加されたものと似ている。追加情報
つっつきボイス:「Trix使っている人向けの修正」「TrixはAction Textと連携しているWYSIWYGエディタでしたね」「UMDって何だろうと思ったらUniversal Module Definitionでした↓」「CJSはCommonJS」
参考: CJS, UMD, ESMとは?その違い。 | milestones
参考: Action Text の概要 - Railsガイド
🔗 rails new
で生成されるDockerfileがKubernetesでエラーになる問題を修正
動機/背景
rails new
で生成されるDockerfileでは、デフォルトのユーザー名とグループ名をUID:GID
ではなく名前で設定していた。Kubernetes podで以下のように
runAsNonRoot: true
というセキュリティコンテキストで実行するとする。apiVersion: v1 kind: Pod metadata: name: rails-rootless labels: name: rails-rootless namespace: default spec: containers: - name: rails-rootless image: rails-rootless securityContext: runAsNonRoot: true
Kubernetesで
container has runAsNonRoot and image has non-numeric user (rails), cannot verify user is non-root
エラーが発生する。詳細
このプルリクでは、数値の
UID:GID
を使うことで問題を修正する。ビルド時には、RAILS_UID
やRAILS_GID
ビルド引数を渡すことでオプションでカスタマイズできる。現在の振る舞いを維持するため、デフォルト値は1000:1000
に設定される。
同PRより
つっつきボイス:「Kubernetes rootless podsで動かすために、可読性が落ちるのを承知でUIDとGIDを名前から数値に変更した、これは仕方なさそう」「BPSで扱ってるプロジェクトではKubernetesの採用事例はあまりありませんが、使っている所はガッツリ使ってるんでしょうね」
参考: Kubernetes | Rootless Containers
🔗 マイグレーションファイル作成に失敗したら0以外のステータスを返すよう修正
動機/背景
マイグレーションファイルの生成でエラーが発生しても、ステータス0で終了してしまう。
% bin/rails generate migration ChangeFieldToUsers invoke active_record conflict db/migrate/20231015071420_change_field_to_users.rb Another migration is already named change_field_to_users: /path/to/rails/db/migrate/20231015020422_change_field_to_users.rb. Use --force to replace this migration or --skip to ignore conflicted file. % echo $? 0
これは失敗であるにもかかわらずステータスが成功になるので、スクリプトなどで成功と見なされてしまう。このプルリクは、失敗時の終了ステータスとして1を返すように修正する。
% bin/rails generate migration ChangeFieldToUsers invoke active_record conflict db/migrate/20231015071420_change_field_to_users.rb Another migration is already named change_field_to_users: /path/to/rails/db/migrate/20231015020422_change_field_to_users.rb. Use --force to replace this migration or --skip to ignore conflicted file. % echo $? 1
追加情報
終了ステータスが常に0になる変更は
c49bb4d
で行われた。当時のコミットメッセージ:どのコマンドで失敗時に0以外の値を返したいかを調査すべきだが、とりあえず警告を無効にして古い振る舞いを維持しておくことにする。
しかし失敗した場合は常に0以外の値を返すべきだろう。0にすべき場合はおそらくない。
同PRより
つっつきボイス:「失敗したのに0(=成功)を返して終了すると、CIとかスクリプトで呼び出したときに判定できなくなるヤツだ」「よくぞ見つけたという感じですね」
🔗 SQLite3にsupports_deferrable_constraints?
を追加
動機/背景
SQLiteは機能豊富なデータベースエンジンであり、productionでの利用は増える一方である。その機能を幅広く開発者に提供するにはRailsでのサポートが必要。
PostgreSQLではdeferred(延期)外部キーをサポートしているが、(SQLite自身も少なくとも2011年から)deferred foreign keysをサポートしている。
詳細
supports_deferrable_constraints?
の完全なコントラクトを実装することで、add_reference
メソッドとadd_foreign_key
メソッドのforeign_key
オプションに:deferrable
キーを追加することで外部キーをdeferredにできるようになる。add_reference :person, :alias, foreign_key: { deferrable: :deferred } add_reference :alias, :person, foreign_key: { deferrable: :deferred }
このプルリクでは、SQLite3Adapterで以下を実装することでフルサポートを追加している。
ActiveRecord::ConnectionAdapters::SQLite3::SchemaCreation#visit_AddForeignKey
ActiveRecord::ConnectionAdapters::SQLite3::SchemaCreation#visit_ForeignKeyDefinition
ActiveRecord::ConnectionAdapters::SQLite3Adapter#supports_deferrable_constraints?
ActiveRecord::ConnectionAdapters::SQLite3::SchemaStatements#assert_valid_deferrable
また、以下も変更している。
ActiveRecord::ConnectionAdapters::SQLite3::SchemaStatements#add_foreign_key
ActiveRecord::ConnectionAdapters::SQLite3Adapter#foreign_keys
追加情報
ActiveRecord::ConnectionAdapters::SQLite3Adapter#foreign_keys
メソッドが適切に動作するために、CREATE TABLE
SQLを取得することでどの外部キーがdeferrableでどの外部キー制約がdeferredかを解析するクエリを追加する必要があった。このユースケースをサポートするため、table_structure_with_collation
メソッドが現在依存しているtable_structure_sql
メソッドを抽出した。
同PRより
SQLiteのdeferred Foreign Keyもサポートされたそうです。
つっつきボイス:「最近RailsのSQLite3を強化しているfractaledmindさん(ウォッチ20231004など)がまた新機能を追加しました」「SQLite強い人出た」「PostgreSQLの新機能をRailsで使えるようにしたりするのと同じノリで、SQLite3でもDEFERRABLE INITIALLY
を使って外部キーでdeferrableをサポートしたということですね👍: PostgreSQLのも外部キーの制約をdeferrableにできる機能だった」
# activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb#L5
module SQLite3
class SchemaCreation < SchemaCreation # :nodoc:
private
+ def visit_AddForeignKey(o)
+ super.dup.tap do |sql|
+ sql << " DEFERRABLE INITIALLY #{o.options[:deferrable].to_s.upcase}" if o.deferrable
+ end
+ end
+
+ def visit_ForeignKeyDefinition(o)
+ super.dup.tap do |sql|
+ sql << " DEFERRABLE INITIALLY #{o.deferrable.to_s.upcase}" if o.deferrable
+ end
+ end
+
🔗 PostgreSQL向けのset_constraints
ヘルパーを追加
PostgreSQLに
set_constraints
ヘルパーが追加された。Post.create!(user_id: -1) # => ActiveRecord::InvalidForeignKey Post.transaction do Post.connection.set_constraints(:deferred) p = Post.create!(user_id: -1) u = User.create! p.user = u p.save! end
Cody Cutrer
同CHANGELOGより
つっつきボイス:「お、PostgreSQLのset_constraints
をサポートしたんですね: こういうのを設定した後コネクションプールに残ると困るので、終わったら片付けてくれるかが気になったけど、上のサンプルコードを見るとトランザクション内にset_constraints(:deferred)
を書く分には大丈夫っぽいのかな」「PostgreSQLのドキュメントにも"現在のトランザクション"とあるので↓、そういう使い方なら大丈夫そうですね」
SET CONSTRAINTS
は、現在のトランザクションにおける制約検査の動作を設定します。IMMEDIATE
制約は、1つの文の実行が終わるごとに検査されます。DEFERRED制約
は、トランザクションがコミットされるまで検査されません。 全ての制約は、IMMEDIATE
かDEFERRED
のどちらかのモードを持ちます。
PostgreSQL 15ドキュメントSET CONSTRAINTS
より
🔗 assert_changes
とassert_no_changes
のエラーメッセージを改善
assert_changes
のエラーメッセージでは.inspect
でオブジェクトが表示されるようになり、nil
と空文字列の違いや、文字列とシンボルの違いなどがわかりやすくなった。
assert_no_changes
のエラーメッセージでは、実際の値が表示されるようになった。pcreux
同CHANGELOGより
つっつきボイス:「assert_changes
やassert_no_changes
が失敗したときのエラーメッセージにオブジェクトを表示するようになった: 地味だけど便利な改修👍」
# activesupport/lib/active_support/testing/assertions.rb#L197
unless from == UNTRACKED
- error = "Expected change from #{from.inspect}, got #{before}"
+ error = "Expected change from #{from.inspect}, got #{before.inspect}"
error = "#{message}.\n#{error}" if message
assert from === before, error
end
after = exp.call
error = "#{expression.inspect} didn't change"
- error = "#{error}. It was already #{to}" if before == to
+ error = "#{error}. It was already #{to.inspect}" if before == to
error = "#{message}.\n#{error}" if message
refute_equal before, after, error
unless to == UNTRACKED
- error = "Expected change to #{to}, got #{after}\n"
+ error = "Expected change to #{to.inspect}, got #{after.inspect}\n"
error = "#{message}.\n#{error}" if message
assert to === after, error
end
🔗Rails
お詫びと訂正(2023/11/15)
Rails6.1のサポートについては次のメジャーバージョン(Rails8.0)が出るまでは続く、と書いてあるので参照しているサイトが間違えているのではないかと思います。 https://t.co/Dcpq1MmQiP
— willnet (@netwillnet) November 15, 2023
記事公開後に上のご指摘をいただきました。ご指摘ありがとうございます。
記事見出しを以下のように変更し、元のトピックを折り畳みにしました。確認漏れ大変失礼いたしました。お詫びして訂正いたします。
週刊Railsウォッチ: Rails 6.1のサポートは来年6月1日終了ほか(20231114前編)
↓
週刊Railsウォッチ: MariaDBのRETURNINGオプションをサポートほか(20231114前編)
補足情報: Rails 7.1リリース前の9/22に、fb6c600でmainブランチのメンテナンスポリシーが更新されて6.1.zがセキュリティ修正の対象から削除されましたが、これは今後のRails 7.2が対象です。現行の7-1-stableブランチでは引き続き6.1.zがセキュリティ修正の対象となっています。
▶修正前の記事(クリックで展開)
🔗 Rails 6.1のサポート終了は2024年6月1日
つっつきボイス:「この間BPSの社内Slackに貼っていたリンクです」「そうそう、Rails 6.1の終了が来年半ばって、割とすぐなんですよね」「7か月先って半年ちょいですね」「そんなに早いのか〜」「ところで7.0の終了はいつでしょうか?」「上のサイトにはまだEOLは表示されていないので大丈夫👌」
🔗 actual_db_schema: Gitブランチ切り替え時のdbスキーマ変更の手間を軽減(RubyFlowより)
つっつきボイス:「開発中にGitブランチを切り替えたときのスキーマ変更をサポートするgemだそうです」「"ただしマイグレーションはすべてリバーシブルであること"」「こういうツールが欲しい人がいるのはわかるけど、CIでも読み込まれることになるので、チーム全員が使うならまだしも、一部の人しか使わないなら少なくともプロジェクトのGemfileには書きたくないかな」「たしかに、その人しか使わない外部ツールになっている方が無難ですね」「development環境であってもGemfileのgemが増えてくると古くなったときにGitHubのDependabotで警告が鳴りまくったりするので、なるべくシンプルにしておきたい」「ですね」
参考: Dependabot を使う - GitHub Docs
🔗 「Bundlerチョットワカルマニュアル」
つっつきボイス:「jnchitoさんがBundlerについて初心者向けにひととおりまとめてくれた記事です」「bundle exec
を付ける場合と付けない場合とか、定番の話が集約されていると若手に説明しやすいのでありがたい🙏」「"gemのバージョンを完全に固定するときはコメントに理由を書く"、これはたしかにやった方がいい」「"Gemfile.lockは手で編集してはいけない"、これホント」「昔やっちゃってた〜」
「--path vendor/bundle
が不要な件、これが必要になることがあるとすれば、プロジェクト全体をzipで固めてソースコードとして単体イメージでデプロイ/実行したいときぐらいしかないと思うので、通常は不要でしょうね」「以前は何も考えずに--path vendor/bundle
を付けてました😅」「特に現代ならDockerイメージの中にgemが保存されるので、わざわざ書く意味はほぼないと思います」
前編は以上です。
バックナンバー(2023年度第4四半期)
週刊Railsウォッチ: Kaigi on Rails発表「Simplicity on Rails」を見るほか(20231107)
- 20231025後編 ShopifyのWebAssemblyツールチェインRuvyほか
- 20231024前編 7.1アップグレードガイドにActive Record暗号化設定の注意事項が追加ほか
- 20231018後編 Kaigi on Rails 2023関連イベント情報公開、複合主キーのlocality解説記事ほか
- 20231017前編 Active Storageのしくみを詳しく解説するDiscussion投稿ほか
- 20231011 Rails 7.1.0リリース、YARPがprismにリネームほか
- 20131004 Rails 7.1.0.rc1と7.1.0.rc2がリリース、SQLite3コンフィグの最適化ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)