A Look into Discard
Ali Ilman
Posted on September 2, 2019
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.
Posted on September 2, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.