Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby on Rails: Rails 6で追加された`insert_all`の実行時にデフォルトでTimestampを付与するMEMO

Rails 6でbulk insertの機能が実現されました🎉
※概要は以前紹介してるので興味のある方は参照してください。

madogiwa0124.hatenablog.com

非常に便利なのですが、記事でも記載した以下の通り、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

github.com

なので、ちょっと対応方法を考えたので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でコメントされている方法です。

github.com

SQLを直接実行するのでDBMSによって多少方言が違う可能性があるのがネックですが、CURRENT_TIMESTAMPMySQLにもPostgreSQLにもSQLite実装されている関数のようなので基本的には大丈夫そうです※挙動は微妙に違うかもですが。。。

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_atupdated_atTime.currentで取得した現在時刻を設定したHashをattributesにmergeしています。

https://github.com/rails/rails/blob/f855139f3d2bb9b032613279d0adfbd6a77a2d07/activerecord/lib/active_record/persistence.rb#L131-L133

以下のような形で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はライブラリのコードでも柔軟に拡張出来るのが良いですね✨