Setting Up a Polymorphic Association

Setting up a polymorphic 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

How to setup a polymorphic association of People and Users?

I agree with moo-juice, the correct approach would be people + roles. here's a concrete example:

class Person < ActiveRecord::Base
has_many :roles, :through => :roles_people
#columns would be username, password, etc
end

class Role < ActiveRecord::Base
has_many :people, :through > :roles_people
#a column of role_type would be required here and the values of such would be Customer, User, etc. in this class you will put logic that is needed to execute the functions of the role
end

class RolesPerson < ActiveRecord::Base
belongs_to :person
belongs_to :role
#this is a join model for a many-to-many relationship. you can store info in here about when a person acquired a certain role, etc.
end

Setting up polymorphic associations in db when the super is a FK of subclasses?

I opted for the simplest solution: using a polymorphic association (as shown in the second block). It will have better performance and be more maintainable.

Why does a polymorphic association in Rails with source_type set result in the wrong SQL statement?

I found a solution that works around the suspected bug: There is an undocumented method called polymorphic_name that ActiveRecord uses to determine what model name to use when doing polymorphic lookups.

When I change the Organization model to:

class Organization < ApplicationRecord
self.table_name = 'organisations'

has_many :roles, as: :access_to
has_many :users, through: :roles

def self.polymorphic_name
"Organisation"
end
end

then Organization.first.users generates the SQL I want:

SELECT "users".* FROM "users" INNER JOIN "roles"
ON "users"."id" = "roles"."user_id"
WHERE "roles"."access_to_id" = ?
AND "roles"."access_to_type" = ?
LIMIT ? [
["access_to_id", 1],
["access_to_type", "Organisation"],
["LIMIT", 11]]

Commit that fixed my example: https://github.com/RSpace/polymorphic-issue/commit/648de2c4afe54a1e1dff767c7b980bb905e50bad

I'd still love to hear why the other approach doesn't work though. This workaround seems risky, as I simply discovered this method by digging through the Rails code base, and it's only used internally: https://github.com/rails/rails/search?q=polymorphic_name&unscoped_q=polymorphic_name

EDIT: I now understand why setting source_type: "Organisation" results in a lookup in the organisations table rather than the users table, as the source_type option controls both model, table and polymorphic name as per the documentation. There is still a bug around getting "access_to_type" set twice, but fixing that won't get my use case working, as source_type is first and foremost for controlling, well, the source type of the association. I will instead pursue to get the polymorphic_name method documented and thus be part of the official ActiveRecord API.

Setting up a polymorphic association where associated tables have different primary key types (UUID and Integer)

The solution I ended up with is adding a likeable_id_string which stores any uuid primary key. I then override the getters and setters to do the following:

# Overriding getters and setters to handle multiple primary key types on
# this polymorphic association ( String and Integer )

# Setters
def likeable=(value)
if !value.nil? && value.id.is_a?(String)
self[:likeable_id] = nil
self[:likeable_id_string] = value.id
self[:likeable_type] = value.class.name
else
self[:likeable_id_string] = nil
super(value)
end
end

# in case likeable_type and likeable_id are updated seperately. I believe
# this is done in FE (rather than passing an object to .likeable)
def likeable_id=(value)
if !value.nil? && value.is_a?(String)
self[:likeable_id] = nil
self[:likeable_id_string] = value
else
self[:likeable_id_string] = nil
super(value)
end
end

# Getters
def likeable
if self.likeable_id
return super
elsif !self.likeable_type.nil?
return self.likeable_type.constantize.find(likeable_id_string)
else
return nil
end
end

You would think that the likeable= setter isn't needed once I've overridden the likeable_id= .. but seems like it is. If anyone knows why I need both please let me know as it would seem logical that likeable delegates to likeable_id and likeable_type

I feel like there's a use case for this to be a Gem. Something I might do soon. Please get in touch if you would like to see this as a gem (or if you think that's a terrible idea)

How to create seeds of a polymorphic relationship

to associate an image with both a seed employee and seed product

This would require a many-to-many relationship (either has_and_belongs_to_many or has_many :through):

#app/models/product.rb
class Product < ActiveRecord::Base
has_many :images, as: :imageable
has_many :pictures, through: :images
end

#app/models/employee.rb
class Employee < ActiveRecord::Base
has_many :images, as: :imageable
has_many :pictures, through: :images
end

#app/models/image.rb
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
belongs_to :picture
end

#app/models/picture.rb
class Picture < ActiveRecord::Base
has_many :images
end

This would allow you to use:

