In Active Record, how do destroy and delete differ, and when would you use destroy_all versus delete_all to purge listings?
answer
- callbacks and dependent: run or not
- destroy returns frozen self or false
- destroy! raises RecordNotDestroyed
- destroy_all: one DELETE per row
- delete_all: one statement, a count
basics
~20 sdestroy loads the record's callbacks and dependent: associations before deleting and can be stopped; delete sends one DELETE with no callbacks. destroy_all destroys each loaded record; delete_all issues a single DELETE and returns the row count.
solid answer
~40 s`listing.destroy` runs `before_destroy`/`after_destroy` and the commit callbacks, honours `dependent:` options on its associations, and returns the frozen record, or `false` if a `before_destroy` throws `:abort`; `destroy!` raises `ActiveRecord::RecordNotDestroyed` instead. `listing.delete` sends `DELETE ... WHERE id = ?` and nothing else: no callbacks, no `dependent:` handling, and it even removes readonly records. On relations, `Listing.where(...).destroy_all` loads every matching row and calls `destroy` on each, so it is at least one DELETE per row and returns the destroyed records; `delete_all` builds one DELETE and returns the number of rows. Use `destroy`/`destroy_all` when the model has cleanup rules (attached photos, child rows, cache purges); use `delete_all` for large purges of rows with no such rules, or when the database's foreign keys handle the cascade.
code
ruby · 10 linesclass Listing < ApplicationRecord
has_many_attached :photos # purge enqueued when the listing is destroyed
has_many :saved_listings, dependent: :delete_all
before_destroy :ensure_not_under_dispute
end
listing.destroy # callbacks + cascades; false if the before_destroy aborts
listing.delete # one DELETE; photos and saved_listings rows left to the database
Listing.where(deleted_by_seller_at: ...1.year.ago).delete_all # => 18_402go deeper
Recall that destroy runs callbacks and dependent: options while delete sends one DELETE and skips them.
Explain the return values, destroy! raising RecordNotDestroyed, and why destroy_all loads every row while delete_all returns a count.
Choose the purge strategy from the model's cleanup rules and the database's foreign keys, and move large destroys out of the request.
Decide where cascade rules live, in model callbacks or in database constraints, so bulk deletes cannot bypass them silently.
## Two ways to remove a row The classified-ads site purges listings that sellers deleted long ago, and lets sellers remove a single listing. Active Record gives two removal paths with very different costs: | | `destroy` / `destroy_all` | `delete` / `delete_all` | |---|---|---| | Loads records | yes (`destroy_all` loads them all) | no (single record already in memory, or none) | | `before_destroy` / `after_destroy` | run | skipped | | `after_commit` on destroy | run | skipped | | `dependent:` on associations | honoured | ignored | | Can be stopped | yes, by `throw :abort` | no | | SQL | one DELETE per record, plus cascades | one DELETE | | Returns | frozen record / `false`; array for `destroy_all` | frozen record; row count for `delete_all` | ## destroy and destroy! `listing.destroy`: 1. raises `ActiveRecord::ReadOnlyRecord` if the record is readonly; 2. handles associations declared with `dependent:` (destroying, deleting or nullifying children); 3. runs `before_destroy`, sends the DELETE, runs `after_destroy`, and later the commit callbacks; 4. marks the object `destroyed?` and **freezes** it, so further assignment raises a `FrozenError`. If a `before_destroy` callback throws `:abort`, `destroy` returns `false` and nothing is deleted; `destroy!` raises `ActiveRecord::RecordNotDestroyed` in the same case. A seller-facing "remove listing" action should use `destroy` (or `destroy!`) so that photo attachments, saved-search notifications and cache entries are cleaned up by the model's own rules. ## delete `listing.delete` sends `DELETE FROM listings WHERE id = ?` and freezes the object. No callbacks, no `dependent:` handling, and, unlike `destroy`, it does not refuse readonly records. Child rows are left behind unless the database itself cascades through a foreign key with `ON DELETE CASCADE`, or a foreign-key constraint rejects the delete. ## destroy_all and delete_all `Listing.where(deleted_by_seller_at: ...1.year.ago).destroy_all`: - loads **every** matching record into memory; - calls `destroy` on each, so each runs its callbacks and cascades; - returns the array of destroyed (frozen) records. For a hundred rows that is fine; for two million it is slow and memory-hungry. `delete_all` on the same relation: - builds **one** `DELETE ... WHERE deleted_by_seller_at < ?`; - returns the **number of rows** deleted; - raises `ActiveRecord::ActiveRecordError` when the relation uses `distinct` or a CTE (`with`), which a DELETE cannot express. `destroy_by(conditions)` and `delete_by(conditions)` are shorthands for `where(conditions).destroy_all` and `where(conditions).delete_all`. ## Choosing for a purge 1. **Does the model have destroy callbacks or `dependent:` associations that matter?** If yes, `delete_all` would orphan or skip them; use `destroy` semantics, processed in batches by a background job (batching belongs to the batches topic). 2. **Are the rules enforced by the database?** If child tables reference listings with `ON DELETE CASCADE`, `delete_all` is safe and fast. 3. **Is it a soft delete?** Archiving often means `update_all(archived_at: Time.current)` rather than removing rows at all. ## After the delete A destroyed or deleted object is frozen: `listing.title = "x"` raises `FrozenError`, and `listing.destroyed?` returns `true`. Other copies of the same row already in memory do not know it is gone; reloading one raises `ActiveRecord::RecordNotFound`. ## Soft delete versus hard delete Many sites never remove listings outright. A **soft delete** marks the row instead: - `listing.update(archived_at: Time.current)` keeps the row for reporting, disputes and undo; - default queries exclude archived rows through a scope; - a later purge job hard-deletes rows archived long ago. The destroy-versus-delete choice then moves to that purge job, where the same questions apply: which callbacks and `dependent:` rules must run, and can the database's foreign keys carry the cascade instead. Interviewers like candidates who ask "do we need the row afterwards?" before choosing any of these calls.
- A before_destroy callback throws :abort. What do destroy and destroy! return or raise?`destroy` returns `false` and deletes nothing; `destroy!` raises `ActiveRecord::RecordNotDestroyed`. The record stays persisted and is not frozen.
- Why can delete_all on listings fail even though destroy_all on the same relation works?If child tables have foreign keys to listings without `ON DELETE CASCADE`, the single DELETE violates them and the database raises. `destroy_all` works because each listing's `dependent:` options remove or nullify the children first.
destroy is checking a tenant out properly: the inventory, the key return and the forwarding address all happen before the room is cleared; delete is clearing the room while nobody watches, which is fast but leaves any follow-up undone.
saying these in an interview costs you the question
- Saying delete runs before_destroy and after_destroy but skips validations.
- Expecting delete_all to honour dependent: :destroy on associations.
- Running destroy_all over millions of rows in one web request.
- Claiming destroy_all issues a single DELETE statement.
- Expecting destroy to raise when a before_destroy callback aborts.