Rails: Polymorphic has_many: through workarounds - ruby-on-rails

My app allows users to follow several different model types, including other users, organizations, products, etc. Each of these models has a has_many: :histories association. My goal is to compile all the :histories of the resources the current_user is following.
My models look like this:
class Follow < ActiveRecord::Base
belongs_to :user
belongs_to :followable, polymorphic: true
end
class History < ActiveRecord::Base
belongs_to :historical, polymorphic: true
end
class User < ActiveRecord::Base
has_many :follows
has_many :followed_resources, through: :follows, source: :followable
has_many :followed_histories, through: :followed_resources, source: :histories
has_many :followings, class_name: "Follow", as: :followable
has_many :histories, as: :historical
end
class Product < ActiveRecord::Base
has_many :followings, class_name: "Follow", as: :followable
has_many :histories, as: :historical
end
class Organization < ActiveRecord::Base
has_many :followings, class_name: "Follow", as: :followable
has_many :histories, as: :historical
end
etc
My goal is to get the histories from all the current_user's followed resources like so:
#histories = current_user.followed_histories
But unfortunately, Rails does not allow us to traverse polymorphic association in a has_many: through relationship. Instead, it insists that we specify only a single association using the source_type option. For instance
has_many :followed_products, through: :follows, source: :followable, source_type: :product
Unfortunately, this approach won't work in this case, unless there's some way to recombine all the associations afterwards. For instance, if there was some way to do this:
has_many :followed_histories, through: {:followed_products, :followed_organizations, :followed_users}
Or perhaps there's yet another approach I'm not considering. I'm open to any suggestions, so long as the end result is a single array containing the combined histories of the followed resources.

Related

how can create relation with has many through in rails

I have 3 model User Project Bug. I want to create many to many relation with through. I create the relation in model i don't know it is correct or not, user have user type column which is enum type user type contain developer, manager , QA
user.user_type.manager belong to many project it has one to many relation
user.user_type.developer has many project and many project belong to developer. it has many to many realtion
project has many bugs and bugs belong to project
developer has many bugs and many bugs belong to developer
bug model
class Bug < ApplicationRecord
belongs_to :project
has_many :developers, -> { where user_type: :Developer }, class_name: 'User', through: :project, source: :bugs
end
project model
class Project < ApplicationRecord
has_many :bugs, dependent: :delete_all
has_many :developers, -> { where user_type: :Developer }, class_name: 'User', through: :users, source: :project
has_many :users //it belong to manager_id
end
user model
class User < ApplicationRecord
enum user_type: %i[Manager Developer QA]
has_many :projects
has_many :bugs
end
developer_bug model
class DevelopersBug < ApplicationRecord
has_many :bugs
has_many :users
end
project_developer model
class ProjectsDeveloper < ApplicationRecord
has_many :projects
has_many :users
end
This
has_many :developers, -> { where user_type: :Developer },
class_name: 'User',
through: :users,
source: :project
is not what you think it is. It means something on the line of:
I already have an association 'users'. The users have an association 'project'.
Please configure an association that makes both JOINs and gives me the list of projects associated to the associated users.
This association will be named "developers" and be of objects of class "User".
You can see how these instructions are inconsistent. This
has_many :projects, through: :users, source: :project
will define a list of associated projects, by jumping over users.
On the other side, this:
has_many :developers, -> { where user_type: :Developer }, class_name: 'User'
will define a direct has-many association with a subset of all the users.
Given your description, your data model seems wrong, maybe this will be better:
class User < ApplicationRecord
has_many :managed_projects, inverse_of: :manager, class_name: 'Project'
has_and_belongs_to_many :projects
has_many :bugs
end
class Project < ApplicationRecord
belongs_to :manager, class_name: 'User', inverse_of: :managed_projects
has_and_belongs_to_many :users
has_many :bugs
end
class Bug < ApplicationRecord
belongs_to :user
belongs_to :project
end
Your schema should include the three tables, and an additional many-to-many join table projects_users that holds foreign keys to both users and projects.
As rewritten has already pointed in his excellent answer out your data model is flawed. What you want instead is a join table which joins the users and projects:
class User < ApplicationRecord
has_many :project_roles
has_many :projects, through: :project_roles
end
class ProjectRole < ApplicationRecord
belongs_to :user
belongs_to :project
end
class Project < ApplicationRecord
has_many :users
has_many :projects, through: :project_roles
end
If you then want to give the user specific roles in a project you would add the enum to the join table and this is where it starts to get hairy so bear with me here:
class ProjectRole < ApplicationRecord
enum roles: [:manager, :developer, :qa]
belongs_to :user
belongs_to :project
end
class User < ApplicationRecord
has_many :project_roles
has_many :projects, through: :project_roles
has_many :project_roles_as_manager,
-> { manager }, # short for `where(role: :manager)`
class_name: 'ProjectRole'
has_many :projects_as_manager,
class_name: 'Project',
through: :project_roles_as_manager,
source: :project
has_many :project_roles_as_developer,
-> { developer },
class_name: 'ProjectRole'
has_many :projects_as_developer,
class_name: 'Project',
through: :project_roles_as_developer,
source: :project
# ...
end
This defines associations with a default scope and then joins through that association. You would then do the same thing on the other end of the assocation:
class Project < ApplicationRecord
has_many :users
has_many :projects, through: :project_roles
has_many :manager_project_roles,
-> { manager },
class_name: 'ProjectRole'
has_many :managers,
through: :manager_project_roles,
source: :user
# ...
end
Of course this is a lot of duplication which you can cut by looping over ProjectRoles.roles.keys and defining the assocations dynamically.
This is a very flexible way of modeling it which makes as few assumptions about the domain as possible. For example it allows multiple managers for a project and it allows users to have different roles in different projects.
If you want to model "bugs" as you would typically would with issues in a tracker you would create one table for the bug and a join table for the assignment:
class Bug < ApplicationRecord
belongs_to :project
has_many :bug_assignments
has_many :users, through: :bug_assignments
end
class BugAssignment < ApplicationRecord
has_one :project, through: :bug
belongs_to :bug
belongs_to :user
end
class User < ApplicationRecord
# ...
has_many :bug_assignments
has_many :bugs
end

