Rails Before_destroy Gotcha
So there’s a Category that has many Expenses. The expenses for a category are destroyed when the category is destroyed. Elsewhere in my logic I would like to know if the category is removable based on whether it has any expenses and, if it does, whether any of the expenses it has are greater than zero.
class Category < ActiveRecord::Base
has_many :items, :dependent => :destroy
def removable?
self.expenses.find(:first, :conditions => ['total_cost > ?', 0], :select => 'id').nil?
end
end
Should really get closer to the vest when protecting the data. So I added a callback to protect the category. Conveniently I can reuse the removable?
method:
before_destroy :removable?
Let’s test it:
def test_should_destroy__when_removable
assert_difference(Category, :count, -1) do
@category.destroy
end
end
Yay!
Loaded suite /Users/bjhess/Sites/.../test/unit/category_test
Started
.
Finished in 1.950119 seconds.
1 tests, 3 assertions, 0 failures, 0 errors
And the other test:
def test_should_not_destroy__when_not_removable
create_expense(:total_cost => 50.00)
assert_no_difference(Category, :count) do
@category.destroy
end
end
NO!
Loaded suite /Users/bjhess/Sites/.../test/unit/expense_category_test
Started
F
Finished in 1.965099 seconds.
1) Failure:
test_should_not_destroy__when_not_removable(CategoryTest)
method assert_no_difference in test_helper.rb at line 58
method test_should_not_destroy__when_not_removable in category_test.rb at line 66
<8> expected but was
<7>.
1 tests, 2 assertions, 1 failures, 0 errors
What the crap? Apparently the conditional in my removable?
method must be working within the transactional space of the destroy call. See, a category has many expenses, set with :dependent => :destroy
. In terms of the transaction, all of my expenses have already been destroyed and the category is indeed removable.
If I were to work entirely in memory, things come out alright.
before_destroy :check_expenses_total
private
def check_expenses_total
self.expenses.inject(0){ |sum, expense| sum + expense.total_cost } <= 0
end
So the lesson, I suppose, is be careful how DB-interactive you’re being in your destroy callbacks. Oh, and test.
(Another weird thing. self.expenses.sum(&:total_cost)
didn’t seem to work. I had to use inject.)