Rails 6.0 ActiveRecord soft delete overriding default_scope and causing problems with single table inheritance (STI)

140 views Asked by At

I'm working on a Rails 6.0 application that has the following soft delete ActiveRecord concern defined:

module ActiveRecordSoftDeletion
  extend ActiveSupport::Concern
  
  def self.included(base)
    base.scope :not_deleted, -> { base.unscoped.where(deleted: false) }
    base.send(:default_scope) { base.not_deleted }
    base.scope :only_deleted, -> { base.unscope(where: :deleted).where(deleted: true) }

    def destroy
      self.deleted = true
      self.save(validate: false)
    end

    def recover
      self.deleted = false
      self.save(validate: false)
    end
  end
end

The concern has worked fine for a long time and is widely used throughout the application.

Recently we introduced single table inheritance, which was also working fine until we decided to include ActiveRecordSoftDeletion to the base class.

Here's an example of my STI classes...

class Widget < ApplicationRecord
  include ActiveRecordSoftDeletion
end

class Thingy < Widget
end

class Dohicky < Widget
end

Before adding ActiveRecordSoftDeletion to Widget, the code behaved like this...

> Thingy.first
SELECT "widgets".* FROM "widgets" WHERE "widgets"."type" = $1
ORDER BY "widgets"."id" ASC LIMIT $2  [["type", "Thingy"], ["LIMIT", 1]]

> Dohicky.first
SELECT "widgets".* FROM "widgets" WHERE "widgets"."type" = $1
ORDER BY "widgets"."id" ASC LIMIT $2  [["type", "Dohicky"], ["LIMIT", 1]]

The problem is ActiveRecordSoftDeletion is replacing the default scope. So after including ActiveRecordSoftDeletion the queries look like this...

> Thingy.first
SELECT "widgets".* FROM "widgets" WHERE "widgets"."deleted" = $1
ORDER BY "widgets"."id" ASC LIMIT $2  [["deleted", false], ["LIMIT", 1]]

> Dohicky.first
SELECT "widgets".* FROM "widgets" WHERE "widgets"."deleted" = $1
ORDER BY "widgets"."id" ASC LIMIT $2  [["deleted", false], ["LIMIT", 1]]

I'd like to avoid changing anything in ActiveRecordSoftDeletion if possible, since that code is used in many places. But I can't find a way to get the STI classes to add their default scope back.

The documentation for scopes clearly states if you have multiple default scopes they get ANDed together but something about the way the concern is doing things seems to replace the default scope rather than add to it.

Adding the default scope to the STI classes almost works but not quite.

If I change the base class to this...

class Widget < ApplicationRecord
  include ActiveRecordSoftDeletion
  default_scope { where(type: sti_name) }
end

Then all my queries become...

SELECT "widgets".* FROM "widgets"
WHERE "widgets"."deleted" = $1 AND "widgets"."type" = $2
ORDER BY "widgets"."id" ASC LIMIT $3
[["deleted", false], ["type", "Widget"], ["LIMIT", 1]]

even for the subclasses; they all look for type Widget.

Even doing this has the same result...

class Widget < ApplicationRecord
  include ActiveRecordSoftDeletion
end

class Thingy < Widget
  default_scope { where(type: sti_name) }
end

class Dohicky < Widget
  default_scope { where(type: sti_name) }
end

How can I add the correct STI default scope back to the models without changing the soft delete code?

Alternately, how can I update the soft delete code without impacting other models that use the concern?

0

There are 0 answers