Rails Complex Model Association, Shared Document Between Users and Teams

I have a complex model association in mind, and was wondering how I could accomplish it. This is what i want to accomplish.
I have a User and a Document model
A User can create documents. He is now the document admin.
He can add other users to his document, and give them permissions such as Editor, Viewer, Admin
He can also make a team, a group of users, and add multiple teams to his document. Each user on a team that the User has added to his document will also have permissions. A user can belong to many teams.
I am a little bit confused about the associations I will have to setup. This is the code I have so far, which has not incorporated the team aspect:
class User < ApplicationRecord
has_many :participations
has_many :documents, through: :participations
end
class Document < ApplicationRecord
has_many :participations
has_many :users, through: :participations
end
class Participation < ApplicationRecord
belongs_to :user
belongs_to :document
enum role: [ :admin, :editor, :viewer ]
end
I would recommend introducing a Team and TeamMembership models in a similary way to existing models. Also change the belongs_to association on Participation from user to a polymorphic participant.
class Team < ApplicationRecord
has_many :team_memberships
has_many :users, through: :team_memberships
has_many :participations, as: :participant
end
class TeamMembership < ApplicationRecord
belongs_to :team
belongs_to :user
end
class User < ApplicationRecord
has_many :team_memberships
has_many :teams, through: :team_memberships
has_many :participations, as: :participant
end
class Participation < ApplicationRecord
belongs_to :participant, polymorphic: true
belongs_to :document
enum role: [ :admin, :editor, :viewer ]
end
class Document < ApplicationRecord
has_many :participations
# if desired create a users method to conveniently get a list of users with access to the document
def users
#users ||= participations.flat_map do |participation|
case participation.partipant
when User
[participation.participant]
when Team
participation.participant.users
end
end
end
end
I would only add has_many :through associations as you discover a benefit/need to having them. That will reduce complexity of maintaining them unless you have specific use case for them. In the case of User having a teams association, it's pretty obvious that you'll be likely to want to get the teams that the user is a part of and since there's no specific information in the TeamMembership object that you are likely to need in that determination, it's a good has_many :through to have.
EDIT: Added Document model.
Since you already have a participation model, you can use that as the join model between users and teams. Since a user can belong to multiple teams, and a document can have multiple teams, you can use a has_many through relationship between teams and documents. We'll call it the DocumentTeam model.
class User < ApplicationRecord
has_many :participations
has_many :documents, through: :participations
has_many :teams, through: :participations
end
class Participation < ApplicationRecord
belongs_to :document
belongs_to :user
belongs_to :team, optional: true
enum role: [ :admin, :editor, :viewer ]
end
class Team < ApplicationRecord
has_many :participations
has_many :users, through: :participations
has_many :document_teams
has_many :document, through: :document_teams
end
class Document < ApplicationRecord
has_many :participations
has_many :users, through: :participations
has_many :document_teams
has_many :teams, through: :document_teams
end
class DocumentTeam < ApplicationRecord
belongs_to :document
belongs_to :team
end

Polymorphic Association with same model Ruby on Rails 4

So I have an app in which users can create cars. They can also like cars and I want to create an association between both. Cars that they created belong to them and Cars that they have liked belong to them through the context of liking them. To do this I have set up my associations as follows:
User Association:
class User < ActiveRecord::Base
has_many :cars
has_many :cars, -> {distinct}, through: :likes
end
Car Association:
class Car < ActiveRecord::Base
belongs_to :users
has_many :likes
has_many :users, -> { distinct }, through: :likes
end
Like Association:
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :car
end
The problem is that before I had my user has_many cars through like relationship declared. I used to be able to call #user.cars and it would present the user's cars. Now it returns the collection of the cars the user has liked. I need methods for each collection.
When I try: User.likes.cars
I get a
No Method error
and the console log looks through the likes records and still doesn't return the cars even though my likes records have a car_id field.
I have looked at a bunch of questions but have trouble understanding them. I've also tried to define methods in the model and nothing is seeming to work. Any help is appreciated.
How would I be able to change my associations so I can have a for query both User.cars (for cars the user has created) and User.likes.cars (for cars the user has liked)?
So the below answer from Oleg didn't work exactly but led me in the right direction. Thank you! I started by following the above example and doing:
class User < ActiveRecord::Base
has_many :cars
has_many :car_likes, -> {distinct}, class_name: 'Car', through: :likes
end
class Car < ActiveRecord::Base
belongs_to :users
has_many :likes
has_many :user_likes, -> { distinct }, class_name: 'User', through: :likes
end
This returned the following error in console:
ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) "car_likes" or :car_like in model Like. Try 'has_many :car_likes, :through => :likes, :source => '. Is it one of user or car?
So I changed it to:
class User < ActiveRecord::Base
has_many :cars
has_many :car_likes, -> {distinct}, through: :likes, source: :cars
end
Car Association:
class Car < ActiveRecord::Base
belongs_to :users
has_many :likes
has_many :user_likes, -> { distinct }, through: :likes, source: :users
end
It nows works for both models! Thanks and hopefully this is helpful to someone else with the same problem.
has_many :cars, -> {distinct}, through: :likes overrides has_many :cars because it redefines User.cars. Try the following:
class User < ActiveRecord::Base
has_many :cars
has_many :car_likes, -> {distinct}, class_name: 'Car', through: :likes
end
Car Association:
class Car < ActiveRecord::Base
belongs_to :users
has_many :likes
has_many :user_likes, -> { distinct }, class_name: 'User', through: :likes
end
#To get them, instead of user.likes.cars
#user.car_likes
#car.user_likes
Please let me know if the problem persists. There might be another error.
I don't see where you are defining any model as polymorphic.
In the past I have done something like this.. actually I did this for tags/taggings and made "like" a tag a user applied to another instance. This is an ad-hoc modification and I may have missed something but it's a pretty common use case for polymorphic associations.
class Like < ActiveRecord::Base
belongs_to :likeable, polymorphic: true
...
end
class Liking < ActiveRecord::Base
belongs_to :like
belongs_to :likeable, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :likings, :as => :likeable
has_many :likes, -> { order(created_at: :desc) }, :through => :taggings
end

