かみぽわーる

kamipo's blog

Rails 6.0でDeprecatedになるActive Recordの振る舞い3つ

Deprecatedにした経緯というか背景が伝わってるのかどうかアレだと思ったので、ここに日本語にて書き記しておく。

Active Recordのuniqueness validatorはデフォルトでcase sensitiveな比較をするんですが、これが、文字列のデフォルトのcollationがcase insensitiveMySQLと相性が悪く、DB上のUNIQUE制約と一致しない振る舞いだったりINDEXが効率よく使えずDBが死ぬみたいな問題を引き起こしていました。

例: 本当にあったRailsの怖い話

僕も主に仕事のコードレビューで過去に何度もこの問題を指摘してきたわけですが、去年ぐらいにmakimotoさんにこれRails側でなんとかならないんですか的なこと言われてそりゃ同僚にRailsコミッターおったら仕事で遭遇したRails側のあらゆる問題はRails側でなんとかなってほしいわなって思ったので気合いでなんとかすることにしたという次第です。

いないとは思いますが、MySQLなどでデフォルトのcollationがcase insensitiveな文字列をあえてわざとcase sensitiveな比較をしていた奇特なユーザーの皆様は今後明示的に case_sensitive: true オプションを指定していただくことになります。

where.not(a: ..., b: ...) の結果が where(a: ..., b: ...) の補集合になるようになります。逆にいうと6.1になるまでは where.not(a: ..., b: ...) の結果は where(a: ..., b: ...) の補集合になるとは限りません。

想像してほしいんですが、血液型B型の男性(私です)の集合があったとして、それのNOTを取った集合は血液型B型の男性以外になってほしいと思いませんか?僕は思います。現状、Active Recordの where.not は血液型B型の男性という集合を反転した結果として、男性でもなく血液型B型でもない集合を返します。もしこれが想像していた振る舞いと違う場合、アプリケーションは暗黙のうちにバグってる可能性があるので where.not を使っている皆さんは手元のコードをよく確認してみるのがよいのではないかと思います。

これは説明がむずかしいというか面倒なんですけど、たとえばTopicのツリー型の掲示板みたいなものがあったとして、トップレベルのTopic、かつその中でも子スレッドを持つものをscopeとして定義したいとします。

class Topic < ActiveRecord::Base
  scope :toplevel, -> { where(parent_id: nil) }
  scope :children, -> { where.not(parent_id: nil) }
  scope :has_children, -> { where(id: Topic.children.select(:parent_id)) }
end

# Works as expected.
Topic.toplevel.where(id: Topic.children.select(:parent_id))

# Doesn't work due to leaking `toplevel` to `Topic.children`.
Topic.toplevel.has_children

子スレッドを持つという条件を has_children として抽出して利用したいわけですが、これは現状ほぼ意図した通りには動きません。というのもscopeのメソッドチェインは条件が累積したrelationを伝搬させるのにクラスグローバルな状態を汚染する実装方法を現状とっており、scope定義内はその汚染されたクラスグローバルな状態の影響を受けるためです。

現在、我々がどのようにしてこの問題を回避しているかというと Topic.unscoped.children.select(:parent_id) のように unscoped をつけるのを忘れないように頑張る、というものですが、6.1からは unscoped をつけるのを忘れないように頑張らなくてもよくなるということになります。

逆にいうと、現状scope定義内でモデルクラスから直接scopeやquery methodsを呼び出してるケースではユーザーの期待に反して意図せず別のscopeの状態が注入されている可能性があるので、今一度手元のコードを確認されるのがよいかと思います。

以上、よろしくお願いいたします。