Separate Polymorphic Association table - ruby-on-rails

After reading the Ruby on Rails guides and a few of the stackoverflow responses to questions about polymorphic association I understand its use and implementation but I have a question about a specific use scenario. I have tags that can be associated with multiple topics, categories, images and other various models (which also have varying tags) but instead of placing the reference fields (foreign_id, foreign_type) within the tags table, I'd prefer to create a separate association table. Is this still possible using :polymorphic => true?
Something like this:
create_table :tags do |t|
t.string :name
t.remove_timestamps
end
create_table :object_tags, :id => false do |t|
t.integer :tag_id
t.references :tagable, :polymorphic => true
t.remove_timestamps
end
If this isn't possible, I was planning on creating the same :object_tags table and using :conditions within the Tag model and other models to force the associations. Is there a rails way of doing this? Thanks! (working with rails 3.0.9 & ruby 1.8.7 <- because deployment server is still using 1.8.7)
UPDATE:
Thanks Delba! Answer is a working solution for HABTM polymorphism.
class Tag < ActiveRecord::Base
has_many :labels
end
class Label < ActiveRecord::Base
belongs_to :taggable, :polymorphic => true
belongs_to :tag
end
class Topic < ActiveRecord::Base
has_many :labels, :as => :taggable
has_many :tags, :through => :labels
end
create_table :tags, :timestamps => false do |t|
t.string :name
end
create_table :labels, :timestamps => false, :id => false do |t|
t.integer :tag_id
t.references :taggable, :polymorphic => true
end
UPDATE: Because I need bi-directional HABTM, I ended up going back to creating individual tables.

Yes, and from your description you couldn't have the tagable columns on your tag anyhow since they can have multiple tagable things and vice versa . You mentioned HABT, but you can't do anything like has_and_belongs_to, :polymorphic => true as far as I know.
create_table :object_tags, :id => false do |t|
t.integer :tag_id
t.integer :tagable_id
t.string :tagable_type
end
Your other tables don't need any columns for object_tags, tags, or tagable.
class Tag < ActiveRecord::Base
has_many :object_tags
end
class ObjectTag < ActiveRecord::Base
belongs_to :tagable, :polymorphic => true
belongs_to :tag
end
class Topic < ActiveRecord::Base
has_many :object_tags, :as => :tagable
has_many :tags, :through => :object_tags
end

Related

Setting up a polymorphic association

I am trying to add a "following" like functionality to my site but I am having trouble finding the right way to use a polymorphic association. A user needs to be able to follow 3 different classes, these 3 classes do not follow the user back. I have created a user following user in the past but this is proving to be more difficult.
My Migration was
class CreateRelationships < ActiveRecord::Migration
def change
create_table :relationships do |t|
t.integer :follower_id
t.integer :relations_id
t.string :relations_type
t.timestamps
end
end
end
My Relationship model is
class Relationship < ActiveRecord::Base
attr_accessible :relations_id
belongs_to :relations, :polymorphic => true
has_many :followers, :class_name => "User"
end
In my User model
has_many :relationships, :foreign_key => "supporter_id", :dependent => :destroy
and in the other 3 models
has_many :relationships, :as => :relations
Am I missing something with setting up this association?
You basically have it right, except for a few minor errors:
attr_accessible :relations_id is redundant. Remove it from your Relationship model.
Both Relationship and User models call has_many to associate with each other. Relationship should call belongs_to because it contains the foreign key.
In your User model, set :foreign_key => "follower_id".
Here is how I would do it.
Have a Follow middle class with polymorphic association on the followable content side and has_many on the follower user side (user has many follows).
First, create a follows table:
class CreateFollows < ActiveRecord::Migration
def change
create_table :follows do |t|
t.integer :follower_id
t.references :followable, :polymorphic => true
t.timestamps
end
end
end
Replace Relationship model with a Follow model:
class Follow < ActiveRecord::Base
belongs_to :followable, :polymorphic => true
belongs_to :followers, :class_name => "User"
end
Include in User model:
has_many :follows, :foreign_key => :follower_id
Include in your three followable classes:
has_many :follows, :as => :followable
You can now do this:
TheContent.follows # => [Follow,...] # Useful for counting "N followers"
User.follows # => [Follow,...]
Follow.follower # => User
Follow.followable # => TheContent

Rails intermediate table assosciations

