Instantiating objects from Model.insert_all in ActiveRecord
Published: November 22, 2024
Recently at work I refactored a piece of code to bulk insert into Postgres using Model.insert_all
, but wanted to support returning the objects of the inserted records to maintain the existing interface.
I figured I could leverage the SQL returning
clause to return all the columns of the inserted records, but wasn’t sure how to build the objects in the same way as if they were returned from a Model.where(...)
query.
Claude Sonnet then recommended me this approach:
results = Model
.insert_all(rows_to_insert, record_timestamps: true, returning: Arel.sql("*"))
.map { |result| Model.instantiate(result) }
This was the first time I heard about Model.instantiate
1, which differs from Model.new
in the following ways:
- It skips callbacks
- It does not apply defaults
- It marks the object as persisted
I noticed one caveat when playing around with it: if you only have some of the columns, it’ll build an incomplete object.
# You'll get objects like these, and only the `id` column will be available!
> results = Model
.insert_all(rows_to_insert, record_timestamps: true, returning: [:id])
.map { |result| Model.instantiate(result) }
=> [#<Model:0x0000000172b30650 id: "01935570-d609-76aa-9768-0d4e5abb199a">]
> results[0].created_at
=> missing attribute 'created_at' for Model (ActiveModel::MissingAttributeError)