Jonathan Bennett

The problem with reusing queries after destroy_all

Hopefully these emails help you generally, but today this one is a reminder for me.

Jonathan, destroy_all affects the results of the original query. You can’t just blindly reuse it:

class PostsController < ApplicationController
  def delete_a_lot
    @posts = Post.where(id: params[:post_ids])
    @posts.destroy_all # nope 

    render turbo_stream: @posts.map { turbo_stream.remove post }
  end
end

The @posts array is always blank when it hits the render call—even though the posts are deleted. The correction is to update the “nope” line above to:

@posts = @posts.destroy_all

But why!?!

Here’s the gotcha: it’s not just about what destroy_all returns. It’s about how ActiveRecord queries work.

When you assign @posts = Post.where(...), you’re creating an ActiveRecord::Relation, a query object. Calling destroy_all on that relation pulls the records, destroys them, and returns the deleted records. But—and here’s the kicker—the original @posts relation still exists. So when you use it again in the render call, Rails re-executes the query… and gets nothing, because the posts are already gone.

Effectively, your code becomes:

@query1 = Post.where(id: params[:post_ids])
@query1.destroy_all
@query2 = Post.where(id: params[:post_ids]) # always []
render turbo_stream: @query2.map { |post| turbo_stream.remove post }

That’s why you must reassign @posts to the result of destroy_all. You’re not just saving what was returned—you’re sidestepping the original query from being lazily re-evaluated after the data is gone.

@posts = @posts.destroy_all

Alternatively, just assign it directly:

@posts = Post.where(id: params[:post_id).destroy_all

Now @posts contains the deleted records in memory, and you can use them to render the appropriate Turbo Stream responses without surprises.