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

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
4b3e6fca361338eefbf5e60432a401c1?d=identicon&s=25 Jason King (Guest)
on 2006-05-20 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?
9f0f89bbd9e1ecfbaab6584e429b7a2f?d=identicon&s=25 Josh Susser (jsusser)
on 2006-05-20 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
4b3e6fca361338eefbf5e60432a401c1?d=identicon&s=25 Jason King (Guest)
on 2006-05-21 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.
9f0f89bbd9e1ecfbaab6584e429b7a2f?d=identicon&s=25 Josh Susser (jsusser)
on 2006-05-21 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
F015537a6c8741e4110e4af12db524e0?d=identicon&s=25 Mariano Belinky (Guest)
on 2007-03-19 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
Dcad03359b242dce6151245a9f798ffa?d=identicon&s=25 Evan Weaver (Guest)
on 2007-03-19 21:38
has_many_polymorphs does all this automatically.

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

Evan Weaver
F015537a6c8741e4110e4af12db524e0?d=identicon&s=25 Mariano Belinky (Guest)
on 2007-03-20 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
F8634aca904bc63cb047cb1bd93bdc74?d=identicon&s=25 Evan Weaver (eweaver)
on 2007-03-25 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
This topic is locked and can not be replied to.