Ruby Forum Ruby on Rails > Polymorphic, many-to-many, self-referential data model

Posted by Jason King (Guest)
on 20.05.2006 12:50
(Received via mailing list)
No matter what I try I've been unable to work how to get this to work 
with
the cool ActiveRecord helpers.  I'm not sure if it's possible, Josh 
Susser's
blog suggests it is, but damned if I can work it out...

I have:

Container which can contain one or more Element.  An Element is a 
polymorph
of either a Container or a Chunk.  An Element can exist in one or more
Containers.  I use a table called ownerships as the join between 
Containers
and Element.  Here's some code:

  create_table :containers do |t|
    t.column :name, :string
  end

  create_table :chunks do |t|
    t.column :name, :string
  end

  create_table :ownerships do |t|
    t.column :name, :string
    t.column :container_id, :integer
    t.column :element_id, :integer
    t.column :element_type, :string
  end

Then in the models:

  class Chunk < ActiveRecord::Base
    has_many :ownerships, :as => :element
  end

  class Ownership < ActiveRecord::Base
    belongs_to :element, :polymorphic => true
    # belongs_to :container
  end

  class Container < ActiveRecord::Base
    has_many :ownerships, :as => :element
    # has_many :ownerships
  end

Ok, so far so good.  The above works as expected.  What is commented out 
is
my attempt at coding the important relationship between the Container 
and the
Ownership, not as a Element but as the parent (ie. referenced by
container_id).  I've tried a number of options, including all sort of
attempts with :through and various other things.

I understand why it doesn't work, obviously the relationship defined 
already
for Container's access to ownerships gets in the way, or takes 
precedence or
whatever.  It doesn't matter what I've tried, the result sets are always
giving me the "Element" relationship of Container to Ownership, not the
direct parental relationship.

Any clues?
Posted by Josh Susser (jsusser)
on 20.05.2006 19:28
Jason King wrote:
> No matter what I try I've been unable to work how to get this to work with
> the cool ActiveRecord helpers.  I'm not sure if it's possible, Josh Susser's
> blog suggests it is, but damned if I can work it out...

Interesting combination of polymorpic and self-ref with through. I think 
this should do it...

-----
create_table :containers do |t|
  t.column :name, :string
end

create_table :chunks do |t|
  t.column :name, :string
end

create_table :ownerships do |t|
  t.column :name, :string
  t.column :owner_id, :integer
  t.column :element_id, :integer
  t.column :element_type, :string
end

class Ownership < ActiveRecord::Base
  # to owner
  belongs_to :owner,  :class_name => "Container",
                      :foreign_key => "owner_id"
  # to elements
  belongs_to :element,    :polymorphic => true
  belongs_to :container,  :class_name => "Container",
                          :foreign_key => "element_id"
  belongs_to :chunk,      :class_name => "Chunk",
                          :foreign_key => "element_id"
end

class Container < ActiveRecord::Base
  # to owner
  has_many :ownings,  :class_name => "Ownership", :as => :element
  has_many :owners,   :through => :ownings
  # to elements
  has_many :ownerships
  has_many :containers, :through => :ownerships, :source => :article,
                        :conditions => "ownerships.element_type = 
'Container'"
  has_many :chunks,     :through => :ownerships, :source => :book,
                        :conditions => "ownerships.element_type = 
'Chunk'"
end

class Chunk < ActiveRecord::Base
  # to owner
  has_many :ownings,  :class_name => "Ownership", :as => :element
  has_many :owners,   :through => :ownings
end
-----

I haven't tested this out, but I'm pretty sure it should do what you 
want, as it's just a combination of the self-ref and polymorphic through 
patterns. The trick is that you have to disambiguate the two different 
associations to containers - one way is as your container, and the other 
is as an element. That means you need different names for those 
relationship. I chose "owner" to indicate an element's container from 
the element's perspective. You also have to use a different name for the 
two kinds of ownerships, containing and contained.

Give it a shot and let me know if it works.  This would be good for a 
writeup.

--
Josh Susser
http://blog.hasmanythrough.com
Posted by Jason King (Guest)
on 21.05.2006 01:11
(Received via mailing list)
Josh, you wrote,
>Jason King wrote:
>> No matter what I try I've been unable to work how to get this to work with
>> the cool ActiveRecord helpers.  I'm not sure if it's possible, Josh Susser's
>> blog suggests it is, but damned if I can work it out...
>
>Interesting combination of polymorpic and self-ref with through. I think 
>this should do it...

