Rails - Polymorphic Favorites (user can favorite different models) - ruby-on-rails

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.

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.

Related

Rails ActiveRecord equivalent of Laravel ORM `attach()` method for polymorphic has_many :through

I'm transitioning from "Laravel ORM" to "Rails Active Record" and I couldn't find how do you do something like this:
$this->people()->attach($person['id'], ['role' => $role]);
Explanation for Laravel code snippet
People is a polymorphic association to the class that is being accessed via $this via the Role class. The function above, creates a record in the middle table (roles/peopleables) like this:
id: {{generically defined}}
people_id: $person['id']
role: $role
peopleable_type: $this->type
peopleable_id: $this->id
How the association is defined on the Laravel end:
class XYZ {
...
public function people()
{
return $this->morphToMany(People::class, 'peopleable')->withPivot('role','id');
}
...
}
My efforts in Ruby
Here is how I made the association in Ruby:
class Peopleable < ApplicationRecord
belongs_to :people
belongs_to :peopleable, polymorphic: true
end
class People < ApplicationRecord
has_many :peopleables
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
I have seen the operation << but I don't know if there is any way to set an additional value on the pivot table while triggering this operation. [in this case the roles or peopleables tables; I use these two terms interchangeably in this app.]
PS. So, basically the question is how to define additional values on the pivot table in a polymorphic-many association in ActiveRecord and dynamically set those values while initiating an attachment relationship
Description of Functionality
Our application has a limitless [generally speaking, not that there is no computational limits!] content type: post, novel, poem, etc.
Each of these content types can be associated to individuals who play certain roles: editor, author, translator, etc.
So, for example:
X is the translator of Post#1. X, Y and Z are authors of Post#1.
There is a distinct People model and each content type has its own unique model [for example: Post, Poem, etc].
The idea of :through is referring to the 'Role class' or 'the pivot table' [whichever way you want to understand it] that the polymorphic association is recorded on it.
In addition to the information regarding a simple polymorphic relationship, there is also the kind of role that is recorded on the pivot table.
For example, X is both the author and the translator for Post#1, so there are two rows with the same people_id, peopleable_type and peopleable_id, however they have different values for role.
From what I understand given your description, I think you have this models (I'll change the names to what I understand they are, hope it's clear enough):
class Person < ApplicationRecord # using singular for models
has_many :person_roles
end
class Poem < ApplicationRecord
has_many :person_roles, as: :content
end
class Novel < ApplicationRecord
has_many :person_roles, as: :content
end
etc...
class PersonRole < ApplicationRecord
belongs_to :person
belongs_to :content, polymorphic: true
# you should have a "role" column on your table
end
So a Person is associated to a "content" (Novel, Poem, etc) via the join model PersonRole with a specific role. A Person that is the author of some novel and the editor of some peom would have two PersonRole records.
So, if you have a person and you want to assign a new role on some content, you can just do:
person.person_roles.create(role: :author, content: some_poem)
or
PersonRole.create(person: person, role: :author, content: some_poem)
or
some_poem.person_roles.create(person: person, role: :author)
You have two things in play here: "belongs_to :content, polymorphic: true" is covers the part of this being a polymorphic association. Then you have the "PersonRole" table that covers the part you know as "pivot table" (join table/model on rails).
Note that :through in rails has other meaning, you may want to get all the poems that a user is an author of, you could then have a "has_many :poems, through: :person_roles" association (that won't actually work, it's more complex than that in this case because you have a polymorphic association, you'll need to configure the association with some extra options like source and scope for this to work, I'm just using it as an example of what we understand as a has many :through association).
Rails is 'convention over configuration'. Models' must be in singular 'Person'.
ActiveRecord has has_many ... through and polymorphic association
"Assignable" and "Assignments" are more natural to read than "peoplable"
class Person < ApplicationRecord
has_many :assignments, as: :assignable
has_many :roles, through: :assignments
end
class Role < ApplicationRecord
has_many :assignments
has_many :people, through: :assignments
end
class Assignment
belongs_to :role
belongs_to :assignable, polymorphic: true
end
You can read more Rails has_many :through Polymorphic Association by Sean C Davis

Multiple Associations With the Same Table (Many-to-Many) Ruby on Rails

