Polymorphic, many-to-many, self-referential data model


#1

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?


#2

Jason K. 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 S.'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 S.
http://blog.hasmanythrough.com


#3

Josh, you wrote,

Jason K. 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 S.'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 :slight_smile:

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.


#4

Jason K. 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 :slight_smile:

If it works and makes sense to you, then it’s the right way :slight_smile: 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 S.
http://blog.hasmanythrough.com


#5

has_many_polymorphs does all this automatically.

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

Evan W.


#6

Evan W. wrote:

has_many_polymorphs does all this automatically.

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

Evan W.

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


#7

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 W.


#8

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