Blog

Redundant Bidirectional Relationships in Rails Suck

Posted: Apr 10, 2008 by Stephen Lombardo Tagged rails

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 &lt; ActiveRecord::Base
    acts_as_network :network, :through => :connections, :conditions => "state = 'accepted'"
    end

    class Connection &lt; 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’

Add a comment