Rails - Delete using destroy, delete, and paranoia.

In this article, we are going to see the different methods to delete items in Rails. It's quite easy to confuse between destroy, delete, and paranoia, but they are different and after a while, you get used to it. We'll also walk you through the differences between delete and destroy in Rails.

Table of Contents


Deleting items in Rails - An Introduction

To illustrate the differences between various methods in Rails to delete items, we use the following Rails Application.

$ rails new bookshelf
  [...]
$ cd bookshelf
$ rails generate model book title
  [...]
$ rails generate model author book_id:integer first_name last_name
  [...]
$ rake db:migrate
  [...]
$

app/models/book.rb
class Book < ActiveRecord::Base
  attr_accessible :title

  has_many :authors, :dependent => :destroy
end

app/models/author.rb
class Author < ActiveRecord::Base
  attr_accessible :book_id, :first_name, :last_name

  belongs_to :book
end

Note that in our rails application, the author model belongs to the book model.

Rails delete operation using destroy method

By using destroy, you can delete the record from rails as well as its other existing dependencies. So in the context of our rails application, if we delete a book record using the destroy function, the authors associated with the book will also be deleted.

Let's look at an example here, where we create a record in rails and then delete using destroy:

$ rails console

Loading development environment (Rails 3.2.9)

>> book = Book.create(title: 'Homo faber')

   (0.1ms)  begin transaction
  SQL (25.8ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 18 Nov 2012 15:11:41 UTC +00:00], ["title", "Homo faber"], ["updated_at", Sun, 18 Nov 2012 15:11:41 UTC +00:00]]
   (2.1ms)  commit transaction

=> #<Book id: 1, title: "Homo faber", created_at: "2012-11-18 15:11:41", updated_at: "2012-11-18 15:11:41">

>> Book.count
   (0.3ms)  SELECT COUNT(*) FROM "books" 
=> 1
>> book.destroy

   (0.1ms)  begin transaction
  Author Load (0.1ms)  SELECT "authors".* FROM "authors" WHERE "authors"."book_id" = 1
  SQL (0.3ms)  DELETE FROM "books" WHERE "books"."id" = ?  [["id", 1]]
   (3.0ms)  commit transaction

=> #<Book id: 1, title: "Homo faber", created_at: "2012-11-18 15:11:41", updated_at: "2012-11-18 15:11:41">

>> Book.count
   (0.2ms)  SELECT COUNT(*) FROM "books" 
=> 0
>>
As we are using the option :dependent => :destroy in the Book model, we can also automatically remove all authors:

>> Book.create(title: 'Homo faber').authors.create(first_name: 'Max', last_name: 'Frisch')

   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 18 Nov 2012 15:12:57 UTC +00:00], ["title", "Homo faber"], ["updated_at", Sun, 18 Nov 2012 15:12:57 UTC +00:00]]
   (2.6ms)  commit transaction

   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 2], ["created_at", Sun, 18 Nov 2012 15:12:57 UTC +00:00], ["first_name", "Max"], ["last_name", "Frisch"], ["updated_at", Sun, 18 Nov 2012 15:12:57 UTC +00:00]]
   (0.9ms)  commit transaction

=> #<Author id: 1, book_id: 2, first_name: "Max", last_name: "Frisch", created_at: "2012-11-18 15:12:57", updated_at: "2012-11-18 15:12:57">

>> Book.count
   (0.3ms)  SELECT COUNT(*) FROM "books" 
=> 1
>> Author.count
   (0.3ms)  SELECT COUNT(*) FROM "authors" 
=> 1
>> Book.first.destroy

  Book Load (0.2ms)  SELECT "books".* FROM "books" LIMIT 1

   (0.1ms)  begin transaction
  Author Load (0.1ms)  SELECT "authors".* FROM "authors" WHERE "authors"."book_id" = 2
  SQL (0.2ms)  DELETE FROM "authors" WHERE "authors"."id" = ?  [["id", 1]]
  SQL (0.1ms)  DELETE FROM "books" WHERE "books"."id" = ?  [["id", 2]]
   (2.2ms)  commit transaction

=> #<Book id: 2, title: "Homo faber", created_at: "2012-11-18 15:12:57", updated_at: "2012-11-18 15:12:57">

>> Author.count
   (0.3ms)  SELECT COUNT(*) FROM "authors" 
=> 0
>>

One key point to note in rails while deleting records is that the instance is frozen after removing the database field. Therefore, even though it is removed from the database, its value is still present in the program, which can no longer be modified, i.e. read only value. To check if an instance is frozen or not, you can use the method frozen?:

>> book = Book.create(title: 'Homo faber')
   (0.1ms)  begin transaction

  SQL (1.2ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 18 Nov 2012 15:14:04 UTC +00:00], ["title", "Homo faber"], ["updated_at", Sun, 18 Nov 2012 15:14:04 UTC +00:00]]
   (2.5ms)  commit transaction

