Rails 6でbulk insertの機能が実現されました🎉
※概要は以前紹介してるので興味のある方は参照してください。
非常に便利なのですが、記事でも記載した以下の通り、timestampが補完されずにcreate_at、updated_atにNOT NULL制約を付与していると、エラーになってしまうのが、activerecord-import
からの乗り換え目的等で検討すると、やはりtimestampをデフォルトで補完したいケースもあるのかなと、、、
created_atとupdated_atを補完してくれないので自分で設定しないといけない点に注意です。実装をシンプルにしてパフォーマンスを担保するのとupdate_allの仕様に合わせているのが理由のようです🤔 https://github.com/rails/rails/issues/35493#issuecomment-470100313
なので、ちょっと対応方法を考えたのでMEMOしておきます📝
DBのDEFAULT制約を利用する
tableを作るときに以下のような形でDAFAULT制約を用いてCURRENT_TIMESTAMP
を実行するようにすることでtimestampのカラムに現在時刻を自動的に設定します。
class CreateFoo < ActiveRecord::Migration[6.0] def change create_table :foo do |t| t.timestamps default: -> { 'CURRENT_TIMESTAMP' } end end end
これは下記のissueでコメントされている方法です。
SQLを直接実行するのでDBMSによって多少方言が違う可能性があるのがネックですが、CURRENT_TIMESTAMP
はMySQLにもPostgreSQLにもSQLite実装されている関数のようなので基本的には大丈夫そうです※挙動は微妙に違うかもですが。。。
https://www.postgresql.jp/document/12/html/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT
https://dev.mysql.com/doc/refman/5.6/ja/timestamp-initialization.html
ActiveRecordにパッチを当てる
あとはActiveRecordにパッチを当ててTime.currentで取得した値を差し込むのもありかなとも思いました。
class ApplicationRecord < ActiveRecord::Base # NOTE: insert_all実行時にデフォルトでtimestampを付与するパッチ def insert_all(attributes, returning: nil, unique_by: nil, now: Time.current) attributes.map! { |attr| attr.merge(created_at: now, updated_at: now) } super end def self.insert_all!(attributes, returning: nil, now: Time.current)) attributes.map! { |attr| attr.merge(created_at: now, updated_at: now) } super end end
上記のコードではinsert_all
では、引数で渡されたattributesをもとにbulk insertを実行するので、created_at
、updated_at
にTime.current
で取得した現在時刻を設定したHashをattributesにmergeしています。
以下のような形でtimestampeを指定しなくても実行出来るようになります。
Foo.insert_all([{ title: 'foo1', body: 'bar1' }, { title: 'foo2', body: 'bar3' }])
具体的には、以下のようなコードを実行しているのとほぼ同じ挙動になります。
now = Time.current Foo.insert_all([ { title: 'foo1', body: 'bar1', created_at: now, updated_at: now }, { title: 'foo2', body: 'bar3', created_at: now, updated_at: now }, ])
おわりに
やりすぎは注意ですが、Rubyはライブラリのコードでも柔軟に拡張出来るのが良いですね✨