DEV Community

Ali Ilman
Ali Ilman

Posted on

A Look into Discard

Originally published on ali-ilman.com/blog

Sometimes, we want to soft delete specific records in our app.
What is a soft delete?

A soft delete is a term for flagging a record in the DB as deleted. A use case for this is when you want it to be hidden from the public but visible to the admin for record purposes.

In the world of Rails, there are various ways you can do that. You can write your own soft delete feature or you can use a gem such as discard.

A Look Into Discard

Discard is a gem created by John Hawthorn, describing the gem as soft deletes for ActiveRecord done right. The gem is basically a simple ActiveRecord mixin to add conventions for flagging records as discarded.

Setup

For this example, I'll create an app where there are authors, books and publishers tables.
An Author can have many Books, a Publisher can have many Authors, and a Publisher can have many Books through Authors.

class Author < ApplicationRecord
  belongs_to :publisher, optional: true
  has_many :books, dependent: :nullify
end

class Book < ApplicationRecord
  belongs_to :author
end

class Publisher < ApplicationRecord
  has_many :authors, dependent: :nullify
  has_many :books, through: :authors
end

Add this into your gemfile and then run bundle.

gem 'discard', '~> 1.0'

For the record that you want to be discardable, in our case, Author, include discard in the author model like so.

class Author < ApplicationRecord
  include Discard::Model
end

Then, you generate a migration to add the relevant attributes to the authors table.

rails generate migration add_discarded_at_to_authors discarded_at:datetime:index

How to discard a record

author = Author.first
author.discard # => true
author.discarded_at # => Sun, 01 Sep 2019 10:48:31 UTC +00:00
author.discarded? # => true

How to undiscard a record

author = Author.first
author.undiscard # => true
author.discarded_at # => nil
author.discarded? # => false

Accessing records

Discard adds the following 4 methods for the discardable record.

Author.with_discarded # an alias for Author.all
Author.kept # returns a collection of undiscarded authors
Author.discarded # returns a collection of discarded authors
Author.undiscarded # an alias for Author.kept

Callbacks?!?!?!

Discard adds before_, around_ and after_ callbacks for you to utilise if needed.

class Author < ApplicationRecord
  ...
  before_discard { puts 'Hallo before_discard!' }
  after_discard { puts 'after_discard!' }

  before_undiscard { puts 'Waving at you before_undiscard...' }
  after_undiscard { puts 'Yay after_undiscard!' }
  ...
end
> Author.first.discard
# other logs
Hallo before_discard!
# other logs
after_discard!
=> true

> Author.first.undiscard
# other logs
Waving at you before_undiscard...
# other logs
Yay after_undiscard!
=> true

Unfortunately though, the around_ callbacks don't seem to work. An example would be, Author.first.discard. It'll query for the author and run the code in around_discard but it won't discard the author. It may be a bug within the gem. 🤔

Summary

Discard removes the magic that comes with alternatives such as paranoia and acts_as_paranoid.

I've used paranoia on a few projects, and one of them's a project of a decent size. I remember initially making most models paranoid. We encountered issues, one of them being a has_many dependent: :destroy association. Those dependent records needed to be paranoid as well, otherwise the records will be destroyed for good. 😱 Another issue is that, although I can't exactly call it, one of the records has a has_many_ association and it was searching for a soft-deleted record using an id that still exist within the collection, but the said record with the existing id couldn't be found! 🤯

One might argue that discard goes against convention over configuration, which is one of the principles of Rails, and can add verbosity to the codebase. An example would be, you'd have to keep on adding .kept to a model or an association to access a collection of undiscarded records. But personally, it's easy to setup and use. Plus unlike the other two gems, discard doesn't change the model's default_scope. I suggest you to go with discard to avoid possible long-term pain. 😉

You can check out the code for the app above here where you can select different branches to view the implementation with discard or paranoia.

Top comments (0)