Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practices for multiple associations with the same class in Rails?

I think my question is best described as an example. Let's say I have a simple model called "Thing" and it has a few attributes that are simple data types. Something like...

Thing
   - foo:string
   - goo:string
   - bar:int

That isn't hard. The db table will contain three columns with those three attributes and I can access them with something like @thing.foo or @thing.bar.

But the problem I'm trying to solve is what happens when "foo" or "goo" can no longer be contained in a simple data type? Assume that foo and goo represent the same type of object. That is, they are both instances of "Whazit" just with different data. So now Thing might look like this...

Thing
  - bar:int

But now there is a new model called "Whazit" that looks like this...

Whazit
  - content:string
  - value:int
  - thing_id:int

So far this is all good. Now here is where I'm stuck. If I have @thing, how can I set it up to refer to my 2 instances of Whazit by name (For the record, the "business rule" is that any Thing will always have exactly 2 Whazits)? That is, I need to know if the Whazit I have is basically foo or goo. Obviously, I can't do @thing.foo in the current setup, but I'd that is ideal.

My initial thought is to add a "name" attribute to Whazit so I can get the Whatzits associated with my @thing and then choose the Whazit I want by name that way. That seems ugly though.

Is there a better way?

like image 643
CJ F Avatar asked Apr 16 '09 18:04

CJ F


People also ask

What is a polymorphic association in Rails?

Polymorphic relationship in Rails refers to a type of Active Record association. This concept is used to attach a model to another model that can be of a different type by only having to define one association.


2 Answers

There are a couple of ways you could do this. First, you could set up two belongs_to/has_one relationships:

things
  - bar:int
  - foo_id:int
  - goo_id:int

whazits
  - content:string
  - value:int

class Thing < ActiveRecord::Base
  belongs_to :foo, :class_name => "whazit"
  belongs_to :goo, :class_name => "whazit"
end

class Whazit < ActiveRecord::Base
  has_one :foo_owner, class_name => "thing", foreign_key => "foo_id"
  has_one :goo_owner, class_name => "thing", foreign_key => "goo_id"

  # Perhaps some before save logic to make sure that either foo_owner
  # or goo_owner are non-nil, but not both.
end

Another option which is a little cleaner, but also more of a pain when dealing with plugins, etc., is single-table inheritance. In this case you have two classes, Foo and Goo, but they're both kept in the whazits table with a type column that distinguishes them.

things
  - bar:int

whazits
  - content:string
  - value:int
  - thing_id:int
  - type:string

class Thing < ActiveRecord::Base
  belongs_to :foo
  belongs_to :goo
end

class Whazit < ActiveRecord::Base
  # .. whatever methods they have in common ..
end

class Foo < Whazit
  has_one :thing
end

class Goo < Whazit
  has_one :thing
end

In both cases you can do things like @thing.foo and @thing.goo. With the first method, you'd need to do things like:

@thing.foo = Whazit.new

whereas with the second method you can do things like:

@thing.foo = Foo.new

STI has its own set of problems, though, especially if you're using older plugins and gems. Usually it's an issue with the code calling @object.class when what they really want is @object.base_class. It's easy enough to patch when necessary.

like image 69
Sarah Mei Avatar answered Sep 27 '22 21:09

Sarah Mei


Your simple solution with adding a "name" doesn't need to be ugly:

class Thing < ActiveRecord::Base
  has_one :foo, :class_name => "whazit", :conditions => { :name => "foo" }
  has_one :goo, :class_name => "whazit", :conditions => { :name => "goo" }
end

In fact, it's quite similar to how STI works, except you don't need a separate class.

The only thing you'll need to watch out for is setting this name when you associate a whazit. That can be as simple as:

def foo=(assoc)
  assos.name = 'foo'
  super(assoc)
end
like image 21
Andrew Vit Avatar answered Sep 27 '22 21:09

Andrew Vit