I am trying to figure out the best way to accomplish a relationship and having some trouble. I have come across a few articles that somewhat address what I am looking to do but not quite.
I have a Users model and a Tickets model. Both are joined through a UserTickets model so I am able to assign multiple Users to a ticket. What I would like to do is segment the Users assigned to a Ticket into requesters and agents. The Users model does not have any columns to declare if the user is a requester or agent, rather I have is_admin, is_customer, etc. I think what I need is along the lines of Ruby On Rails - many to many between the same table but not quite.
Ideally, I'd like to have my Tickets table take agent_id's (user_id from User class) and requester_id's (user_id from User class) rather than the general user_id's (user_id from User class which combines all the users into one group). I would assume would still allow me to call #current_user.tickets to pull all the tickets that are assigned to that user.
Here is my Users model:
has_many :user_tickets
has_many :support_tickets, through: :user_tickets
Here is my Tickets model:
has_many :user_tickets
has_many :users, through: :user_tickets
Here is my UserTickets join model:
belongs_to :user
belongs_to :support_ticket
Your help is greatly appreciated!
This is not a duplicate.
#Skylar,
1) If your goal is to assign multiple Users to a ticket? Because this doesn't require a many-to-many relationship. You just need to a one-many relationship and a boolean attribute agent? on User model. You can even create am Agent model that uses User model, if you like this better.
2) However, if you wanted to create a many-to-many relationship check out this. The Rails documentation is better than I write. See section 2.6 The has_and_belongs_to_many Association.
Solution to 1)
Models
class User < ApplicationRecord
has_many :tickets, class_name: :Ticket, foreign_key: :agent_id
def agent?
self.agent
end
end
class Agent < User
default_scope { where(agent: true) }
end
class Ticket < ApplicationRecord
belongs_to :user
belongs_to :agent, class_name: :User
end
Migrations
class CreateTickets < ActiveRecord::Migration[5.0]
def change
create_table :tickets do |t|
t.references :user, index: true
t.references :agent, index: true
t.string :event_name
t.timestamps
end
end
end

Rails Create "Collection" of posts

In my rails app, user's can create Designs.
Design.rb
belongs_to :user
User.rb
has_many :designs
I'm trying to create a new model Look so user's can create Looks. The way I envision this to work is when a user goes to /looks/new, they have a list of all the designs they have favorited (which I have that set up that variable already) in a table format with the right column being checkboxes where the user can go through and check a few of those Designs and click Create. All the Designs that have been checked would be part of that Look.
As I haven't done this sort of thing before, I need some help accomplishing this in all aspects MVC.
Look.rb
has_many :designs
Design.rb
belongs_to :looks # ??? Would the model be something different since technically when you create a design it doesn't belong to a look.
Looks Controller
def new
#designs = #user.favorites #This get's me all the designs that the particular user has favorited
#look = Look.new # ??? Again, as I haven't set this sort of relation up before, I'm unsure.
end
Please let me know any other code I can provide to help out. I may even be making this sound more complicated than it is.
This configuration should work for you Justin:
class User < ActiveRecord::Base
has_many :designs
has_many :looks, through: :designs
end
class Design < ActiveRecord::Base
belongs_to :user
has_many :designs_looks
has_many :looks, through: :designs_looks
end
class Look < ActiveRecord::Base
has_many :designs_looks
has_many :designs, through: :designs_looks
end
class DesignsLook < ActiveRecord::Base
belongs_to :design
belongs_to :look
validates :design_id, presence: true
validates :look_id, presence: true
end
I don't know what you want to do in the future but you might want to consider putting the user_id on the DesignsLook model, so you would not need a complex join query to retrieve all the Looks of a User. And also you implement shared Designs with all users
Your user has many designs. New looks can have many designs. And design can belong to MANY looks, users. Smells like has many ..., :through
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
class User
has_many :designs, through: :design_possesion
end
class Look
has_many :designs, through: :look_designs
end
class Design
has_many :look_designs, :design_possesion
end
Of course you'll have to create corresponding tables.

has_many and belongs_to of the same model

I am pretty new to rails. I am trying to figure out the most efficient way to create a relationship between two models that states:
A user can "favorite" many songs
A song has an owner.
This is what I am thinking of doing. Does it make sense ?
class User < ActiveRecord::Base
has_many :songs #songs this user has favorited
end
class Song < ActiveRecord::Base
belongs_to :user #the user whom submitted this song
end
My concern about this method is that I'm unsure about the efficiency of doing query on every song in the database just to figure out which songs a particular user owns. Is there a different way I should be thinking about this ?
By the way, is there a method by which I can call the attribute something different than it's model name. So rather than User.find(1).songs[0] I could say User.find(1).favorites[0] even though the model is still a "Song".
You'll need 2 separate relationships between the User and Song models. Namely, you'll need an 'owner' relationship and a 'favorite' relationship. The 'owner' relationship can be a simple has_many/belongs_to as you have it now. The 'favorite' relationship is many-to-many and will need a join table used either as a habtm table or a first class model with a has_many through relationship as explained here.
The generallly recommended approach is to use has_many through as it gives you better control:
class User
has_many :songs # these are songs 'owned' by the user
has_many :user_favorite_songs
has_many :favorite_songs, :through => :user_favorite_songs # these are the favorites
end
class Song
belongs_to :user
has_many :user_favorite_songs
end
class UserFavoriteSong
belongs_to :user
belongs_to :favorite_song, :class_name => 'Song', :foreign_key => :song_id
end
This looks perfectly fine.
Rails associations try to be most efficient - don't prematurely optimize.
You can alias the association's name like so:
class User < ActiveRecord::Base
has_many :favorites, class_name: 'Song'
end
see the docs about associations.
Regarding performance anyway, you might want to have a look at the :inverse_of association option.
I haven't tested this code, but you'll need something like this.
class User < ActiveRecord::Base
has_and_belongs_to_many :favorites, :class_name => "Song" #user's favorited songs
end
class Song < ActiveRecord::Base
belongs_to :user #the user who submitted the song
has_and_belongs_to_many :user, :as => :favorite
end
And since multiple users can favorite a song, you'll need a 'join table'
CreateUsersFavorites < ActiveRecord::Migration
def up
create_table :users_favorites do |t|
t.references :user
t.references :favorite
end
create_index :users_favorites, :user_id
create_index :users_favorites, :favorite_id
end
def down
drop_table :users_favorites
end
end
Also, I highly recommend taking a look at the rails guide for active record relationships.

