Activerecord has_many :through through multiple models - ruby-on-rails

I'm trying to access all comments from a given user with user.comments. The query is to go through two different models, which likely both return results. My relations are set up as follow:
class User < ActiveRecord::Base
has_many :organisers
has_many :participants
has_many :comments, through: :participants / :organisers (see explenation below)
end
class Organiser < ActiveRecord::Base
belongs_to :user
end
class Participant < ActiveRecord::Base
belongs_to :user
end
class Comment < ActiveRecord::Base
belongs_to :organiser
belongs_to :participant
end
A comment is validated to belong to either a participant, or an organiser.
I'm not sure how to go about this. I've tried
has_many :comments, through: :participants
has_many :comments, through: :organisers
and
has_many :comments, through: [:organisers, :participants]
But that last one isn't rails. Is there a proper way to do this? Thanks!

has_many :comments, ->(user) {
unscope(where: :user_id).
left_joins(:organizer, :participant).
where('organizers.user_id = ? OR participants.user_id = ?', user.id, user.id)
}
The unscope is to remove the comments.user_id = ? clause (which is added by default when you define a has_many relation).
The left_joins is called on Comment, so you need to pass in the relation names as defined on Comment, hence the singulars in this example.

I found a solution after many tries. You can use a scope with param in your last has_many sentence in the User model:
has_many :comments, -> (user) {where organiser: user.organisers}, through: :participants
The "user" param represet the User object whom is calling the comments method.

For anyone coming across this using polymorphic associations, the following worked for me inspired by magni- and Carlos Jimenez' answers:
has_many :comments, -> (user) {
unscope(where: :user_id).
where(commentable: [user.organizers, user.participants])
}

Since we couldn't use has_many, through here because comments come from both of organisers and participants. I just think there are 2 solutions here:
Solution #1 Define comments method:
class User < ActiveRecord::Base
def comments
Comment.joins([{organiser: :user}, {participant: :user}])
.where(users: {id: self.id})
end
end
So then your query to find comments is:
User.first.comments
Solution #2 Use scope in Comment
class Comment < ActiveRecord::Base
scope :from_user, -> (user) {
joins([{organiser: :user}, {participant: :user}]).where(users: {id: user.id})
}
end
So your query will be like:
user = User.first
comments = Comment.from_user(user)

Since we couldn't use has_many, through here because comments come from both of organizers and participants. I just think there are 2 solutions here:
Basically you can still change the foreign key to accept the self.id automatically with Rails here
User.first.comments
class User
has_many :comments, -> { joins([{ organiser: :user }, { participant: :user }]) }, through: :participants, foreign_key: "users.id"
end

I believe your associations would be confused, as user.comments wouldn't know whether it's going through Participant or Organiser, so the best option would be to declare two different joins (with different names):
http://guides.rubyonrails.org/association_basics.html#self-joins

Related

Find record with two ids at the same time

I have models:
User
class User < ActiveRecord::Base
has_many :users_conversations, dependent: :destroy
has_many :conversations, through: :users_conversations, dependent: :destroy
end
UsersConversation
class UsersConversation < ActiveRecord::Base
belongs_to :conversation
belongs_to :user
end
Conversation
class Conversation < ActiveRecord::Base
has_many :users_conversations, dependent: :destroy
has_many :users, through: :users_conversations
end
I need to create query to get the joint conversation between two known users by their ID's. I have this one
Conversation.left_joins(:users).where(users: { id: [current_user.id, params[:user_id]]}).first
but it finds conversations where is any of [current_user.id, params[:user_id]], so I always get the same conversation, where is current_user, but it is wrong. As I see it is because id: [current_user.id, params[:user_id]] works like OR, but I need AND. Thank you.
The answer is not correct as mentioned in the comments. Please do not refer it.
Try using
Conversation.left_joins(:users).find_by("users.id IN(?)", [current_user.id, params[:user_id]]})
Haven't actually tried but this should work. Please let me know if you face an error.
Also, use find_by instead or Model.where(condition).first. Using the latter is a bad habit and convention in rails.
Explanation: When you have to find the records where a field matches an array, i.e., you need the value of this filed to be specific from an array, you should use IN operator of SQL. Like
SELECT * FROM Customers WHERE Country IN ('Germany', 'France', 'UK');

Rails 4: Model Logic - (Limited Associations)

