パパエンジニアのアウトプット帳

30歳に突入した1児のパパエンジニアのブログ

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)があることを思い出し無駄な時間を使っただけになってしまったのでした。。。