Rails ActiveRecord Association

Okay, so here is my question. I have a 3 different models, People, Roles, Client, and Store. Clients have many Stores and can also have many people. Stores have many people. People have various roles. 1 Person can work at multiple stores, and they may have different roles at each store.
For example. Joe may be an assistant manager at one store and a manager at another store. What I would like to be able to do is pull the correct roles by doing something like Store.find(1).people.find(1).roles (would return 'assistant manager' for example) or
Store.find(2).people.find(1).roles (would return 'manager' for example). Is this possible to do in ActiveRecord?
I've created a table :roles_people which has the following definition:
create_table :roles_people, :id => false do |t|
t.references :role
t.references :person
t.references :store
t.references :client
end
However i can't figure out how to get associations to work properly using this table. Can anyone point me in the right direction?
Thanks
class People
belongs_to :client
has_many :store_roles
end
class Roles
has_many :store_roles
end
class StoreRole
belongs_to :role
belongs_to :people
belongs_to :store
end
class Client
has_many :stores
has_many :people
end
class Store
belongs_to :client
has_many :store_roles
has_many :roles, :through => :store_roles
end
Assume that all of those classes inherit from ActiveRecord::Base ;)
You're going to need to setup the migration and database structure to mirror these relationships. For each belongs_to there is an :object_id field on the table reference the appropriate table's id.
Your query is going to need to look something like:
Store.find(1).roles.find(:all, :conditions => ["store_roles.person_id = ?", 1])
I would probably add a method to the store model to make this a little easier:
def roles_for(person_id)
roles.find(:all, :conditions => ["store_roles.person_id = ?", person_id])
end
This way you can find the roles using:
Store.find(1).roles_for(1)
Or, better yet:
def self.roles_for(store_id, person_id)
Role.find(:all, :joins => :store_roles, :conditions => ["store_roles.store_id = ? AND store_roles.person_id = ?", store_id, person_id])
end
Which changes our finder to:
Store.roles_for(1, 1)
I would say that this last method is the most ideal since it causes only a single query, while each of the other options execute two queries to the database per role look-up (one to find the store, and one to get the roles for a person_id). Of course if you already have the Store object instantiated then it's not a big deal.
Hopefully this answer was sufficient :)
I think what you want is has_many :through
class Person < ActiveRecord::Base
has_many :roles_people
has_many :roles, :through => :roles_people
end
class Store < ActiveRecord::Base
has_many :roles_people
has_many :people, :through => roles_people
end
You'll also need to add relationships to RolePerson:
class RolePerson < ActiveRecord::Base
belongs_to :store
belongs_to :person
has_one :role
end
Is that what you were looking for?
Very helpful link #blog.hasmanythrough.com
has_and_belongs_to_many is your friend.
class Person < ActiveRecord::Base
has_and_belongs_to_many :roles
end
That way, you can get all roles the person has by calling Person.roles.all. The resulting query is going to use the people_roles table. You can also use has_many :through but have to build model classes for the join table yourself and maintain all the associations yourself. Sometimes it's necessary, sometimes it's not. Depends on the complexity of your actual model.
Nice question. You can't do exactly what you wanted, but i guess we can come close.
For completeness, i am going to recap your datastructure:
class Client
has_many :stores
end
class Store
has_many :people
has_many :roles
end
class Person
has_many :roles
has_many :stores
end
class Role
belongs_to :store
belongs_to :person
end
You see that the role does not need the link to the client, because that can be found straightaway from the store (i am assuming a stored is "owned" by only one client).
Now a role is linked both to a person and a store, so a person can have different roles per store.
And to find these in a clean way, i would use a helper function:
class Person
has_many :roles
has_many :stores
def roles_for(store)
roles.where("store_id=?", store.id)
end
end
So you can't write something like
store.people.first.roles
to get the roles of the first person working for that store.
But writing something like:
store.people.first.roles_for(store)
is not too hard i hope.
The reason why this is so is because in the context of the person (-> store.people.first) we no longer have any notion of the store (how we got there).
Hope this helps.
You need to change your table name in people_roles and you can drop both store and client references:
create_table :roles_people, :id => false do |t|
t.references :role
t.references :person
t.references :store
end
Role is something that belongs only to people.
You then need to use has_and_belongs_to_many:
class Person < ActiveRecord::Base
has_many :roles
has_many :stores, :through => :people_roles
end
class Store < ActiveRecord::Base
has_many :roles
has_many :people, :through => :people_roles
end
Than you can query:
Store.find(1).people.find(1).roles

Resources