Table of contents
Reading Settings
16px

I just made a serious mistake with Rails destroy_all
Software Engineer
Software Engineer
Ruby on Rails
Ruby on Rails
Rails Active Record is convenient and human-readable for interacting with SQL models. But not understanding the generated SQL behind these elegant methods can cause serious data loss that might go unnoticed until it's too late.
1. Setting the Scene
This is a simple student enrollment system with a classic many-to-many relationship. It includes students, courses, and registrations connecting them. Here's the basic setup:
Models:
Models:
// language: ruby class Student < ApplicationRecord has_many :registrations, dependent: :destroy has_many :courses, through: :registrations end class Registration < ApplicationRecord belongs_to :student belongs_to :course end class Course < ApplicationRecord has_many :registrations, dependent: :destroy has_many :students, through: :registrations end
Seed data:
// language: ruby alice = Student.create(name: "Alice") bob = Student.create(name: "Bob") math = Course.create(name: "Math 101") science = Course.create(name: "Science 101") alice.courses = [math, science] bob.courses = [math, science]
Pretty standard Rails stuff. Alice and Bob are both enrolled in Math 101 and Science 101. Everything looks good, right?
2. The Innocent Query That Went Wrong
I can get all the courses registered by Alice by querying:
// language: ruby alice.courses
Here's where things get interesting. I needed to remove all of Alice's course enrollments, so I can write:
// language: ruby alice.courses.destroy_all
The generated SQL looked like this:
// language: sql TRANSACTION (xxx) BEGIN /*application='xxx'*/ Registration Load (xxx) SELECT "registrations".* FROM "registrations" WHERE "registrations"."student_id" = 1 AND "registrations"."course_id" IN (1, 2) /*application='xxx'*/ Registration Destroy (xxx) DELETE FROM "registrations" WHERE "registrations"."id" = 1 /*application='xxx'*/ Registration Destroy (xxx) DELETE FROM "registrations" WHERE "registrations"."id" = 2 /*application='xxx'*/ TRANSACTION (xxx) COMMIT /*application='xxxxx'*/ => [#<Course:0x000000010f5c5ed0 id: 1, name: "Math 101", created_at: "xxx", updated_at: "xxx">, #<Course:0x000000011903c450 id: 2, name: "Science 101", created_at: "xxx", updated_at: "xxx">]
This worked exactly as expected. Looking at the SQL logs, I could see it was doing the right thing – finding the registration records for Alice and deleting them. The courses themselves remained untouched. Perfect.
But then I had a slightly different requirement. I needed to unenroll Alice from just one specific course. So I modified the query to add a filter:
// language: ruby alice.courses.where(id: 1).destroy_all
Seems logical, right? Just add a
3. The Shocking Result
Here's what actually happened when I ran that second query:
// language: sql Course Load (xxx) SELECT "courses".* FROM "courses" INNER JOIN "registrations" ON "courses"."id" = "registrations"."course_id" WHERE "registrations"."student_id" = 1 AND "courses"."id" = 1 /*application='xxxxx'*/ TRANSACTION (xxx) BEGIN /*application='xxx'*/ Registration Load (xxx) SELECT "registrations".* FROM "registrations" WHERE "registrations"."course_id" = 1 /*application='xxx'*/ Registration Destroy (xxx) DELETE FROM "registrations" WHERE "registrations"."id" = 3 /*application='xxx'*/ Registration Destroy (xxx) DELETE FROM "registrations" WHERE "registrations"."id" = 5 /*application='xxx'*/ Course Destroy (xxx) DELETE FROM "courses" WHERE "courses"."id" = 1 /*application='xxxxx'*/ TRANSACTION (xxx) COMMIT /*application='xxxxx'*/ => [#<Course:0x000000010d1ff190 id: 1, name: "Math 101", created_at: "xxx", updated_at: "xxx">]
Wait, what just happened? Not only did it remove Alice's registration for Math 101, but it also deleted Bob's registration AND completely destroyed the Math 101 course itself!
4. Why This Happens
The key difference lies in how Rails interprets these two seemingly similar queries:
Query 1:student_a.courses.destroy_all
Query 1:
- Rails treats this as "destroy all associations between this student and their courses"
- It deletes only the registration records
- Courses remain intact
Query 2:
- Rails treats this as "find courses matching this criteria, then destroy them"
- It's essentially equivalent to
Course.joins(:registrations).where(registrations: {student_id: 1}, id: 1).destroy_all - This destroys the actual course records
- The
dependent: :destroy on the Course model then cascades to delete all related registrations
The moment you add a
The safe ways to remove registrations
// language: ruby # Safe: Remove specific registrations alice.registrations.where(course_id: 1).destroy_all # If you want to remove multiple specific courses course_ids = [1, 2] alice.registrations.where(course_id: course_ids).destroy_all # Or even more explicit Registration.where(student: alice, course_id: 1).destroy_all
These approaches are explicit about what you're deleting and won't accidentally cascade to other records.
5. The Broader Lesson
Rails tries to be helpful by making associations feel natural to work with, but sometimes that helpfulness can hide important distinctions. The scariest part is that both queries return similar-looking results in the console, so you might not even notice the problem until users start reporting missing data. In production, this could be catastrophic.
Always test these Active Record queries locally first and carefully examine the generated SQL before running them in production. Pay special attention to anyDELETE statements and trace through what the dependent: :destroy callbacks will cascade to. And remember – maintain frequent backups so you can mitigate data loss when these subtle bugs slip through. Your future self (and your users) will thank you for the extra caution.
Always test these Active Record queries locally first and carefully examine the generated SQL before running them in production. Pay special attention to any

2025-06-23 18:47:22 +0700
Related blogs

How I built PRIVATE IMAGE TOOLS in just 2 days with Cursor
I've been using AI tools in my development process for a while now, and they've significantly boosted my performance by reducing the knowledge gap. In...
Software Engineer
Software Engineer

2025-04-11 10:28:14 +0700


How to add a custom Inline Code to Trix editor
Lately, I’ve been improving the writing experience in my Rails app, and something kept bugging me: I wanted a way to add inline code formatting in the...
Ruby on Rails
Ruby on Rails

2025-05-17 15:50:55 +0700