class Foo < ActiveRecord::Base has_one :bar, :dependent => :delete def before_destroy if self.class.count == 1 raise end end end class Bar < ActiveRecord::Base belongs_to :foo end
どこにでもある関連。最後の Foo は消さないでね、という状況。
さて、
>> [Foo, Bar].each(&:delete_all) >> 2.times{Foo.create.bar = Bar.create} >> [Foo, Bar].map(&:count) => [2, 2] >> Foo.find(:all, :include => :bar).each(&:destroy) RuntimeError: (snip) >> [Foo, Bar].map(&:count) => [1, 1]
最後の Foo は、Bar とともに生き残っている。でもこれ、test が書けなくない?
$ cat test/unit/foo_test.rb require File.dirname(__FILE__) + '/../test_helper' class FooTest < Test::Unit::TestCase def test_truth [Foo, Bar].each(&:delete_all) 2.times{Foo.create.bar = Bar.create} assert_equal 2, Foo.count assert_equal 2, Bar.count assert_raise(RuntimeError){ Foo.find(:all, :include => :bar).each(&:destroy) } assert_equal 1, Foo.count assert_equal 1, Bar.count, "last Bar should be alive, as well" end end $ ruby test/unit/foo_test.rb Loaded suite test/unit/foo_test Started F Finished in 0.28703 seconds. 1) Failure: test_truth(FooTest) [test/unit/foo_test.rb:13]: last Bar should be alive, as well. <1> expected but was <0>. 1 tests, 5 assertions, 1 failures, 0 errors
本来なら、transaction が効いて Bar も保護されるはずなんだけど、 test 環境では、初っ端から transaction に入って最後に抜けてロールバック、 という感じなので、テスト中には Bar が戻ってこないですよね。:delete じゃなくて :destroy でも同じで、:nullify だと <1>-<0> が <2>-<1> になるだけ。
[追記]: そうか。:dependent って結局はクラスメソッドの方の before_destroy なので、 それより先にクラスメソッドの方の before_destroy でセットしとけばいいのか。 これで上に書いたテストが通りました。 でも、ここに書くなら、raise じゃなくて、エラーをセットして false を返す方がいいのかも。
class Foo < ActiveRecord::Base before_destory do |record| if record.class.count == 1 record.errors.add(:base, "save the last Foo") false end end has_one :bar, :dependent => :delete end