I have a User model and a Tag model. The User has Skills and Interests.
A Skill is a Tag, and an Interest is a Tag.
I have a table for Users, Tags, UsersSkills, UsersInterests. The last two being the intermediate table. How do I associate all this. The following is what I have but is not working. Thanks ahead of time.
#User model
class User < ActiveRecord::Base
has_and_belongs_to_many :skills
has_and_belongs_to_many :interests
end
#Tag model
class Tag < ActiveRecord::Base
has_and_belongs_to_many :users
end
#Migrations
create_table :users_interests, :id => false do |t|
t.references :user
t.references :tag
end
create_table :users_skills, :id => false do |t|
t.references :user
t.references :tag
end
SO here is the answer for anyone else experiencing this problem. The intermediate table had to have its name be alphabetically in order, even if that means readability goes down the tube. A join_table was then used. If this is not the right answer (it works but might not be good coding), please let me know.
class User < ActiveRecord::Base
has_and_belongs_to_many :skills, :class_name => "Tag", :join_table => "skills_users"
has_and_belongs_to_many :interests, :class_name => "Tag", :join_table => "interests_users"
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :users
end
create_table :skills_users, :id => false do |t|
t.references :user
t.references :tag
end
create_table :interests_users, :id => false do |t|
t.references :user
t.references :tag
end
It's expecting your join tables to have skill_id and interest_id FK's rather than tag_id.
I believe you're looking for (don't have a terminal handy):
class User < ActiveRecord::Base
has_and_belongs_to_many :skills, :association_foreign_key => :tag_id
has_and_belongs_to_many :interests, :association_foreign_key => :tag_id
end

has_many :through with :primary_key on join table not working

In my Rails 3 project, I have a user model with a self referential join, through the follow model. I want to use this join table to find activity related to the followed user. I have almost everything set up correctly, except that the query generated by the join is totally ignoring the :primary_key option on the join model.
Here is the relevant schema for the relevant models:
create_table "users", :force => true do |t|
t.string "email", :default => "", :null => false
t.string "first_name"
t.string "last_name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "follows", :force => true do |t|
t.integer "user_id"
t.integer "followed_user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "activities", :force => true do |t|
t.integer "user_id"
t.text "body"
t.datetime "created_at"
t.datetime "updated_at"
end
Here's the associations in the models
class User < ActiveRecord::Base
has_many :follows
has_many :followed_users, :through => :follows
has_many :followed_activities, :through => :follows
has_many :activities
end
class Follow < ActiveRecord::Base
belongs_to :user
belongs_to :followed_user, :class_name => "User"
has_many :followed_activities, :primary_key => :followed_user, :foreign_key => :user_id, :class_name => "Activity"
end
The following work just fine:
u = User.first
u.follows # returns corresponding records from the follows table
u.followed_users # returns all users that u is following
u.followed_users.first.activities # returns all activity records corresponding to the first person the user is following
Follow.first.activities # same as the previous
However, the following just returns an empty array:
u.followed_activities
Here is the sql that is generated from the last statement:
Activity Load (0.2ms) SELECT `activities`.* FROM `activities` INNER JOIN `follows` ON `activities`.user_id = `follows`.id WHERE ((`follows`.user_id = 1))
The reason it isn't working is because it is trying to join use 'follows'.id as the primary key rather than 'follows'.followed_user.
Is this a bug, or do I have to repeat the :primary_key declaration somewhere on the user model? I can't find any mention anywhere in the Rails api, or anywhere else online.
Rails Version: 3.0.7
I've found it intuitive to daisy chain relationships with the 'nested_has_many_through' gem, http://rubygems.org/gems/nested_has_many_through which will be a standard part of rails 3.1 and could give you another tool to tackle the issue here
It will let you do something like this:
class Author < User
has_many :posts
has_many :categories, :through => :posts, :uniq => true
has_many :similar_posts, :through => :categories, :source => :posts
has_many :similar_authors, :through => :similar_posts, :source => :author, :uniq => true
has_many :posts_of_similar_authors, :through => :similar_authors, :source => :posts, :uniq => true
has_many :commenters, :through => :posts, :uniq => true
end
class Post < ActiveRecord::Base
belongs_to :author
belongs_to :category
has_many :comments
has_many :commenters, :through => :comments, :source => :user, :uniq => true
end
This has super-simplified my queries and collections. I hope you find an answer to your problem, it's a tough one!
Justin, you have 2 associations called "followed_activities". sure, they have different context (different models), but I'd like to ask you to try method inside the association block like this:
has_many :followed_users, :through => :follows do
def activities
end
end

Polymorphic association using namespaced classes

I am using Ruby on Rails 3 and I would like to set a polymorphic association using namespaced classes.
Migrations are:
create_table :users_users do |t|
t.integer :id
t.string :full_name
t.references :userable, :polymorphic => true
end
create_table :users_profiles do |t|
t.integer :id
...
end
create_table :users_accounts do |t|
t.integer :id
...
end
Classes are:
class Users::User < ActiveRecord::Base
# Association ...
end
class Users::Profile < ActiveRecord::Base
# Association ...
end
class Users::Account < ActiveRecord::Base
# Association ...
end
How I must write code associations for above classes (using :class_name => "Users:User", ...?) in order to auto-create and auto-destroy associated model records, "mapping" those in the users_users table and viceversa?
Do you have some advice about that? What string values I will have in userable_type attributes (example: 'Users::Profile', 'Profile', ...)?
To setup the associations, you don't need to use class name...
class Users::User < ActiveRecord::Base
belongs_to :userable, :polymorphic => true
end
class Users::Profile < ActiveRecord::Base
has_one :user, :as => :userable, :dependent => :destroy
end
class Users::Account < ActiveRecord::Base
has_one :user, :as => :userable, :dependent => :destroy
end
:dependent => :destroy will deal with deleting them when the Users::User is destroyed, but in terms of creating, you've got the same options as you have with normal relationships. If you're doing it from a form, it's best to use nested attributes.
In the database, the userable_type column would include the namespace. So it'd be 'Users::Account' or 'Users::Profile'.

