Rails - Polymorphic Favorites (user can favorite different models)
Asked Answered
C

1

5

We are trying to add multiple favoritable objects, where a user can favorite many different objects, but are not sure how to make it work.

Here is the Favorite model:

class Favorite < ActiveRecord::Base
  # belongs_to :imageable, polymorphic: true
  belongs_to :user
  belongs_to :category
  belongs_to :business
  belongs_to :ad_channel
  belongs_to :location
  belongs_to :offer
end

The user model:

class User < ActiveRecord::Base
  has_many :favorites, as: :favoritable
end

And one example model of something that can be favorited:

class Category < ActiveRecord::Base
  has_many :sub_categories
  has_many :ad_channels
  has_many :offers
  belongs_to :favoritable, polymorphic: true
end

I'm not sure if this is set up properly so that would be the first thing we need some feedback on.

Secondly how do we "favorite" something for a user?

This is what we've tried so far unsuccessfully:

@user.favorites << Category.find(1)

EDIT: Also will this need a favorites database table to record things? This is a pretty new concept for us.

Cauca answered 16/2, 2014 at 21:37 Comment(0)
E
18

Model Relationships

Your Favorite model looks like this:

class Favorite < ActiveRecord::Base
  belongs_to :favoritable, polymorphic: true
  belongs_to :user, inverse_of: :favorites
end

Then, your User model will look like this:

class User < ActiveRecord::Base
  has_many :favorites, inverse_of: :user
end

Then, the models that can be favorited should look like this:

class Category < ActiveRecord::Base
  has_many :favorites, as: :favoritable
end

Yes, you will need a favorites table in your database.

Favoriting Items

So, this should allow you to do stuff like:

@user.favorites << Favorite.new(favoritabe: Category.find(1))  # add favorite for user

Just keep in mind that you need to add instances of Favorite to @user.favorites, not instances of favoritable models. The favoritable model is an attribute on the instance of Favorite.

But, really, the preferred way to do this in Rails is like so:

@user.favorites.build(favoritable: Category.find(1))

Finding Favorites of a Certain Kind

If you wanted to find only favorites of a certain type, you could do something like:

@user.favorites.where(favoritable_type: 'Category')  # get favorited categories for user
Favorite.where(favoritable_type: 'Category')         # get all favorited categories

If you're going to do this often, I think adding scopes to a polymorphic model is pretty clean:

class Favorite < ActiveRecord::Base
  scope :categories, -> { where(favoritable_type: 'Category') }
end

This allows you to do:

@user.favorites.categories

Which is gets you the same result as @user.favorites.where(favoritable_type: 'Category') from above.

Allowing Users to Favorite an Item Only Once

I'm guessing that you might also want to allow users to only be able to favorite an item once, so that you don't get, for example, duplicate categories when you do something like, @user.favorites.categories. Here's how you would set that up on your Favorite model:

class Favorite < ActiveRecord::Base
  belongs_to :favoritable, polymorphic: true
  belongs_to :user, inverse_of: :favorites

  validates :user_id, uniqueness: { 
    scope: [:favoritable_id, :favoritable_type],
    message: 'can only favorite an item once'
  }
end

This makes it so that a favorite must have a unique combination of user_id, favoritable_id, and favoritable_type. Since favoritable_id and favoritable_type are combined to get the favoritable item, this is equivalent to specifying that all favorites must have a unique combination of user_id and favoritable. Or, in plain English, "a user can only favorite something once".

Adding Indexes to the Database

For performance reasons, when you have polymorphic relationships, you want database indexes on the _id and _type columns. If you use the Rails generator with the polymorphic option, I think it will do this for you. Otherwise, you'll have to do it yourself.

If you're not sure, take a look your db/schema.rb file. If you have the following after the schema for your favorites table, then you're all set:

add_index :favorites, :favoritable_id
add_index :favorites, :favoritable_type

Otherwise, put those lines in a migration and run that bad boy.

While you're at it, you should make sure that all of your foreign keys also have indexes. In this example, that would be be the user_id column on the favorites table. Again, if you're not sure, check your schema file.

And one last thing about database indexes: if you are going to add the uniqueness constraint as outlined in the section above, you should add a unique index to your database. You would do that like this:

add_index :favorites, [:favoritable_id, :favoritable_type], unique: true

This will enforce the uniqueness constraint at the database level, which is necessary if you have multiple app servers all using a single database, and generally just the right way to do things.

Electrotype answered 16/2, 2014 at 23:5 Comment(9)
This works great. What does inverse_of: actually mean?Cauca
It's actually kind of dumb, in my opinion, because it's something that should happen automatically, but inverse_of basically informs Rails that these two relations are the inverse of each other. This prevents Rails from creating duplicates of records in memory. Read more here: guides.rubyonrails.org/…Electrotype
Great thanks again. One more question, How would you grab the categories that are favorited?Cauca
The categories that have been favorited by any user, or the categories favorited by a certain user?Electrotype
Nevermind my last question, I expanded the answer to cover both cases, as well as added some other stuff that you'll need to know for dealing with polymorphic associations.Electrotype
Thanks a lot. We really appreciate sharing such detailed info. Wish I could send you some gold or somethingCauca
Also, there is a small typo in the favoritable section of the scope for categories.Cauca
I'm not seeing it (probably because I wrote it :-). Can you edit the answer? Or paste it in a comment and I'll edit.Electrotype
I tried to edit it, but the favoritabe_type in the scope code is missing an "l". Caused an error, just trying to eliminate this for anyone who might use this in the future.Cauca

© 2022 - 2024 — McMap. All rights reserved.