Skip to content

Instantly share code, notes, and snippets.

@therrick
Last active August 19, 2016 14:38
Show Gist options
  • Select an option

  • Save therrick/11cce78cc13b3cfe0301d94e2901e223 to your computer and use it in GitHub Desktop.

Select an option

Save therrick/11cce78cc13b3cfe0301d94e2901e223 to your computer and use it in GitHub Desktop.
Rails 4.2.4 + Paranoia issue for with_deleted on belongs_to
# I've been troubleshooting an issue that cropped up in our app when we upgraded from
# rails 4.1 to 4.2. We make heavy use of the 'paranoia' gem to allow for soft-deleting
# records via a deleted_at attribute. The gem uses default scopes that exclude
# `deleted_at: nil` records from default queries.
#
# In our join tables, we unscope the belongs_to relation because once the app has found
# its way to a record in the join table, it expects to be able to see the joined
# records, even if those records have been deleted.
#
# Starting with Rails 4.2.4, it appears that this unscope on the belongs_to unexpectedly
# affects the has_many relation from the other side, unscoping that relation and
# causing records with deleted_at set to be returned.
#
# I've created this test script to demonstrate the problem. The key parts of the
# example are the following:
#
# ```
# class Product < ActiveRecord::Base
# has_many :category_products
# has_many :categories, through: :category_products
#
# default_scope { where deleted_at: nil }
# end
#
# class CategoryProduct < ActiveRecord::Base
# belongs_to :product, -> { unscope(where: :deleted_at) }
# belongs_to :category, -> { unscope(where: :deleted_at) }
# end
#
# class Category < ActiveRecord::Base
# has_many :category_products
# has_many :products, through: :category_products
#
# default_scope { where deleted_at: nil }
# end
# ```
#
# With this setup, after marking a product's category as deleted, I expect
# that product.categories should no longer return the category. In 4.2.3 it did not,
# but in 4.2.4 and later, it does.
#
begin
require 'bundler/inline'
rescue LoadError => e
$stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
raise e
end
gemfile(true) do
source 'https://rubygems.org'
# Activate the gem you are reporting the issue against.
gem 'activerecord', '4.2.7'
gem 'sqlite3'
end
require 'active_record'
require 'minitest/autorun'
require 'logger'
# Ensure backward compatibility with Minitest 4
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :products, force: true do |t|
t.string :name
t.datetime :deleted_at
end
create_table :category_products, force: true do |t|
t.integer :product_id
t.integer :category_id
end
create_table :categories, force: true do |t|
t.string :name
t.datetime :deleted_at
end
create_table :category_product_visits, force: true do |t|
t.string :user_name
t.integer :category_product_id
end
end
class Product < ActiveRecord::Base
has_many :category_products
has_many :categories, through: :category_products
default_scope { where deleted_at: nil }
end
class CategoryProduct < ActiveRecord::Base
# we unscope the belongs_to relations because if we are looking at a
# CategoryProduct record (found via the CategoryProductVisit table, perhaps),
# we expect to be able to see its category or product, even if the category or product
# has been marked as deleted
belongs_to :product, -> { unscope(where: :deleted_at) }
belongs_to :category, -> { unscope(where: :deleted_at) }
end
class Category < ActiveRecord::Base
has_many :category_products
has_many :products, through: :category_products
default_scope { where deleted_at: nil }
end
class CategoryProductVisit < ActiveRecord::Base
belongs_to :category_product
end
class BugTest < Minitest::Test
def test_association_stuff
product1 = Product.create! name: 'Product 1'
category1 = Category.create! name: 'Category 1'
category1.products << [product1]
product1.reload
assert_equal 1, product1.categories.count
# a user visits a product in a category. let's record that!
visit1 = CategoryProductVisit.create! user_name: 'tom',
category_product: category1.category_products.first
# now we delete the category
category1.deleted_at = DateTime.now
category1.save!
# the visit record can still see the product and category
visit1.reload
assert_equal visit1.category_product.product.name, 'Product 1'
assert_equal visit1.category_product.category.name, 'Category 1'
product1.reload
# in rails 4.2.3, the following test passes, which is the desired behavior
assert_equal 0, product1.categories.count
# in rails 4.2.4 and later, product1.categories.count == 1, which is not desired behavior
#assert_equal 1, product1.categories.count
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment