【Rails Advent Calendar 2020】大好きな changes が deprecated になるなんて間違っている!!!【2日目】

Rails Advent Calendar 2020 2日目の記事になります。
この記事では最近までずーっっっっと Rails で勘違いしていた事があったのでその事の顛末を簡単にまとめてみたいと思います。

Rails 5.1 から changes 等が deprecated になる?

さて、わたしが Rails を始めた頃ですかね。当時は Rails 5.1 あたりが最新版だったので Rails 5.1 関連の記事を見かけることが多かったと思います。
その頃に『 changes を使うと DEPRECATION WARNING という記事』を見かけて当時あまり Rails に精通してなかったわたしは『へー changes とかって deprecated なんだー』ぐらいにしか思っていませんでした。
実際レビュー等で『 changes とかは deprecated だから使わないほうがいいよ』みたなコメントも見たような気がします、多分。
と、言う感じで長らく『あ〜 changes とかは deprecated だから使わないほうがいいんだなあ』と何も考えずに思っていました。
ちなみにと DEPRECATION WARNING なのは以下のようなメソッドになります(参照

  • attribute_was(attr_name)
  • attribute_change(attr_name)
  • attribute_changed?(attr_name)
  • changed
  • changes
  • changed?
  • changed_attributes
  • changed?

それ、本当に deprecated ですか

changes って deprecated なんだなぁ』という認識はあったんですが実際に changes などと使っても警告などは出ずに『なんで出ないんだろうか』と疑問には思っていました。
で、最近重い腰を上げて調べてみたり調べてもらったりしたのでその経緯を簡単にまとめてみたいと思います。
結論からいうと changes などは最新版では DEPRECATION WARNING ではないので普通に使うことができます。

そもそも何が DEPRECATION WARNING だったのか

そもそも Rails 5.1 でどうなっていたのか確認してみましょう。
まず、 changes を使っただけでは警告は出ません。

pp ActiveRecord::VERSION::STRING
"5.1.0"

class User < ActiveRecord::Base
end

user = User.create(name: "mami")
user.name = "homu"

# ただ使っただけでは警告は出ない
pp user.changes
# => {"name"=>["mami", "homu"]}

では、どういうときに警告が出ていたのかというと after_xxx などのコールバック内で使用した場合に警告が出ていました。

class User < ActiveRecord::Base
  # こっちは警告は出ない
  before_update { pp changes }
  # => {"name"=>["mami", "homu"]}

  # ここで changes を呼び出すと警告が出る
  after_update { pp changes }
  # => {"name"=>["mami", "homu"]}
  # warning:
  # DEPRECATION WARNING: The behavior of `changed_attributes` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_changes.transform_values(&:first)` instead. (called from block in <class:User> at /home/worker/test/ruby/rails/active_record/deprecated_changes/main.rb:44)
  # DEPRECATION WARNING: The behavior of `changes` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_changes` instead. (called from block in <class:User> at /home/worker/test/ruby/rails/active_record/deprecated_changes/main.rb:44)
  # DEPRECATION WARNING: The behavior of `changed` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_changes.keys` instead. (called from block in <class:User> at /home/worker/test/ruby/rails/active_record/deprecated_changes/main.rb:44)
  # DEPRECATION WARNING: The behavior of `attribute_change` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute` instead. (called from block in <class:User> at /home/worker/test/ruby/rails/active_record/deprecated_changes/main.rb:44)
  # DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead. (called from block in <class:User> at /home/worker/test/ruby/rails/active_record/deprecated_changes/main.rb:44)
end

user = User.create(name: "mami")
user.name = "homu"
pp user.changes
# => {"name"=>["mami", "homu"]}

user.save!

pp user.changes
# => {}

まずこの時点でかなり勘違いしていたことがわかりました。

Rails 5.2 でどうなった?

では次に Rails 5.2 でどうなったのか見てみましょう。
先程のコードを Rails 5.2 で動作させると以下のようになります。

pp ActiveRecord::VERSION::STRING
# => "5.2.0"

class User < ActiveRecord::Base
  before_update { pp changes }
  # => {"name"=>["mami", "homu"]}

  # after_xxx で警告が出なくなる
  after_update { pp changes }
  # => {}
end

user = User.create(name: "mami")
user.name = "homu"

pp user.changes
# => {"name"=>["mami", "homu"]}

user.save!

pp user.changes
# => {}

Rails 5.2 では after_updatechanges を使っていても警告は出なくなっていますね。
それよりも注目なのは changes の戻り値が異なっている点です。
各バージョンで after_update 内で changes を呼んだ場合、以下のようになります。

after_update { pp changes }
# 5.0 => no wanirng:          {"name"=>["mami", "homu"]}
# 5.1 => DEPRECATION WARNING: {"name"=>["mami", "homu"]}
# 5.2 => no wanirng:          {}

なんてこったい。

DEPRECATION WARNING な何に対しての警告だったのか

一旦話をまとめるとこういう挙動になっていました。

  • DEPRECATION WARNING after_xxxchanges を使うと出ていた
  • DEPRECATION WARNINGRails 5.1 でのみ出ていた
  • after_xxxchanges を使用した場合、5.1 と 5.2 で戻り値が変わっていた

ここで重要なのが『Rails 5.1 -> 5.2 で破壊的変更が入った』という点です。
つまりこの話で出てきた DEPRECATION WARNING というのはこの『非互換になる挙動』に対する警告だったのです!!!!(多分
あくまでもこの非互換に対する警告であって『 change 自体の呼び出しに対する警告』ではなかったということですね。
なので Rails 5.2 やそれ以降のバージョンではすでに DEPRECATION WARNING は出ずに changes などを安全に使うことができます。
最終的な挙動のまとめは以下のとおりです。

class User < ActiveRecord::Base
  before_update { pp changes }
  # 5.0 => no warning: {"name"=>["mami", "homu"]}
  # 5.1 => no warning: {"name"=>["mami", "homu"]}
  # 5.2 => no warning: {"name"=>["mami", "homu"]}

  after_update { pp changes }
  # 5.0 => no wanirng:          {"name"=>["mami", "homu"]}
  # 5.1 => DEPRECATION WARNING: {"name"=>["mami", "homu"]}
  # 5.2 => no wanirng:          {}
end

user = User.create(name: "mami")
user.name = "homu"
pp user.changes
# => {"name"=>["mami", "homu"]}

# before_update / after_update が呼ばれる
user.save!
pp user.changes
# 5.0 => {}
# 5.1 => {}
# 5.2 => {}

まとめ

と、言うことで長年勘違いしていた事が解決できてすっきりしました。
この手の話は盲目的に信用せずどういう意図なのかをちゃんと調べないとダメですね…。
とはいえ全部に対して1つ1つ調べていくのも時間がかかるので難しいところ。
むしろわたしは『こういうのを書く事』が多いのでなるべく注意して情報を発信していきたいですねえ。

参照