Wasn't quite right, apart from the :source => :article and :source => 
:book
in the container, there were some other small things which brought 
things to
a halt.  Basically in the "to elements" stuff in the Container the lack 
of
the foreign key in the "has_many :ownerships" (which I've renamed) meant 
that
the JOIN was wrong.

I ended up coding things like this (with some slight name changes to 
make
more sense, and some work on the "to elements" stuff in Container).  I'm
still not sure if this is the best way to do it, but it seems to work, 
and
*does* actually make sense to me :)

  class Container < ActiveRecord::Base
    # to owner
    has_many :ownerships, :as => :element
    has_many :owners, :through => :ownerships

    # to elements
    has_many :ownings, :class_name => "Ownership", :foreign_key => 
"owner_id"
    has_many :containers,
             :through => :ownings,
             :conditions => "ownerships.element_type = 'Container'"
    has_many :chunks,
             :through => :ownings,
             :conditions => "ownerships.element_type = 'Chunk'"
  end

Chunk, you can probably guess from the above (just a name change from 
your
":ownings" to my ":ownerships" which made more sense to me.  The all
important (thank you, thank you, thank you) Ownership class remained the
same.

You truly are a legend, I knew this was complex, and throught that it 
might
have been outside the realm of these helpers.  Thanks for taking the 
time to
code up some very helpful code.
Posted by Josh Susser (jsusser)
on 21.05.2006 01:26
Jason King wrote:
> Wasn't quite right, apart from the :source => :article and :source => :book
> in the container, there were some other small things which brought 
> things to
> a halt.  Basically in the "to elements" stuff in the Container the lack of
> the foreign key in the "has_many :ownerships" (which I've renamed) meant 
> that the JOIN was wrong.

Ah yes, copy/paste is ever the bane of my postings. Cool that you got it 
sorted out and that it's working for you.

> I ended up coding things like this (with some slight name changes to make
> more sense, and some work on the "to elements" stuff in Container).  I'm
> still not sure if this is the best way to do it, but it seems to work, 
> and *does* actually make sense to me :)

If it works and makes sense to you, then it's the right way :-)  I like 
the way you changed the names of ownings/ownerships, I agree it reads 
better. And I always like to look at the SQL in the log to make sure 
things are getting put together right, but it sounds like you do that 
too so I expect you're OK.

> You truly are a legend, I knew this was complex, and throught that it 
> might have been outside the realm of these helpers.  Thanks for taking the 
> time to code up some very helpful code.

Glad to be of service. I love how helpful everyone is in this community, 
just trying to do my part.

--
Josh Susser
http://blog.hasmanythrough.com
Posted by Mariano Belinky (Guest)
on 19.03.2007 19:40
Hi,
I am new to RoR, and found this example incredibly helpful. It took me a 
while to adapt it to my needs, but once I did it worked perfectly.
Now, how would I go about creating the controller for it, ie 
add/delete/update elements (either containers or chunks) to a given 
container? I tried a scaffold on Container but that didn't work. Can 
this be done "automagically", or do I need to code it by hand?

many thanks,

mariano
Posted by Evan Weaver (Guest)
on 19.03.2007 21:38
has_many_polymorphs does all this automatically.

http://blog.evanweaver.com/pages/has_many_polymorphs

Evan Weaver
Posted by Mariano Belinky (Guest)
on 20.03.2007 14:08
Evan Weaver wrote:
> has_many_polymorphs does all this automatically.
> 
> http://blog.evanweaver.com/pages/has_many_polymorphs
> 
> Evan Weaver

Hi Evan, thanks for your reply. I'm sorry for being thick, but which
part is automatic? the self-referential polymorphism or the scaffolding
/ controller. I looked at the link you posted, and it seems it does
indeed to the polymorphism, but I'm not sure about the controller part.

thanks again,

Mariano
Posted by Evan Weaver (eweaver)
on 25.03.2007 07:39
It does not do the controller. When I posted I didn't realize you were 
reanimating a year-old thread.

However, I am working on adding some controller examples for tagging 
which might help you. Check in a few weeks.

Evan Weaver