#db/seed.rb
@employee = Employee.find_or_create_by x: "y"
@picture = @employee.pictures.find_or_create_by file: x

@product = Product.find_or_create_by x: "y"
@product.pictures << @picture

ActiveRecord, has_many :through, and Polymorphic Associations


Because you're using a polymorphic relationship, you won't be able to use has_and_belongs_to_many.

The above will set the polymorphism on the join table; each Picture being "naked" (without a "creator"). Some hacking would be required to define the original creator of the image.

Polymorphic association, creating a record

I fixed this issue by doing this:

   new_item = GroupItem.new(group_id: group.id)
new_item.update_attribute(:groupable, person)

irb(main):046:0> new_item = GroupItem.new(group_id: group.id)
=> #<GroupItem id: nil, group_id: 1, groupable_type: nil, groupable_id: nil, created_at: nil, updated_at: nil>

irb(main):047:0> new_item.update_attribute(:groupable, person)
(0.1ms) begin transaction
SQL (0.4ms) INSERT INTO "group_items" ("group_id", "groupable_type", "groupable_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["group_id", 1], ["groupable_type", "Role"], ["groupable_id", 29], ["created_at", 2017-02-23 03:14:33 UTC], ["updated_at", 2017-02-23 03:14:33 UTC]]
(1.6ms) commit transaction
=> true

irb(main):049:0> new_item
=> #<GroupItem id: 3, group_id: 1, groupable_type: "Role", groupable_id: 29, created_at: "2017-02-23 03:14:33", updated_at: "2017-02-23 03:14:33">

Setting up a polymorphic has_many :through relationship

To create a polymorphic has_many :through, you must first create your models. We will use'Article,' 'Category,' and 'Tag' where 'Tag' is the join-model and Article is one of many objects which can be "tagged" with a category.

First you create your 'Article' and 'Category' models. These are basic models which do not need any special attention, just yet:

rails g model Article name:string
rails g model Category name:string

Now, we will create our polymorphic join-table:

rails g model Tag taggable_id:integer taggable_type:string category_id:integer

The join-table joins together two tables, or in our case one table to many others via polymorphic behavior. It does this by storing the ID from two separate tables. This creates a link. Our 'Category' table will always be a 'Category' so we include 'category_id.' The tables it links to vary, so we add an item 'taggable_id' which holds the id of any taggable item. Then, we use 'taggable_type' to complete the link allowing the link to know what it is linked to, such as an article.

Now, we need to set up our models:

class Article < ActiveRecord::Base
has_many :tags, :as => :taggable, :dependent => :destroy
has_many :categories, :through => :tags
end
class Category < ActiveRecord::Base
has_many :tags, :dependent => :destroy
has_many :articles, :through => :tags, :source => :taggable, :source_type => 'Article'
end
class Tag < ActiveRecord::Base
belongs_to :taggable, :polymorphic => true
belongs_to :category
end

After this, setup your database using:

rake db:migrate

That's it! Now, you can setup your database with real data:

Category.create :name => "Food"
Article.create :name => "Picking the right restaurant."
Article.create :name => "The perfect cherry pie!"
Article.create :name => "Foods to avoid when in a hurry!"
Category.create :name => "Kitchen"
Article.create :name => "The buyers guide to great refrigeration units."
Article.create :name => "The best stove for your money."
Category.create :name => "Beverages"
Article.create :name => "How to: Make your own soda."
Article.create :name => "How to: Fermenting fruit."

Now you have a few categories and various articles. They are not categorized using tags, however. So, we will need to do that:

a = Tag.new
a.taggable = Article.find_by_name("Picking the right restaurant.")
a.category = Category.find_by_name("Food")
a.save

You could then repeat this for each, this will link your categories and articles. After doing this you will be able to access each article's categories and each categorie's articles:

Article.first.categories
Category.first.articles

Notes:

1)Whenever you want to delete an item that is linked by a link-model make sure to use "destroy." When you destroy a linked object, it will also destroy the link. This ensures that there are no bad or dead links. This is why we use ':dependent => :destroy'

2)When setting up our 'Article' model, which is one our 'taggable' models, it must be linked using :as. Since in the preceeding example we used 'taggable_type' and 'taggable_id' we use :as => :taggable. This helps rails know how to store the values in the database.

3)When linking categories to articles, we use:
has_many :articles, :through => :tags, :source => :taggable, :source_type => 'Article'
This tells the category model that it should have many :articles through :tags. The source is :taggable, for the same reason as above. The source-type is "Article" because a model will automatically set taggable_type to its own name.



Related Topics



Leave a reply



Submit