In one of my projects, I wanted to prevent instances of particular classes to be deleted if they were in any way associated to another object. In database terms, I wanted an ON DELETE RESTRICT constraint.
Since I cannot rely on the database to enforce it for me (MySQL 4, MyISAM engine), I coded the following:
test/unit/city_test.rb
1 require File.dirname(FILE) + ‘/../test_helper’
2
3 class CityTest < Test::Unit::TestCase
4 fixtures :cities, :contacts
5
6 def setup
7city</span> = <span class="co">City</span>.find(<span class="sy">:first</span>) <span class="no"> 8</span> <span class="r">end</span> <span class="no"> 9</span> <span class="no"><strong>10</strong></span> <span class="r">def</span> <span class="fu">test_prevent_destruction_if_associated_to_any_contact</span> <span class="no">11</span> <span class="iv">city.contacts << contacts(:jill)
12city</span>.destroy <span class="no">13</span> assert_not_nil <span class="co">City</span>.find(<span class="iv">city.id), ‘should not have been destroyed’
14 assert_match /cannot destroy.*contacts/i,city</span>.errors.on_base, <span class="no"><strong>15</strong></span> <span class="s"><span class="dl">'</span><span class="k">reports error condition to user</span><span class="dl">'</span></span> <span class="no">16</span> <span class="r">end</span> <span class="no">17</span> <span class="no">18</span> <span class="r">def</span> <span class="fu">test_allow_destruction_if_not_associated_to_any_contact</span> <span class="no">19</span> <span class="iv">city.destroy
20 assert_raises ActiveRecord::RecordNotFound do
21 City.find(@city.id)
22 end
23 end
24 end
app/models/city.rb
1 class City < ActiveRecord::Base
2 has_and_belongs_to_many :contacts, :join_table => ‘contacts_cities’
3
4 def destroy
5 unless self.contacts.empty?
6 self.errors.add_to_base \
7 ‘We cannot destroy this instance since one or more contacts refer to it’)
8 return
9 end
10
11 super
12 end
13 end
I had to override destroy because in has_and_belongs_to_many relationships, Rails deletes join table records before deleting the record. This means that in before_destroy filters, self.contacts.empty? will always report true. Ticket #1183: dependents are destroyed before client before_destroy hooks are called is already opened on this issue.
UPDATE (2006-03-08) Fixed since 2006-02-13