has_many :through a has_and_belongs_to_many association

I am trying to do the following in a Ruby on Rails project:
class FoodItem < ActiveRecord::Base
has_and_belongs_to_many :food_categories
has_many :places, :through => :food_categories
end
class FoodCategory < ActiveRecord::Base
has_and_belongs_to_many :food_items
belongs_to :place
end
class Place < ActiveRecord::Base
has_many :food_categories
has_many :food_items, :through => :food_category
end
But calling the instance method some_food_item.places gives me the following error:
ActiveRecord::StatementInvalid: PGError: ERROR: column
food_categories.food_item_id does not exist
LINE 1: ...laces".id = "food_categories".place_id WHERE (("food_cate...
: SELECT "places".* FROM "places" INNER JOIN "food_categories" ON "places".id = "food_categories".place_id WHERE (("food_categories".food_item_id = 1))
Which makes perfect sense - because of the HABTMs on FoodItem and FoodCategory I have the mapping table named food_categories_food_items.
What do I have to do to get some_food_item.places to look places up correctly through the mapping table instead of looking for a food_item_id in the food_categories table?
My first version of the answer was incorrect, but this one works perfectly. I made a couple of typos the first time (the hazard of not actually creating an app to test) but this time I verified. And a plugin is needed, but this is easy. first, install the plugin:
script/plugin install git://github.com/ianwhite/nested_has_many_through.git
This installs Ian White's workaround, and it works seamlessly. Now the models, copied directly from the test app I setup to get this working:
class FoodItem < ActiveRecord::Base
has_many :food_category_items
has_many :food_categories, :through => :food_category_items
has_many :places, :through => :food_categories
end
class FoodCategory < ActiveRecord::Base
has_many :food_category_items
has_many :food_items, :through => :food_category_items
belongs_to :place
end
class FoodCategoryItem < ActiveRecord::Base
belongs_to :food_item
belongs_to :food_category
end
class Place < ActiveRecord::Base
has_many :food_categories
has_many :food_category_items, :through => :food_categories
has_many :food_items, :through => :food_category_items
end
Now "far" associations work just as well. place_instance.food_items and food_item.places both work flawlessly, as well as the simpler associations involved. Just for reference, here's my schema to show where all the foreign keys go:
create_table "food_categories", :force => true do |t|
t.string "name"
t.integer "place_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "food_category_items", :force => true do |t|
t.string "name"
t.integer "food_item_id"
t.integer "food_category_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "food_items", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "places", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
Hope this helps!
UPDATE: This question has come up a few times recently. I wrote an article, nesting your has_many :through relationships, to explain in detail. It even has an accompanying example application on GitHub to download and play around with.
A few months ago I wrote an article about this. In short, has_many through a has_and_belongs_to_many association is not allowed by Rails. However, you can partly simulate the relationship by doing something like this:
class FoodItem < ActiveRecord::Base
has_and_belongs_to_many :food_categories
named_scope :in_place, lambda{ |place|
{
:joins => :food_categories,
:conditions => {:food_categories => {:id => place.food_category_ids}},
:select => "DISTINCT `food_items`.*" # kill duplicates
}
}
end
class FoodCategory < ActiveRecord::Base
has_and_belongs_to_many :food_items
belongs_to :place
end
class Place
has_many :food_categories
def food_items
FoodItem.in_place(self)
end
end
This will give you the some_food_item.places method you seek.
I'm using Rails 3.2.13 and Rasmus, your original setup now seems to work fine on a HABTM.
I'd suggest users try first before attempting a workaround.
This is correct, because you can't peform "has many through" on a join table. In essence, you're trying to extend the relationship one degree further than you really can. HABTM (has_and_belongs_to_many) is not a very robust solution to most problems.
In your case, I'd recommend adding a model called FoodCategoryItem, and renaming your join table to match. You'll also need to add back the primary key field. Then setup your model associations like this:
class FoodItem < ActiveRecord::Base
has_many :food_categories, :through => :food_category_items
has_many :places, :through => :food_categories
end
class FoodCategory < ActiveRecord::Base
has_many :food_items, :through => :food_category_items
belongs_to :place
end
class FoodCategoryItems < ActiveRecord::Base
belongs_to :food_item
belongs_to :food_category
end
class Place < ActiveRecord::Base
has_many :food_categories
has_many :food_items, :through => :food_categories
end
Note, I also fixed a typo in "Place -> has_many :food_items". This should take care of what you need, and give you the added bonus of being able to add functionality to your FoodCategoryItems "join" model in the future.

Resources