Redundant Bidirectional Relationships in Rails Suck
Update 06-JUN-2008: This plugin now includes acts_as_union, and we moved the repository to GitHub.
A better-late-than-never announcement: we released a Rails plugin a while ago that implements a better, DRYer way to roll network relationships using ActiveRecord. It's called, acts_as_network and it now updated to support Rails 2.0.
So why is this such a problem? It may not be immediately apparent, but the short answer is that these types of relationships usually require 2 redundant rows of storage in your database. Take a social network relationship: one record might say that Jack is Jill's friend, but a separate row must be present to say Jill is Jack's friend.
acts_as_network does away with this nonsense, and lets you say implicitly that If Jack is Jill's friend then Jill is Jack's friend. Or, in Ruby
# Jane invites Jack to be friends invite = Invite.create(:person => jane, :person_target => jack, :message => "let's be friends!") jane.friends.include?(jack) => false # Jack is not yet Jane's friend jack.friends.include?(jane) => false # Jane is not yet Jack's friend either invite.is_accepted = true # Now Jack accepts the invite invite.save and jane.reload and jack.reload jane.friends.include?(jack) => true # Jack is Janes friend now jack.friends.include?(jane) => true # Jane is also Jacks friend
The syntax is clean, and it stores only one row in your HABTM table. Online Documentation available or install/upgrade the plugin:
% script/plugin install git://github.com/sjlombardo/acts_as_network.git % rake doc:plugins
Much thanks to Maurycy for submitting patches to AAN!
Note: for a more in depth look at the acts_as_network syntax and usage please check out the original release page.

