Rails for beginners : How to make a 'soft delete' ?

20 January
  • tutorial, programming, technical, tools

Deleting something from your database is very common and handled very easily by rails as part of a CRUD action. In some cases, it is useful to create a “soft delete”, meaning that the deleted element will still exist in the database but won’t appear to the user. This method is useful if you or a user accidentally delete something and would like to retrieve it.

In this article, we’re going to work through an easy way to create an ‘Archive’ method and hide it from a user when they use it.

Let’s start with a fairly easy object.

First thing let’s create our book model. Let’s say a book has an author and a number of pages

rails g model Book author:string pages:integer

You should have something like this: book.rb

class Book < ApplicationRecord
end

Now we are going to add to our routes.rb

resources :books

Now that we have our model and the different routes associated with it, we are going to generate our controller

rails g controller Books

Now we are going to fill up that controller with our simple CRUD actions.

class BooksController < ApplicationController

  def index
    @books = Book.all
  end
 def show
    @book = Book.find(params[:id])
  end
 def new
    @book = book.new
  end

 def create
    @book = Book.new(book_params)
    @book.save
    redirect_to book_path(@book)
  end

 def edit
    @book = Book.find(params[:id])
  end

 def update
    @book = Book.find(params[:id])
    @book.update(book_params)
    redirect_to book_path(@book)
  end

 def destroy
    @book = Book.find(params[:id])
    @book.destroy
    redirect_to books_path :id
  end

 private

  def book_params
    params.require(:book).permit(:author, :pages)
  end

end

At this point you should be able to operate your CRUD actions; create, view, edit, update and destroy any books from your database. Now we’re getting into the interesting part. The destroy method is irreversible, so you might want to find a way to create a “soft delete” meaning that your book will still exist in the database but will be hidden from the user.

The first step is to create an additional ‘status’ column in the db for books. In order to do this let’s run a migration.

rails g migration AddStatusToBooks status:string

But wait up, before you run rails db:migrate we’re going to add some important things to our migration file

class AddStatusToBooks < ActiveRecord::Migration[6.0]
 def change
   add_column :books, :status, :string, default: 'active', null: false
 end
end

Adding this will make the book ‘active’ by default when creating a new record in your db.

We can now run rails db:migrate

Once the migration is complete, return to your Book.rb model and add some scopes

class Book < ApplicationRecord

 def self.active
   where(status:'active')
 end
 
 
 
 def self.archived
   where(status: 'archived')
 end

  def active?
   status == "active"
 end

 def archived?
   status == "archived"
 end
end 

These scopes can be used in your views to access the active (or the archived books) So if you wanted to show all the books in your index that are active, you would do something in those lines

Index.html.erb

<%  @books.archived.each do |book| %>
  <%= book.author %>
  <%= book.pages %>
<% end %>

By using the self.active scope that we have defined in our model, we are able to only select the books that have the ‘active’ status. None of our books have that status because we have not given the ability to archive for a user.

Let’s go back to our controller and add an archive method

def archive
  @book = Book.find(params[:id])
  @book.update(status: 'archived')
  redirect_to :books, notice: 'Successfully Archived'
 end

In order to get this to work, we need modify routes.rb and add a route to the action.

resources :books do
  put :archive, on: :member
end

If your run rails routes | grep book in your terminal you should see a new route in addition to our previous CRUD ones.

Let’s go back to out index.html.erb

<%  @books.active.each do |book| %>
 <%= book.author %>
 <%= book.pages %>
 <%= link_to 'Delete', [:archive, book], method: :put %>
<% end %>

And there you go ! If a user clicks on Delete, the book will be archived and it will still be accessible in the database ! If you have an admin side for example you can use the book.archived to see all the books.

BONUS : If you are having an admin side and you want to restore the book you can !

Go to your controller and add a restore method

def restore
   @book.update(status: 'active')
   redirect_to :books, notice: 'Successfully restored'
 end

Go to your routes.rb and add the restore path

resources :books do
  put :archive, on: :member
  put :restore, on: :member
end 

In your index.html.erb

<%  @books.archived.each do |book| %>
  <%= book.author %>
  <%= book.pages %>
  <%= link_to 'restore', [:restore, book], method: :put %>
<% end %>

You can also use

<%  @books.each do |book| %>
 <%= book.author %>
 <%= book.pages %>
   <% if book.archived? %>
     <%= link_to 'Restore', [:restore, book], method: :put %>
   <% end %>
   <% if book.active? %>
     <%= link_to 'Delete', [:archive, book], method: :put %>
   <% end %>
<% end %>

Here you go ! hope you found this little tutorial useful !


Have a project? Get in touch and we can talk it through.