This is available in Rails since 1.1
So you want to use the new polymorphic associations to allow many models to be related to one another in some way. Here’s how you do it.
Lets assume you’re trying to mimic a file system with your database. You have Folders and you want to associate Images and TextFiles with folders.
Step 1: Make your Folder, TextFile, and Image models.
Step 2: We would normally need to make a has_and_belongs_to_many (habtm) table for each type of object to folder. Since we are using polymorphic associations however, we’re going to put all of these relationships into a single habtm table called “linkings”.
CREATE TABLE linkings (
id INTEGER AUTO_INCREMENT,
folder_id INTEGER,
linkable_id INTEGER,
linkable_type VARCHAR(20),
PRIMARY KEY(id)
)
You’ll notice that linkable_id is what a folder has and belongs to many of. In order for this one table to keep all of our object types we put in a linkable_type column to hold the type of object that the row relates to.
A sample row in this linkings table would look like: [1, 1, 3, ‘Image’], or [2, 1, 4, ‘TextFile’]. You can see how this allows us to associate any model we’d want with a folder.
Step 3: Create the Linking model.
class Linking < ActiveRecord::Base
belongs_to :folder
belongs_to :linkable, :polymorphic => true
end
You’ll see that we told linkings that it has a foreign key to folder (folder_id).
The polymorphic option tells the model that there is a foreign key to ‘linkable’ (linkable_id), and that it is polymorphic so that each ‘linkable’ class is stored in the linkable_type column.
There is actually no Linkable model, it is virtual in the sense that models are linkable if they have linkings. We are using the term linkable to imply a certain type of behavior for our models but the choice of words is arbitrary.
Step 4: Modify the Folder model.
class Folder < ActiveRecord::Base
has_many :linkings
end
Step 5: Modify your Image and TextFile models.
class Image < ActiveRecord::Base
has_many :linkings, :as => :linkable, :dependent => :destroy
has_many :folders, :through => :linkings
end
class TextFile < ActiveRecord::Base
has_many :linkings, :as => :linkable, :dependent => :destroy
has_many :folders, :through => :linkings
end
The as option in the first has_many of each model tells us that we have a foreign key in the linkings table, but that it is “linkable_id” instead of “image_id” or “text_file_id” as it would normally be.
The dependent option tells the model to destroy all the associated linkings when this object is destroyed so that no linkings are left orphaned. Note that :dependent => true has been deprecated and replaced with :dependent => :destroy, and that it can’t be set if :exclusively _dependent is also set (see has_many docs for more info).
The through option in the second has_many of each model tells the model that each Image and TextFile has a collection of folders associated with it that is accessable (joinable) through the Linking model. See [ThroughAssociations] for more about :through
Step 6: Supporting functions
To add linkables (Images or TextFiles) to a folder is not ‘beautiful’ at this point, so we’re going to add two methods to the Folder model that allow us to do things a little better.
class Folder < ActiveRecord::Base
has_many :linkings
def files
linkings.collect {|linking| linking.linkable}
end
def link(linkable)
return false if !linkable.new_record? && files.include?(linkable)
linkings.create :linkable => linkable
return true
end
end
The files method allows us get all of the files through our linkings table. This is where the real beauty of the polymorphic associations come in. The array it returns will actually have the linkables cast to the appropriate model class.
The link method allows us to easily add linkables to a folder by first creating the linkable and then having this method create the linking for us. If the linkable already exists in this folder the method returns false since we don’t want a file to be included in the same folder multiple times.
That’s all there is to it. This is only a basic treatment but it will allow you to add linkables to folders and to get a list of objects in any given folder as well as a list of folders an Image or TextFile is a member of.
A few other good uses of this are for things like taggings (taggable), groupings (groupable), befriendings (befriendable), etc. Use your imagination.
Happy modeling!linkings.create :linkable => linkable
return true
linkable.valid? ? resourcelinks.create(:resourceable => resourceable) : false
Given a Folder as described above, how can I get the names of files in it (assuming each files table has a “name” column)?
Doing this:
<% folder.linkings.each do |item| %>
Item:
<%= item.name %>
<br />
<% end %>
Gives a no method error, since the linkings table doesn’t have names. Why doesn’t Rails see :through the linkings into the Images and TextFiles?
Using the files method above instead of linkings gives this error: ””image” is not a valid constant name!”
This will work, but is ugly:
<% if item.linkable_type == "image" %>
<%= Image.find( item.linkable_id ).name %>
<% elsif item.linkable_type == "text_file" %>
<%= TextFile.find( item.linkable_id ).name %>
<% end %>
JCubed
h2. Answer
You can reference the file this way.
<% folder.files.each do |file| %>
Item:
<%= file.name %>
<% end %>
If you need to know the class of “file” use
<% folder.files.each do |file| %>
Item:
<%= file.name %>
Item Type:
<%= file.class %>
<% end %>
Like I said, the files method doesn’t work for me.
There’s either something wrong with this code or my install of 1.1:
def files
linkings.collect {|linking| linking.linkable}
end
Because this is the error I get whenever I try to use that method:
"image" is not a valid constant name!
I tried it out with Rails 1.1.0 and everything checks out. In console, I get
>> Folder.find(1).files.first.class
=> Image
as expected.
The linkable_type field should contain the class name “Image”, not “image”, “TextFile”, not “text_file”.
class Foo < ActiveRecord::Base
set_table_name "foo"
end
class FooSub1 < Foo
set_table_name "foo"
end
class FooSub2 < Foo
set_table_name "foo"
end
class Bar < ActiveRecord::Base
belongs_to :foo
end
bar = Bar.find(1)
bar.foo
==>
nil
The SQL generated by bar.foo generates something like:
SELECT * FROM foo where (id = 123) AND (type = 'Foo')
Which completely filters out FooSub1 and FooSub2 objects.
SELECT * FROM foo where (id = 123)
would be more correct. Or:
SELECT * FROM foo where (id = 123) AND (type = 'Foo' OR type = 'FooSub1' OR type = 'FooSub2')
if you’re “not into the whole brevity thing”.
Having to store the Foo type in foo.type and in bar.foo_type seems redundant for an abstract STI base class (Foo).
Rails 1.0 seemed to do the right thing.
KurtStephens Does sticking “model :foo_sub1, :foo_sub2” in your controller make any difference?—ChristofferSawicki
Almost alternate approach…Seems like you should be able to do this without the reifing the join table as a model.
I tried to use something like the following in my “owning” objects. The case below is from ReverbNation, a music site, where multiple page type objects (e.g., a Venue), can have a special “sharing” relation to a playlist.
has_and_belongs_to_many :playlists_shared, :class_name => "Playlist", :join_table => "shared_playlists", :foreign_key => "on_page_id", :conditions => ['on_page_type =?','Venue'], :order=> 'position'
Then when you add use the following instead of playlists_shared << playlist
playlists_shared.push_with_attributes(playlist,{:on_page_type => self.class.to_s.downcase})
However, I got stuck when I got to the delete. I think I would have had to use sql. When I left it, it would delete all the join table rows with the given from and to ids without taking into account the polymorphic type.
ManytoManyPolymorphicAssociations
How to use in a form?*
Every single polymorphic howto I find only shows the models, how about the controllers and views? How do you know what partial to load in a form? How do you prepare the instance variables for a form?
This is available in Rails since 1.1
So you want to use the new polymorphic associations to allow many models to be related to one another in some way. Here’s how you do it.
Lets assume you’re trying to mimic a file system with your database. You have Folders and you want to associate Images and TextFiles with folders.
Step 1: Make your Folder, TextFile, and Image models.
Step 2: We would normally need to make a has_and_belongs_to_many (habtm) table for each type of object to folder. Since we are using polymorphic associations however, we’re going to put all of these relationships into a single habtm table called “linkings”.
CREATE TABLE linkings (
id INTEGER AUTO_INCREMENT,
folder_id INTEGER,
linkable_id INTEGER,
linkable_type VARCHAR(20),
PRIMARY KEY(id)
)
You’ll notice that linkable_id is what a folder has and belongs to many of. In order for this one table to keep all of our object types we put in a linkable_type column to hold the type of object that the row relates to.
A sample row in this linkings table would look like: [1, 1, 3, ‘Image’], or [2, 1, 4, ‘TextFile’]. You can see how this allows us to associate any model we’d want with a folder.
Step 3: Create the Linking model.
class Linking < ActiveRecord::Base
belongs_to :folder
belongs_to :linkable, :polymorphic => true
end
You’ll see that we told linkings that it has a foreign key to folder (folder_id).
The polymorphic option tells the model that there is a foreign key to ‘linkable’ (linkable_id), and that it is polymorphic so that each ‘linkable’ class is stored in the linkable_type column.
There is actually no Linkable model, it is virtual in the sense that models are linkable if they have linkings. We are using the term linkable to imply a certain type of behavior for our models but the choice of words is arbitrary.
Step 4: Modify the Folder model.
class Folder < ActiveRecord::Base
has_many :linkings
end
Step 5: Modify your Image and TextFile models.
class Image < ActiveRecord::Base
has_many :linkings, :as => :linkable, :dependent => :destroy
has_many :folders, :through => :linkings
end
class TextFile < ActiveRecord::Base
has_many :linkings, :as => :linkable, :dependent => :destroy
has_many :folders, :through => :linkings
end
The as option in the first has_many of each model tells us that we have a foreign key in the linkings table, but that it is “linkable_id” instead of “image_id” or “text_file_id” as it would normally be.
The dependent option tells the model to destroy all the associated linkings when this object is destroyed so that no linkings are left orphaned. Note that :dependent => true has been deprecated and replaced with :dependent => :destroy, and that it can’t be set if :exclusively _dependent is also set (see has_many docs for more info).
The through option in the second has_many of each model tells the model that each Image and TextFile has a collection of folders associated with it that is accessable (joinable) through the Linking model. See [ThroughAssociations] for more about :through
Step 6: Supporting functions
To add linkables (Images or TextFiles) to a folder is not ‘beautiful’ at this point, so we’re going to add two methods to the Folder model that allow us to do things a little better.
class Folder < ActiveRecord::Base
has_many :linkings
def files
linkings.collect {|linking| linking.linkable}
end
def link(linkable)
return false if !linkable.new_record? && files.include?(linkable)
linkings.create :linkable => linkable
return true
end
end
The files method allows us get all of the files through our linkings table. This is where the real beauty of the polymorphic associations come in. The array it returns will actually have the linkables cast to the appropriate model class.
The link method allows us to easily add linkables to a folder by first creating the linkable and then having this method create the linking for us. If the linkable already exists in this folder the method returns false since we don’t want a file to be included in the same folder multiple times.
That’s all there is to it. This is only a basic treatment but it will allow you to add linkables to folders and to get a list of objects in any given folder as well as a list of folders an Image or TextFile is a member of.
A few other good uses of this are for things like taggings (taggable), groupings (groupable), befriendings (befriendable), etc. Use your imagination.
Happy modeling!linkings.create :linkable => linkable
return true
linkable.valid? ? resourcelinks.create(:resourceable => resourceable) : false
Given a Folder as described above, how can I get the names of files in it (assuming each files table has a “name” column)?
Doing this:
<% folder.linkings.each do |item| %>
Item:
<%= item.name %>
<br />
<% end %>
Gives a no method error, since the linkings table doesn’t have names. Why doesn’t Rails see :through the linkings into the Images and TextFiles?
Using the files method above instead of linkings gives this error: ””image” is not a valid constant name!”
This will work, but is ugly:
<% if item.linkable_type == "image" %>
<%= Image.find( item.linkable_id ).name %>
<% elsif item.linkable_type == "text_file" %>
<%= TextFile.find( item.linkable_id ).name %>
<% end %>
JCubed
h2. Answer
You can reference the file this way.
<% folder.files.each do |file| %>
Item:
<%= file.name %>
<% end %>
If you need to know the class of “file” use
<% folder.files.each do |file| %>
Item:
<%= file.name %>
Item Type:
<%= file.class %>
<% end %>
Like I said, the files method doesn’t work for me.
There’s either something wrong with this code or my install of 1.1:
def files
linkings.collect {|linking| linking.linkable}
end
Because this is the error I get whenever I try to use that method:
"image" is not a valid constant name!
I tried it out with Rails 1.1.0 and everything checks out. In console, I get
>> Folder.find(1).files.first.class
=> Image
as expected.
The linkable_type field should contain the class name “Image”, not “image”, “TextFile”, not “text_file”.
class Foo < ActiveRecord::Base
set_table_name "foo"
end
class FooSub1 < Foo
set_table_name "foo"
end
class FooSub2 < Foo
set_table_name "foo"
end
class Bar < ActiveRecord::Base
belongs_to :foo
end
bar = Bar.find(1)
bar.foo
==>
nil
The SQL generated by bar.foo generates something like:
SELECT * FROM foo where (id = 123) AND (type = 'Foo')
Which completely filters out FooSub1 and FooSub2 objects.
SELECT * FROM foo where (id = 123)
would be more correct. Or:
SELECT * FROM foo where (id = 123) AND (type = 'Foo' OR type = 'FooSub1' OR type = 'FooSub2')
if you’re “not into the whole brevity thing”.
Having to store the Foo type in foo.type and in bar.foo_type seems redundant for an abstract STI base class (Foo).
Rails 1.0 seemed to do the right thing.
KurtStephens Does sticking “model :foo_sub1, :foo_sub2” in your controller make any difference?—ChristofferSawicki
Almost alternate approach…Seems like you should be able to do this without the reifing the join table as a model.
I tried to use something like the following in my “owning” objects. The case below is from ReverbNation, a music site, where multiple page type objects (e.g., a Venue), can have a special “sharing” relation to a playlist.
has_and_belongs_to_many :playlists_shared, :class_name => "Playlist", :join_table => "shared_playlists", :foreign_key => "on_page_id", :conditions => ['on_page_type =?','Venue'], :order=> 'position'
Then when you add use the following instead of playlists_shared << playlist
playlists_shared.push_with_attributes(playlist,{:on_page_type => self.class.to_s.downcase})
However, I got stuck when I got to the delete. I think I would have had to use sql. When I left it, it would delete all the join table rows with the given from and to ids without taking into account the polymorphic type.
ManytoManyPolymorphicAssociations
How to use in a form?*
Every single polymorphic howto I find only shows the models, how about the controllers and views? How do you know what partial to load in a form? How do you prepare the instance variables for a form?