=> #<Book id: 3, title: "Homo faber", created_at: "2012-11-18 15:14:04", updated_at: "2012-11-18 15:14:04">

>> book.destroy
   (0.1ms)  begin transaction
  Author Load (0.2ms)  SELECT "authors".* FROM "authors" WHERE "authors"."book_id" = 3
  SQL (0.2ms)  DELETE FROM "books" WHERE "books"."id" = ?  [["id", 3]]
   (1.9ms)  commit transaction

=> #<Book id: 3, title: "Homo faber", created_at: "2012-11-18 15:14:04", updated_at: "2012-11-18 15:14:04">

>> Book.count
   (0.3ms)  SELECT COUNT(*) FROM "books" 
=> 0
>> book

=> #<Book id: 3, title: "Homo faber", created_at: "2012-11-18 15:14:04", updated_at: "2012-11-18 15:14:04">

>> book.frozen?
=> true
>>

As mentioned above the record has been removed from the database, but the object with all its data is still present in the running Ruby program. The good news is that we can still revive the deleted record, but it will then be a new record.

>> Book.create(title: book.title)
   (0.1ms)  begin transaction

  SQL (0.6ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 18 Nov 2012 15:15:06 UTC +00:00], ["title", "Homo faber"], ["updated_at", Sun, 18 Nov 2012 15:15:06 UTC +00:00]]
   (1.7ms)  commit transaction

=> #<Book id: 4, title: "Homo faber", created_at: "2012-11-18 15:15:06", updated_at: "2012-11-18 15:15:06">

>> exit
$

Rails Delete operation using delete method

Unlike the destroy method, with delete, you can remove a record directly from the database. Any dependencies to other records in the model are not taken into account. The method delete only deletes that one row in the database and nothing else. It is important to note that the delete method is muchfaster than the destroy method.

$ rails console
Loading development environment (Rails 3.2.9)


>> Book.create(title: 'Homo faber').authors.create(first_name: 'Max', last_name: 'Frisch')
   (0.1ms)  begin transaction


  SQL (24.4ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 18 Nov 2012 15:27:59 UTC +00:00], ["title", "Homo faber"], ["updated_at", Sun, 18 Nov 2012 15:27:59 UTC +00:00]]
   (3.0ms)  commit transaction


   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 1], ["created_at", Sun, 18 Nov 2012 15:27:59 UTC +00:00], ["first_name", "Max"], ["last_name", "Frisch"], ["updated_at", Sun, 18 Nov 2012 15:27:59 UTC +00:00]]
   (1.0ms)  commit transaction


=> #<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-11-18 15:27:59", updated_at: "2012-11-18 15:27:59">


>> Book.count
   (0.3ms)  SELECT COUNT(*) FROM "books" 
=> 1
>> Author.count
   (0.3ms)  SELECT COUNT(*) FROM "authors" 
=> 1
>> Book.last.delete
  Book Load (0.3ms)  SELECT "books".* FROM "books" ORDER BY "books"."id" DESC LIMIT 1
  SQL (2.8ms)  DELETE FROM "books" WHERE "books"."id" = 1


=> #<Book id: 1, title: "Homo faber", created_at: "2012-11-18 15:27:59", updated_at: "2012-11-18 15:27:59">


>> Book.count
   (0.3ms)  SELECT COUNT(*) FROM "books" 
=> 0
>> Author.count
   (0.3ms)  SELECT COUNT(*) FROM "authors" 
=> 1
>> Author.last
  Author Load (0.3ms)  SELECT "authors".* FROM "authors" ORDER BY "authors"."id" DESC LIMIT 1


=> #<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-11-18 15:27:59", updated_at: "2012-11-18 15:27:59">


>> exit
$

The record of the book 'Homo faber' is deleted, but the author is still in the database. As with destroy, an object also gets frozen when you use delete. The record is already removed from the database, but the object itself is still there. Although it is often much faster than the alternative, #destroy, skipping callbacks might bypass business logic in your application that ensures referential integrity or performs other essential jobs.

Soft Deleting records using gems in Rails

Finally, we will see a brief overview about soft deleting in rails.

So what does soft deletion mean?

It is an operation in which a flag is used to mark data as unusable rather than deleting it permanently from the database.

Many times, we don’t want to delete a record permanently, but may not want to build an archiving system manually. Rails provides a way to do just with gems such as paranoia for soft deleting. Soft deleting has a few advantages such as:
  • Easier/faster undeletes
  • History tracking (keeping deleted rows around for auditing purposes)
Adding the method call acts_as_paranoid to the model that you want the functionality added to, as shown below, will hijack the ActiveRecord’s standard destroy and destroy! Functionality. If you call either of these methods, instead of the record being deleted from the database, its deleted_at column is updated from nil to the current timestamp. Resulting in a soft deleted record.

class User < ActiveRecord::Base
  acts_as_paranoid
end

If you call destroy or destroy! a second time (i.e. on a record that has already been soft deleted), it will be actually deleted from the database. Alternatively, you can call destroy_fully! from the beginning to skip the soft delete.