SimplyRichAssociation

This plugin is based on Chad Fowler's Rails Recipe #18 :
Self-referential Many-to-Many Relationships in Rails Recipe book.

This plugin simplifies self referential many to many relationship. It manages the
bidirectional link creation and deletion automatically.

For example:

class Person < ActiveRecord::Base
  has_and_belongs_to_many :friends,
                            :class_name => "Person",
                            :join_table => "friends_people",
                            :association_foreign_key => "friend_id",
                            :foreign_key => "person_id",
                            :after_add => :be_friendly_to_friend,
                            :after_remove => :no_more_mr_nice_guy
    def be_friendly_to_friend(friend)
      friend.friends << self unless friend.friends.include?(self)
    end
    
    def no_more_mr_nice_guy(friend)
      friend.friends.delete(self) rescue nil
    end
end

becomes:

class Person < ActiveRecord::Base
  has_self_referential_many_to_many :friends
end


In the script/console you can see:

~/work/plugins/sra > script/console
Loading development environment.
>> p1 = Person.create :name => "Chad"
=> #<Person:0x31dd5c8 @errors=#<ActiveRecord::Errors:0x31d4644 @errors={}, @base=#<Person:0x31dd5c8 ...>, new_recordfalse, attributes{"name"=>"Chad", "id"=>48}, new_record_before_savetrue
>> p2 = Person.create :name => "Tintin"
=> #<Person:0x31cb92c @errors=#<ActiveRecord::Errors:0x31cafa4 @errors={}, @base=#<Person:0x31cb92c ...>, new_recordfalse, attributes{"name"=>"Tintin", "id"=>49}, new_record_before_savetrue
>> p1.friends << p2
=> [#<Person:0x31cb92c @errors=#<ActiveRecord::Errors:0x31cafa4 @errors={}, @base=#<Person:0x31cb92c ...>, new_recordfalse, friends[#<Person:0x31dd5c8 @errors=#<ActiveRecord::Errors:0x31d4644 @errors={}, @base=#<Person:0x31dd5c8 ...>, new_recordfalse, friends[....], attributes{"name"=>"Chad", "id"=>48}, new_record_before_savetrue], attributes{"name"=>"Tintin", "id"=>49}, new_record_before_savetrue]
>> p1.friends.size
=> 1
>> p2.friends.size
=> 1

It also provides syntactic sugar for ActiveRecord has_many :through macro:

For instance if you have a migration as:

class Subscription < ActiveRecord::Migration

  def self.up
    create_table :subscriptions do |t|
      t.column :reader_id, :integer
      t.column :magazine_id, :integer
      t.column :last_renewal_on, :date
      t.column :length_in_issues, :integer
    end

    create_table :magazines do |t|
      t.column :title, :string
    end
    
    create_table :readers do |t|
      t.column :name, :string
    end
  end

  def self.down
  end
end

and the models as:

class Subscription < ActiveRecord::Base
belongs_to :reader
belongs_to :magazine
end

class Reader < ActiveRecord::Base
has_many :subscriptions
has_many :magazines, :through => :subscriptions
end

class Magazine < ActiveRecord::Base
has_many :subscriptions
has_many :readers, :through => :subscriptions
end

using this plugin, your models become:

class Subscription < ActiveRecord::Base
belongs_to :reader
belongs_to :magazine
end

class Reader < ActiveRecord::Base
has_many_through :magazines, :subscriptions
end

class Magazine < ActiveRecord::Base
has_many_through :readers, :subscriptions
end

This example is based on Rails Recipe #22, Many-to-Many Relationships with
Extra Data of Chad Fowler's Rails Recipes book.

~/work/plugins/test/hmt > script/console
Loading development environment.
>> m = Magazine.create :title => "Ruby Illustrated"
=> #<Magazine:0x31d9e8c @errors=#<ActiveRecord::Errors:0x31d0f08 @errors={}, @base=#<Magazine:0x31d9e8c ...>, new_recordfalse, attributes{"title"=>"Ruby Illustrated", "id"=>2}, new_record_before_savetrue
>> r = Reader.create :name => "TinTin"
=> #<Reader:0x31ba3e8 @errors=#<ActiveRecord::Errors:0x31b89bc @errors={}, @base=#<Reader:0x31ba3e8 ...>, new_recordfalse, attributes{"name"=>"TinTin", "id"=>2}, new_record_before_savetrue
>> s = Subscription.create(:last_renewal_on => Date.today, :length_in_issues => 6)
=> #<Subscription:0x357846c @errors=#<ActiveRecord::Errors:0x3571860 @errors={}, @base=#<Subscription:0x357846c ...>>, @new_record=false, @attributes={"last_renewal_on"=>#<Date: 4908351/2,0,2299161>, "id"=>2, "length_in_issues"=>6, "reader_id"=>nil, "magazine_id"=>nil}>
>> m.subscriptions << s
=> [#<Subscription:0x357846c @errors=#<ActiveRecord::Errors:0x3571860 @errors={}, @base=#<Subscription:0x357846c ...>>, @new_record=false, @attributes={"last_renewal_on"=>#<Date: 4908351/2,0,2299161>, "id"=>2, "length_in_issues"=>6, "reader_id"=>nil, "magazine_id"=>2}>]
>> r.subscriptions << s
=> [#<Subscription:0x357846c @errors=#<ActiveRecord::Errors:0x3571860 @errors={}, @base=#<Subscription:0x357846c ...>>, @new_record=false, @attributes={"last_renewal_on"=>#<Date: 4908351/2,0,2299161>, "id"=>2, "length_in_issues"=>6, "reader_id"=>2, "magazine_id"=>2}>]
>> s.save
=> true
>> m.readers
=> [#<Reader:0x353c1c4 @attributes={"name"=>"TinTin", "id"=>"2"}]
>> r.magazines
=> [#<Magazine:0x35378a4 @attributes={"title"=>"Ruby Illustrated", "id"=>"2"}]
>>

CREDITS: Thanks to Chad Fowler for his help during the development of this plugin.