In Rails 8.1, what do update, update_column(s), update_all and touch each skip among validations, callbacks and updated_at?
answer
- update runs the full save path
- update_columns: one UPDATE, no callbacks
- update_all: no objects loaded
- touch: after_touch and commit only
- 8.1: touch: option on update_column
basics
~10 supdate runs validations, callbacks and bumps updated_at. update_column(s) and update_all skip validations, callbacks and updated_at (update_column accepts touch: true since 8.1); touch skips validations and runs only after_touch and commit callbacks.
solid answer
~40 s`listing.update(attrs)` is assign plus `save`: validations, save and update callbacks, `updated_at`, all in a transaction. `listing.update_columns(archived_at: Time.current)` sends one UPDATE for that row: no validations, no callbacks, no `updated_at` unless you pass `touch: true` (added to `update_column`/`update_columns` in Rails 8.1); it raises on a new record. `Listing.where(...).update_all(archived_at: Time.current)` sends one UPDATE for every matching row without loading objects: no validations, no callbacks, no timestamps, and it returns the number of rows. `listing.touch` writes `updated_at` (plus any named columns), skips validations and runs only `after_touch`, `after_commit` and `after_rollback`. Records already in memory do not see an `update_all`; call `reload`. Pick the narrowest call whose skipped steps you can defend.
code
ruby · 10 lineslisting.update(price_cents: 29_000) # validations, callbacks, updated_at
listing.update_columns(status: "featured") # one UPDATE, nothing else
listing.update_column(:status, "featured", touch: true) # Rails 8.1: also updated_at
n = Listing.where(expires_on: ...Date.current)
.update_all(archived_at: Time.current, status: "archived")
# n => number of rows archived; loaded listings are stale until reload
listing.touch # updated_at only, after_touch runsgo deeper
Recall that update runs validations and callbacks while update_column and update_all go straight to SQL.
Explain the full skip table, including touch's narrow callback set, update_all's row count and the reload needed afterwards.
Review every skipping call for the callbacks it silences, such as cache purges or broadcasts, and use touch: true on Rails 8.1 where timestamps matter.
Decide where bulk SQL is allowed versus where domain rules must run, and make that boundary visible in code review.
## The update family at a glance In the classified-ads app, nightly maintenance **archives** listings past their expiry, and sellers edit listings through a form. Both are updates, but they need very different amounts of machinery. Active Record offers a ladder: | Call | Rows | Loads objects | Validations | Callbacks | `updated_at` | |---|---|---|---|---|---| | `record.update(attrs)` | one | yes | yes | save/update callbacks | bumped if changed | | `record.update_attribute(name, v)` | one | yes | **skipped** | yes | bumped | | `record.update_columns(attrs)` | one | already loaded | skipped | skipped | only with `touch: true` (8.1) | | `record.update_column(name, v)` | one | already loaded | skipped | skipped | only with `touch: true` (8.1) | | `record.touch(*names)` | one | already loaded | skipped | `after_touch`, `after_commit`, `after_rollback` | always | | `relation.update_all(attrs)` | many | **no** | skipped | skipped | not touched | | `relation.touch_all(*names)` | many | no | skipped | skipped | set | ## update: the full save path `update` assigns the attributes and calls `save` inside a transaction. Everything a model promises runs: normalization, validations, `before_save`/`after_save`, `before_update`/`after_update`, commit callbacks, dirty tracking and the timestamp. This is the default for any change a user makes. ## update_column and update_columns: one row, straight to SQL `listing.update_columns(archived_at: Time.current, status: "archived")`: - issues a single `UPDATE listings SET ... WHERE id = ?`; - skips validations and **all** callbacks; - still casts values through each attribute's type, and writes them into the in-memory object, leaving no pending change for those columns; - does not set `updated_at` unless you pass `touch: true`, an option added in **Rails 8.1**; on 8.0 and earlier you had to add `updated_at: Time.current` yourself; - raises `ActiveRecord::ActiveRecordError` on a new or destroyed record, and on attributes marked readonly. `update_column(name, value)` is `update_columns(name => value)`. ## update_all: many rows, no objects `Listing.where(expires_on: ...Date.current).update_all(archived_at: Time.current)`: 1. builds one `UPDATE ... WHERE expires_on < ?` from the relation; 2. never instantiates a `Listing`, so no validations, callbacks or dirty tracking run; 3. does **not** set `updated_at` (pass it explicitly, or use `touch_all` for timestamps alone); 4. returns the **number of rows affected**; 5. accepts a SQL string for expressions, such as `update_all("views_count = views_count + 1")`. Objects already loaded in memory are **not** updated. A `listing` loaded earlier still shows the old `archived_at` until you call `listing.reload`, which re-reads the row, clears pending changes and resets cached associations. ## touch: the timestamp alone `listing.touch` sets `updated_at` (and any columns you name, as in `listing.touch(:bumped_at)`) to the current time with one UPDATE. Validations do not run and the only callbacks are `after_touch`, `after_commit` and `after_rollback`. It is how a record signals "something about me changed" to caches keyed on `updated_at` without a real attribute change. Calling it on a new record raises. ## Choosing well - **User edits**: `update`. Skipping validations on user input is a bug factory. - **One technical column on one loaded record** (a denormalized flag, a cached value): `update_column`, accepting that callbacks such as cache expiry or broadcasts will not fire. - **Bulk maintenance** (archive expired listings): `update_all`, after checking that no callback or validation carries a rule the job must honour. - **Bump a timestamp only**: `touch`. The review question for any of the skipping calls is the same: which callback or validation did this bypass, and is that intended? A `before_save` that recomputes a search column, or an `after_commit` that purges a cache, silently stops running. ## A worked review A pull request adds a nightly job that archives expired listings with `update_all`. The review walks the skip table: 1. **Validations**: does any validation on `archived_at` or `status` matter for an expiry? Usually not, since the job sets known-good values. 2. **Callbacks**: does `Listing` have an `after_update_commit` that removes the listing from search or purges a cached page? If so, `update_all` silently skips it, and archived listings stay searchable until something else touches them. 3. **Timestamps**: should `updated_at` move? Caches keyed on it will keep serving the old page unless the job sets `updated_at: Time.current` too. 4. **Objects in memory**: the job holds no loaded listings, so no `reload` is needed; a test that loaded one first must reload it before asserting. If step 2 finds a callback that must run, the job switches to loading the listings in batches and calling `update` on each, trading speed for the domain rules.
- After Listing.where(id: listing.id).update_all(status: "archived"), why does listing.status still say "active"?`update_all` writes straight to the table and never touches objects in memory. The `listing` variable keeps the values it loaded. Calling `listing.reload` re-reads the row, giving `"archived"` and clearing any pending changes.
- What is the difference between update_attribute and update_column?`update_attribute` sets one attribute and calls `save(validate: false)`: validations are skipped, but callbacks run, other dirty attributes are saved too, and `updated_at` moves. `update_column` sends one UPDATE for that column only, skipping validations, callbacks and (without `touch: true`) the timestamp.
saying these in an interview costs you the question
- Saying update_all runs each record's callbacks once per row.
- Expecting update_all to bump updated_at automatically.
- Believing update_attribute skips callbacks the way update_column does.
- Using update_columns on a record that has not been saved yet.
- Expecting already loaded objects to reflect an update_all without reload.
- Claiming touch runs validations before writing updated_at.