Bulk and direct persistence APIs in Active Record such as update_all, delete, delete_all, update_columns, insert_all, and upsert_all bypass parts of the record lifecycle: validations, callbacks, timestamps, and in-memory synchronization. The piece walks through concrete examples showing missing audit rows, stale updated_at values, stale in-memory objects, optimistic-locking StaleObjectError surprises, and skipped normalization on bulk inserts. It closes with a repair pattern that wraps update_all and insert_all! in a transaction with explicit timestamps and audit writes, arguing developers should choose a persistence method based on which lifecycle behaviors the operation actually requires, noting that Rails 8.1 added touch: true support to update_column/update_columns (absent in 8.0 and 7.x).

12m read timeFrom railsrevelry.substack.com
Post cover image
Table of contents
Two paths through Active Recorddestroy and delete remove a row through different contractsupdate_columns takes the direct path through one objectupdate_all can create a delayed stale-object failureBulk inserts use the model without running the modelMake the omitted work visible

Questions this post answers

Does Rails update_columns support a touch option to update timestamps automatically?

Yes, Rails 8.1 added a touch: true option to both update_column and update_columns, which adds the model's timestamp columns to the direct update call. Without this option, updated_at does not update automatically when using these methods. The option is unavailable in Rails 8.0 and the 7.x series. Rails upgraders track version-specific API additions like this on daily.dev before touching production update code.

Why don't my Active Record callbacks run when I use update_all in Rails?

update_all compiles a single SQL UPDATE statement directly against the database without instantiating any model objects, so there are no records for callbacks, validations, or automatic timestamp updates to run against. It only returns the affected-row count and resets the relation; any Active Record object still in memory continues showing the old attribute values until reloaded. daily.dev helps Rails developers debugging lifecycle gaps in bulk persistence methods stay sharp on Active Record internals.

How is Active Record's delete method different from destroy for removing a record?

delete issues a direct SQL DELETE for the receiver's row and then marks it destroyed and frozen, without running destroy callbacks or honoring dependent association options like dependent: :destroy. destroy instead runs the full destroy callback chain and associated cleanup behavior before removing the row, so a before_destroy callback can even abort the deletion. Rails developers choosing between destroy and delete for cleanup logic can dig deeper on daily.dev.

345 Impressions