Saturday, May 10, 2008
Tobin says:
Hey Stephen, this is great, it's exactly what I was looking for. Do you mind if I ask you a question? I'm having a problem.
<br/><br/>
Here's my setup (Rails 2.0.2):
<pre>
create_table :connections do |t|
t.column "inviter_id", :integer
t.column "invited_id", :integer
t.column "state", :string
end
class Machine < ActiveRecord::Base
acts_as_network :network, :through => :connections, :conditions => "state = 'accepted'"
end
class Connection < ActiveRecord::Base
belongs_to :inviter, :class_name => 'Machine', :foreign_key => 'inviter_id'
belongs_to :invited, :class_name => 'Machine', :foreign_key => 'invited_id'
end
</pre>
I'm getting the following error (in IRB):
<pre>
>> m = Machine.find :first
=> #<Machine id: 1>
>> m.friends
ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) :machine in model Connection. Try 'has_many :friends_in, :through => :connections_in, :source => <name>'. Is it one of :invited or :inviter?
from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/reflection.rb:179:in `check_validity!'
from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations/has_many_through_association.rb:6:in `initialize'
from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations.rb:1032:in `new'
from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/associations.rb:1032:in `friends_in'
from (eval):2:in `friends'
from (irb):12
</pre>
Am I missing something? I thought the friends method was supposed to have been added (along with 4 others).
<br/><br/>
(just to clarify, there are no connections records yet, I'm expecting back empty arrays)
<br/><br/>
Thanks.
Tuesday, May 27, 2008
Stephen says:
@tobin - The virtual relationship accessor is named based on the first parameter to the acts_as_network call. So, in the example code you posted the Machine class should respond like this:
<pre>
>> m = Machine.find :first
=> #<machine>
>> m.network
[]
</pre>
If you'd like the relationship to be called <code>friends</code> then you'll need to define as such:
<pre>
class Machine < ActiveRecord::Base
acts_as_network :friends, :through => :connections, :conditions => "state = 'accepted'"
end
</pre>
Hope that helps clarify!
Tuesday, June 10, 2008
Kim Chirnside says:
I have run into problems when using Models with two words in their name. For example:
class BigFoo < ActiveRecord::Base
acts_as_network :friends, :through => :invites, :conditions => "is_accepted = 't'"
end
It fails due to your use of 'name.downcase' when creating the has_many relationships 'in' and 'out'. (Line 235-247 in network.rb)
It tries to define the source relationship as 'bigfoo_target' and 'bigfoo' when it should be 'big_foo_target' and 'big_foo'.
I have remedied this on my copy by changing the name.downcase to name.tableize.singularize
This should be safer. I haven't run the unit tests so this needs further investigation. But it would be good if a fix to this did make it into your repo.
Other than that, this is a great addition to active record. Thanks!!
Kim
Tuesday, June 10, 2008
Kim Chirnside says:
Sorry about the lack of formatting in that previous comment. There are 3 lines of code near the top which you can hopefully extract.
I'm not up to speed with blogging and the likes.
Kim
Friday, July 18, 2008
Jason Galvin says:
Looks like a very useful plugin. I'm getting two failed tests right from the start. Anyone else experiencing this? Thanks.
1) Failure:
test_assigments_conditions(ActsAsNetworkTest) [test/network_test.rb:252]:
<[#<Person id: 9, name: "Alex">]> expected but was <[]>.
2) Failure:
test_hmt_assignments(ActsAsNetworkTest) [test/network_test.rb:224]:
<false> is not true.
23 tests, 103 assertions, 2 failures, 0 errors
acts_as_network version: 0.2
rails version: 2.0.2
ruby version: 1.8.6 [i386-mswin32]
Saturday, July 19, 2008
Billy Gray says:
<p>@Kim,</p>
<p>
Thanks so much for finding this and working out the fix. We'll definitely work it in to the plugin asap. And yes, we need to textile these comments soon so you don't have to format them!</p>
<p>@Jason Galvin,</p>
<p>We just ran tests in Rails 2.0.2 and Rails 2.1 to confirm and everything's running just fine. Are you still using the old subversion plugin on rubyforge? We've moved to GitHub and I've updated the article above to reflect that.</p>
<p>Head over here to get the newest goods:</p>
<p><a href="http://github.com/sjlombardo/acts_as_network/tree/master">http://github.com/sjlombardo/acts_as_network/tree/master</a></p>
<p>You can use this command to get the freshest stuff:</p>
<pre>script/plugin install git://github.com/sjlombardo/acts_as_network.git</pre>
Sunday, July 20, 2008
Billy Gray says:
<p>@Kim, Stephen committed a fix for the issue you reported on GitHub yesterday:</p>
<a href="http://github.com/sjlombardo/acts_as_network/commit/c7fcb8aacb8b41e37cbd5b7e54fe8816c67af2fa">http://github.com/sjlombardo/acts_as_network/commit/c7fcb8aacb8b41e37cbd5b7e54fe8816c67af2fa</a>
Tuesday, July 22, 2008
Jason Galvin says:
OK, I figured out why my plugin tests we failing (details on my previous post, and sorry for the lack of formatting there).
I have a model named invite.rb in my app, and I think the tests were somehow reading off this model rather than the Invite model for the plugin. Anyhow, as soon as I deleted the invite.rb model in my app, the plugin tests all pass. HTH
Friday, August 08, 2008
John Ferin says:
I don't have time to hack it myself, but it would be great if you were able to support eager loading. Right now I get the error
"NoMethodError: undefined method `loaded' for #<Array:0xb6c5ecb4>", if I try to include my network table.
Thanks for your work.
Monday, September 28, 2009
John Woods says:
I concur with John Ferin. I get a similar error:
undefined method `loaded?’ for []:Array
/home/jwoods/NetBeansProjects/phenologdb/vendor/plugins/acts_as_network/lib/zetetic/acts/network.rb:161:in `send’ /home/jwoods/NetBeansProjects/phenologdb/vendor/plugins/acts_as_network/lib/zetetic/acts/network.rb:161:in `method_missing’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/association_preload.rb:228:in `preload_has_many_association’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/association_preload.rb:118:in `send’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/association_preload.rb:118:in `preload_one_association’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activesupport/lib/active_support/ordered_hash.rb:97:in `each’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activesupport/lib/active_support/ordered_hash.rb:97:in `each’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/association_preload.rb:112:in `preload_one_association’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/association_preload.rb:89:in `preload_associations’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/association_preload.rb:88:in `preload_associations’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/association_preload.rb:88:in `each’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/association_preload.rb:88:in `preload_associations’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/base.rb:1580:in `find_every’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/base.rb:1613:in `find_one’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/base.rb:1599:in `find_from_ids’ /home/jwoods/NetBeansProjects/phenologdb/vendor/rails/activerecord/lib/active_record/base.rb:648:in `find’ /home/jwoods/NetBeansProjects/phenologdb/app/controllers/phenotypes_controller.rb:57:in `show’