ActiveRecord や ActiveModel::Attributes の attribute メソッドによって属性を定義する際に、第二引数の cast_type を指定せず、かつデフォルト値を指定している場合に、この属性に対して破壊的な変更をするとデフォルト値が書き変わってしまう。
以下はデフォルト値に空の配列を指定した場合の例。
class C
include ActiveModel::Attributes
attribute :hoge, default: []
end
c = C.new
c.hoge << :x
c = C.new
c.hoge #=> [:x] # [] が期待値のはず
この挙動によるバグを引き起こしやすい attribute :hoge, default: []
や attribute :hoge, default: {}
のような記述が、アプリケーションコードに入らないようにするための Cop が Rails/AttributeDefaultBlockValue
である。
上記ドキュメントをみると配列やハッシュ以外にも対策されていることがわかる。
この Cop が作られた経緯については以下のあたりを参照。
ただし、この Cop を手元で試してみたところうまく動かなかった。
調べたところ些細なバグだったので直してPRした。
(迅速にレビュー&マージしていただけた。ありがとうございます)
このPRがリリースに入るまでは、以下をアプリケーション側の設定に記述するのがワークアラウンドとなる。
Rails/AttributeDefaultBlockValue:
Include:
- app/models/**/*.rb
↓以下、このような attribute 周りの挙動について ruby-jp の Slack (#rails チャンネル) で得られた反応
kkitadate 9:46 PM
ActiveRecord や ActiveModel::Attributes の attribute メソッドについてですが、デフォルト値に Array や Hash を指定した時の挙動に驚きました
#
# String を指定した場合は想定通りに動く
#
class C
include ActiveModel::Attributes
attribute :str, default: ‘’
end
c = C.new
c.str = ‘s’
# インスタンスごとにデフォルト値で初期化されるのが期待値
c = C.new
c.str # => “”
#
# Array の場合、インスタンスを生成するたびに初期化されるのではなく、クラス自体に保存される?
#
class C
include ActiveModel::Attributes
attribute :array, default: []
end
c = C.new
c.array << :x
c = C.new
c.array # => [:x] ※ [] が期待値のはず
#
# Hash の場合も Array と同様
#
class C
include ActiveModel::Attributes
attribute :hash, default: {}
end
c = C.new
c.hash[:x] = :y
c = C.new
c.hash # => {:x=>:y} ※ {} が期待値のはず
chiastolite 10:33 PM
クラス読み込んだときに評価されちゃうから、←で作られたオブジェクトが毎回参照されちゃうやつかな (edited)
10:33
(表現合ってるか?)
10:34
やったことないから正しく動くかわからないけど default: -> { [] }
とかしたらどうだろう?
chiastolite 10:40 PM
↑これでよさそう
koheisg 10:43 PM
えーまじか〜辛い。。。
kkitadate 10:46 PM
default: -> { [] }
ですね。あとこの用途であれば attr_accessor でよさそうという感じなんですが、オブジェクトを reload したあとの結果が異なるという罠がありました
chiastolite 10:52 PM
Railsでもよく if: -> { … }
みたいな書き方が必要なことあるけど、大体こういうのが理由ですよね
mksava 11:02 PM
ちょっとそれちゃうんですが、そもそも attribute ってhashやarrayってサポートしていましたっけ?
昔ここを見てサポートしてなくてカスタムタイプ作った記憶がありまして…
https://github.com/rails/rails/tree/main/activemodel/lib/active_model/type (edited)
chiastolite 11:05 PM
typeを指定してなかったら任意の値が入れられるのかなと
(もちろんカスタムタイプ作れるなら作ったほうがよさそう)
zunda 3:13 AM
先日Pythonのメソッドの定義時のデフォルト値の評価で同じような話がありましたねー
kkitadate 8:43 AM
オブジェクトを reload したあとの結果が異なる
これは ActiveRecord オブジェクトの場合でした。
(ActiveModel::Attributes を include しただけのクラスであれば関係なさそう)
class Post < ActiveRecord::Base
attribute :tags, default: -> { [] }
end
post = Post.new
post.tags << :ruby
post.save!
p post.tags #=> [:ruby]
p post.reload.tags #=> []
class Article < ActiveRecord::Base
attr_accessor :tags
after_initialize { self.tags ||= [] }
end
article = Article.new
article.tags << :ruby
article.save!
p article.tags #=> [:ruby]
p article.reload.tags #=> [:ruby]