has_many :through and has_many relationship between same 2 models

A list has one owner (a user). A list also has a number of panelists (also users). I have tried defining the relationships between the three models: User, List, and Panelist. But I'm getting nowhere.
user.rb
class User < ActiveRecord::Base
has_many :lists
has_many :panelMemberships, :through => :panelists, :source => :lists
end
list.rb
class List < ActiveRecord::Base
belongs_to :user
has_many :panelMembers, :through => :panelists, :source => :user
end
panelist.rb
class Panelist < ActiveRecord::Base
belongs_to :list
belongs_to :user
end
I've tried all different combinations but nothing seems to work. Thanks in advance for any help you can provide.
The model also has to have a has_many relationship for whatever the through model is, so wherever you have has_many :x, through: :y, you also need to say has_many :y. You also shouldn't have a panelist model separate from your user model if panelists are users (unless you're doing STI, which you're not). From what I understand, you're trying to do something like this:
class User < ActiveRecord::Base
has_many :owned_lists, class_name: "List", foreign_key: :owner_id # this is for the owner/list relationship
has_and_belongs_to_many :lists # for the normal panelist / list relationship
end
class List < ActiveRecord::Base
belongs_to :owner, class_name: "User"
has_and_belongs_to_many :users
end
Then you'll need to make a migration for a users_lists (with user id and list id) table which will be your join table but won't need its own model. But if you really want to keep the through relationship (good for if you do other stuff with the join model), then you'd do:
class User < ActiveRecord::Base
has_many :owned_lists, class_name: "List", foreign_key: :owner_id # this is for the owner/list relationship
has_many :panel_memberships
has_many :lists, through: :panel_memberships
end
class List < ActiveRecord::Base
belongs_to :owner, class_name: "User"
has_many :panel_memberships
has_many :users, through: :panel_memberships
end
class PanelMembership < ActiveRecord::Base
belongs_to :user
belongs_to :list

Rails: ignoring duplicates in an nested association

I have models User, Team, Document. There's a many-to-many relationship between Users and Teams, and a many-to-many relationship between Teams and Documents, using join tables called TeamMembership and TeamDocument respectively.
The relationships in my models look like this:
class Document < ActiveRecord::Base
has_many :team_documents, dependent: :destroy
has_many :teams, through: :team_documents
end
class User < ActiveRecord::Base
has_many :team_memberships, dependent: :destroy, foreign_key: :member_id
has_many :teams, through: :team_memberships
has_many :documents, through: :teams
end
class TeamDocument < ActiveRecord::Base
belongs_to :team
belongs_to :document
end
class TeamMembership < ActiveRecord::Base
belongs_to :team
belongs_to :member, class_name: "User"
end
class Team < ActiveRecord::Base
has_many :team_documents, dependent: :destroy
has_many :documents, through: :team_documents
has_many :team_memberships, dependent: :destroy
has_many :members, through: :team_memberships
end
The idea is that users can belong to many teams, a document can be associated with many teams, and users will only have access to documents that "belong" to at least one team that the user is a member of.
Here's the question: I can use User#documents to retrieve a list of all the documents that this user is allowed to view. But this will return duplicates if a document is viewable by more than one team which the user is a member of. How can I avoid this?
I know I can remove the duplicates after the fact with #user.documents.uniq, but as I will never want to include the duplicates in any case, is there a way I can just make #documents not include duplicates every time?
I don't have nested has_many :through like yours to test it, but I suspect using uniq option on your user association would help :
class User < ActiveRecord::Base
has_many :documents, through: :teams, uniq: true
end
You can add a default_scope on Document model:
class Document < ActiveRecord::Base
default_scope group: { documents: :id }

Resources