RailsのActiveRecord保存時にtimestamp(created_at,updated_at)が自動更新されないようにする
結論は下記なんだけど。
# Rails5以上 book.save(touch: false) # Rails4以下 ActiveRecord::Base.record_timestamps = false
どうも古くからRailsをやっているからかsaveのtouchオプションが全く出てこず、record_timestampsの方でゴニョゴニョしたので下記は備忘として
きっとまだRails4みたいな古美術品に出会ったらやくに立つはず。。。
Railsでsaveした時にtimestamp(created_atやupdated_at)を更新したくない場合にどうするかをググると下記のように書くのが出てくる。
ActiveRecord::Base.record_timestamps = false book.save # 戻すの忘れずに! ActiveRecord::Base.record_timestamps = true
昨今のpumaやsidekiqのようなマルチスレッド環境だと、こういうクラス変数っぽいものに代入しているのを見たときはかなり慎重に検証するようにしていて、 今回これだとマズそうだったのでどうにかできないか試行錯誤していた。
まず今回のケースだと
ActiveRecord::Base.record_timestamps = false multi_table_copy ActiveRecord::Base.record_timestamps = true
こんな感じにするとmulti_table_copyに時間がかかると他のスレッドまで ActiveRecord::Base.record_timestamps = false
が効いた状態が長く続き、通常の処理が意図せず失敗してしまう。
(created_atなどが自動設定されないので通常のsaveで Field 'created_at' doesn't have a default value
みたいにNot Null制約に引っかかるはず)
なので、その影響を少なくするためにsaveするところでrecord_timestampsのtrue/falseをバチバチ切り替えるようにしようと考えた。
def multi_table_copy ・・・ target_records.each do |r| ActiveRecord::Base.record_timestamps = false r.save(validate: false) ActiveRecord::Base.record_timestamps = true end ・・・ end
しかし、これでも影響がゼロになった訳ではない。
(並列でジョブが動いていてrecord_timestamps = falseにするのと他のジョブのsaveが被らないとは言えないはず)
なので、他への影響を与えないようにできないものかと思って調べているとrecord_timestampsはcattr_accessorではなくclass_attributeで定義されているのでなんとかなりそうだと分かった。
※自分の今回初めてclass_attributeというものの存在を知った。
railsguides.jp
クラス変数だと継承先で上書きすると親クラスにも影響するが、class_attributeだと子クラスでの上書きが他へ影響しなくなる。
[1] pry(main)> class Base [1] pry(main)* class_attribute :record_timestamps, default: true [1] pry(main)* end => [:record_timestamps] [2] pry(main)> [3] pry(main)> [4] pry(main)> class A < Base [4] pry(main)* end => nil [5] pry(main)> class B < Base [5] pry(main)* end => nil [6] pry(main)> [7] pry(main)> [8] pry(main)> Base.record_timestamps => true [9] pry(main)> A.record_timestamps => true [10] pry(main)> B.record_timestamps => true [11] pry(main)> [12] pry(main)> A.record_timestamps = false => false [13] pry(main)> Base.record_timestamps => true [14] pry(main)> A.record_timestamps => false [15] pry(main)> B.record_timestamps => true
cattr_accessorだと他も上書きされてしまう。
[1] pry(main)> class Base [1] pry(main)* cattr_accessor :record_timestamps, default: true [1] pry(main)* end => [:record_timestamps] [2] pry(main)> class A < Base [2] pry(main)* end => nil [3] pry(main)> class B < Base [3] pry(main)* end => nil [4] pry(main)> [5] pry(main)> [6] pry(main)> Base.record_timestamps => true [7] pry(main)> A.record_timestamps => true [8] pry(main)> B.record_timestamps => true [9] pry(main)> [10] pry(main)> A.record_timestamps = false => false [11] pry(main)> [12] pry(main)> Base.record_timestamps => false [13] pry(main)> A.record_timestamps => false [14] pry(main)> B.record_timestamps => false
便利!
しかもclass_attributeはnewしたオブジェクトで上書きしても他のオブジェクトに影響しない。
[1] pry(main)> class Base [1] pry(main)* class_attribute :record_timestamps, default: true [1] pry(main)* end => [:record_timestamps] [2] pry(main)> [3] pry(main)> class A < Base [3] pry(main)* end => nil [4] pry(main)> class B < Base [4] pry(main)* end => nil [5] pry(main)> a1 = A.new => #<A:0x00007fc684888568> [6] pry(main)> a2 = A.new => #<A:0x00007fc68408aed8> [7] pry(main)> [8] pry(main)> [9] pry(main)> a1.record_timestamps = false => false [10] pry(main)> [11] pry(main)> [12] pry(main)> Base.record_timestamps => true [13] pry(main)> A.record_timestamps => true [14] pry(main)> B.record_timestamps => true [15] pry(main)> a1.record_timestamps => false [16] pry(main)> a2.record_timestamps => true
なので、Book.record_timestamps = falseとするよりは、book.record_timestamps = falseとした方が他への影響をなくすことができる。
しかもこの方法だとrecord_timestamps = trueに戻すことも考えなくていいので楽チン。
なので、最終的には下記のようにできる。
def multi_table_copy ・・・ target_records.each do |r| r.record_timestamps = false r.save(validate: false) end ・・・ end
これで完璧や!!っと、ここまでやったところでRails5からはsave(touch: false)があることを思い出し無駄な時間を使っただけになってしまったのでした。。。