/ Tags: RAILS-CODE / Categories: SOLUTIONS

Soft Delete Records In Rails Without A Gem Using Discarded_at

Some records should disappear from the application without leaving the database — orders referenced by invoices, users referenced by audit logs. A nullable timestamp column and a default scope gives you soft deletion in about fifteen lines.

Description

Add a nullable discarded_at timestamp. “Discarding” sets it to now; “undiscarding” sets it back to nil. Scopes named kept and discarded express the two states, and a default_scope keeps discarded rows out of ordinary queries. Use a partial unique index so a discarded record doesn’t block a new one from reusing the same email or slug — this is the detail most hand-rolled soft deletes miss. Keep really_destroy! available for GDPR deletion requests and for cleaning up test data.

Sample input:

  user.discard!
  User.count            # excludes the discarded row
  User.with_discarded.count


Sample Output:

  # => discarded_at set; row still present in the table

Answer

  # Migration
  class AddDiscardedAtToUsers < ActiveRecord::Migration[7.1]
    disable_ddl_transaction!

    def change
      add_column :users, :discarded_at, :datetime
      add_index  :users, :discarded_at, algorithm: :concurrently

      # Uniqueness that ignores discarded rows
      remove_index :users, :email
      add_index :users, :email,
                unique: true,
                where: "discarded_at IS NULL",
                algorithm: :concurrently
    end
  end

  # Concern
  module Discardable
    extend ActiveSupport::Concern

    included do
      scope :kept,      -> { where(discarded_at: nil) }
      scope :discarded, -> { where.not(discarded_at: nil) }

      default_scope { kept }
    end

    class_methods do
      def with_discarded = unscope(where: :discarded_at)
    end

    def discard!  = update!(discarded_at: Time.current)
    def undiscard! = update!(discarded_at: nil)
    def discarded? = discarded_at.present?

    def really_destroy! = self.class.with_discarded.where(id: id).delete_all
  end

  class User < ApplicationRecord
    include Discardable
  end

Learn More

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More