Is there a way to use a similar command to .where() in the Model?
In my instance I want to loop through items but I want to check for a database value first:
User Model:
has_many :purchases
has_many :purchasedtools, through: :purchases, source: :tool #only where "status: "Completed"!
Purchase Model:
belongs_to :user
belongs_to :tool
Tool Model:
belongs_to :user
has_many :purchases
Tools Controller:
def index
#boughttools = current_user.purchasedtools
end
Tool Index View:
- #boughttools.limit(4).each do |tool|
I expect something like: #boughttools = current_user.purchases.where(status: "Completed", user_id: current_user.id).The only difference is that I need purchasedtools instead of purchases because I want the actual Tools as source.Fortunately I can't just change this as is because the db values I check are on the purchase table and not on the tool table.
Thanks in advance for each answer! Please tell me if you need additional information.
As I suggested in my comment, you might be looking for a conditional association:
has_many :purchasedtools,
-> { where(status: 'Completed') }
through: :purchases,
source: :tool
Check out docs for more info on associations.
To add a scope to an association you simply pass a lambda or proc to the association macro:
class User
has_many :purchases
has_many :completed_purchases, ->{ where(status: 'Complete') },
class_name: 'Purchase'
has_many :purchased_tools
through: :completed_purchases
source: :tool
end
Added
A simpler way to do this as noticed by the OP would just be to specify that the condition applies to the join table:
has_many :purchased_tools, -> { where(purchases: {status: "Completed"} ) }
through: :completed_purchases
source: :tool
See Scopes for has_many
You can use joins
Purchase.joins(:tool).where(tools: {user_id: current_user.id}, purchases: {status: 'Completed'})

Active record query using joins

I have three different models User, Order, Product:-
class User < ActiveRecord::Base
has_many :orders
has_many :products
end
class Product < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :user
belongs_to :product
end
How to find all the products which an user has ordered using a one line active-record query?
Another options here is using has_many :through relation:
Add to User model
has_many :products, through: :orders
and now user.products does the trick
This is explained in the ActiveRecord documentation:
Product.joins(:orders).where(orders: { user_id: user.id })
I got it finally through this
Product.joins(:orders).where(order: { user_id: user.id })

Rails: Find array of related objects

This is the code I currently have:
project_ids = ProjectEnrollment.unscoped.where(user: self).pluck(:project_id)
Project.where(id: project_ids)
I'm sure there has to be a better way. Thanks!
Edit: I'm already using the method from the has_many relationship, with scoped projects for another case.
In your User model:
has_many :project_enrollments, -> { unscoped }
has_many :projects, through: :project_enrollments
In your ProjectEnrollment model:
belongs_to :user
belongs_to :project
Then you can do
user.projects

rails polymorphic with includes based on type of class

Let's say we have these models
class Message
belongs_to :messageable, polymorphic: true
end
class Ticket
has_many :messages, as: :messageable
has_many :comments
end
class User
has_many :messages, as: :messageable
has_many :ratings
end
class Rating
belongs_to :user
end
class Comment
belongs_to :ticket
end
Now I want to load all messages (which have associated tickets or users), and eager load depending on the type of class, either comments for tickets and ratings for users
Of course Message.includes(:messageable).order("created_at desc") will only include the directly associated object, but the question would be how to include the different association types that derive from each model type (i.e. in this example, how to eager load comments for tickets and ratings for users)?
This is just a simple example, but what about even more complicated cases, where I'd like to include something else for the user, another association, and what if that association needs more includes?
The only way I can think of to do this is to duplicate the associations on each model with a common name:
class Ticket
has_many :messages, as: :messageable
has_many :comments
has_many :messageable_includes, class_name: "Comment"
end
class User
has_many :messages, as: :messageable
has_many :ratings
has_many :messageable_includes, class_name: "Rating"
end
Message.includes(:messageable => :messageable_includes) ...
I'm not sure I would use this strategy as a widespread solution, but if this is a complicated as your case gets, it may work for you.
I've used the following helper methods in my own project:
def polymorphic_association_includes(association, includes_association_name, includes_by_type)
includes_by_type.each_pair do |includes_association_type, includes|
polymorphic_association_includes_for_type(association, includes_association_name, includes_association_type, includes)
end
end
def polymorphic_association_includes_for_type(association, includes_association_name, includes_association_type, includes)
id_attr = "#{includes_association_name}_id"
type_attr = "#{includes_association_name}_type"
items = association.select {|item| item[type_attr] == includes_association_type.to_s }
item_ids = items.map {|item| item[id_attr] }
items_with_includes = includes_association_type.where(id: item_ids).includes(includes).index_by(&:id)
items.each do |parent|
parent.send("#{includes_association_name}=", items_with_includes[parent[id_attr]])
end
end
These would allow you to say:
messages = Message.all
polymorhpic_association_includes messages, :messageable, {
Ticket => :comments,
User => :ratings
}
Not a particularly fluent interface but it works in general.
Place the includes on a default scope for each model:
class Ticket
has_many :messages, as: :messageable
has_many :comments
default_scope -> { includes(:comments).order('id DESC') }
end
class User
has_many :messages, as: :messageable
has_many :ratings
default_scope -> { includes(:ratings).order('id DESC') }
end
Then whenever you call Message.all each polymorphic association will include it's own resources.
Also if you need to call the class without the scope just use unscoped or create a different scope:
class Ticket
has_many :messages, as: :messageable
has_many :comments
has_many :watchers
default_scope -> { includes(:comments).order('id DESC') }
scope :watched -> {includes(:watchers)}
end
Ticket.unscoped.all # without comments or watchers (or order)
Ticket.watched.all # includes watchers only

Resources