Eager load polymorphic associations - ruby-on-rails

I have the following models:
class Conversation < ActiveRecord::Base
belongs_to :sender, foreign_key: :sender_id, polymorphic: true
belongs_to :receiver, foreign_key: :receiver_id, polymorphic: true
.
class Owner < ActiveRecord::Base
.
class Provider < ActiveRecord::Base
.
class Account < ActiveRecord::Base
has_one :provider, dependent: :destroy
has_many :owners, dependent: :destroy
So in a Conversation, sender can be an Owner or a Provider. With this in mind, I can make queries like:
Conversation.includes(sender: :account).limit 5
This works as intended. The problem is when I want to use a where clause in associated model Account. I want to filter conversations which associated account's country is 'US'. Something like this:
Conversation.includes(sender: :account).where('accounts.country' => 'US').limit 5
But this wont work, I get the error ActiveRecord::EagerLoadPolymorphicError: Cannot eagerly load the polymorphic association :sender
What is the correct way of doing this kind of query?
I've also tried to use joins, but I get the same error.

I've found myself a solution, in case anyone else is interested. I ended up using joins with a SQL query:
Conversation.joins("LEFT JOIN owners ON owners.id = conversations.sender_id AND conversations.sender_type = 'Owner'
LEFT JOIN providers ON providers.id = conversations.sender_id AND conversations.sender_type = 'Provider'
LEFT JOIN accounts ON accounts.id = owners.account_id OR accounts.id = providers.account_id").
where('accounts.country' => 'US').limit 5

Related

Finding entries in a join table with identical links

In my chat app I have users and chats. The tables for each of these is connected by a join table:
class User < ApplicationRecord
has_and_belongs_to_many :chats_users
has_and_belongs_to_many :chats, :through => :chats_users
end
class Chat < ApplicationRecord
has_and_belongs_to_many :chats_users
has_and_belongs_to_many :users, :through => :chats_users
end
class ChatsUsers < ApplicationRecord
belongs_to :chat, class_name: 'Chat'
belongs_to :user, class_name: 'User'
validates :ad_id, presence: true
validates :tag_id, presence: true
end
And the inverse in chat.rb.
When creating a new chat with a list of participating list of user_ids, I want to first check a chat doesn't already exist with the exact same list of associated user_ids, but I can't work out a sane way to do this. How can this be done?
has_and_belongs_to_many is only used in the case where you do not need a join model (or where you initially think you don't need it) as its headless. Instead you want to use has_many through::
class User < ApplicationRecord
has_many :chat_users
has_many :chats, through: :chat_users
end
class Chat < ApplicationRecord
has_many :chat_users
has_many :users, through: :chat_users
end
class ChatUser < ApplicationRecord
belongs_to :chat
belongs_to :user
# not needed if these are belongs_to associations in Rails 5+
validates :ad_id, presence: true
validates :tag_id, presence: true
end
You may need to create a migration to change the name of your table to chat_users and make sure it has a primary key.
has_and_belongs_to_many uses an oddball plural_plural naming scheme that will cause rails to infer that the class is named Chats::User since plural words are treated as modules. While you can work around that by explicitly listing the class name its better to just align your schema with the conventions.
If your still just messing about in development roll back and delete the migration that created the join table and run rails g model ChatUser chat:belongs_to user:belongs_to to generate the correct table with a primary key and timestamps.
If you want to select chats connected to a given set of users:
users = [1,2,3]
Chat.joins(:users)
.where(users: { id: users })
.group(:id)
.having(User.arel_table[Arel.star].count.eq(users.length))
.exists?
Note that you don't really need to tell ActiveRecord which table its going through. Thats the beauty of indirect associations.

Joining Nested Associations (Multiple Level)

I have the following models and relationships:
A User has many Offers (where he/she is the seller), an Offer has many Purchases, a Purchase has many Accbooks
Models and associations:
class User < ApplicationRecord
has_many :offers, foreign_key: :seller_id
has_many :purchases, foreign_key: :buyer_id
end
class Offer < ApplicationRecord
has_many :purchases
belongs_to :seller, class_name: 'User'
end
class Purchase < ApplicationRecord
belongs_to :offer
belongs_to :buyer, class_name: 'User'
has_one :seller, through: :offer
has_many :accbooks, class_name: 'Admin::Accbook', foreign_key: 'purchase_id'
end
module Admin
class Accbook < ApplicationRecord
belongs_to :purchase
end
end
I want to get all the Accbooks of any given user (as a seller). The equivalent SQL statement would look like this:
SELECT "accbooks".*
FROM "accbooks"
INNER JOIN "purchases" ON "purchases"."id" = "accbooks"."purchase_id"
INNER JOIN "offers" ON "offers"."id" = "purchases"."offer_id"
INNER JOIN "users" ON "users"."id" = "offers"."seller_id"
WHERE "users"."id" = ?
So far I've tried this:
Admin::Accbook.joins( {purchase: :offer} )
Which gives me this SQL as a result:
SELECT "accbooks".*
FROM "accbooks"
INNER JOIN "purchases" ON "purchases"."id" = "accbooks"."purchase_id"
INNER JOIN "offers" ON "offers"."id" = "purchases"."offer_id"
Now I donĀ“t know how to add the join to the User model, and then how to add the Where condition.
Thanks for any insight.
You can joins the relations together and apply where clause on the joined relations:
Admin::Accbook
.joins(purchase: :offer)
.where(offers: { seller_id: 123 })
A thing to know, where uses the DB table's name. joins (and includes, eager_load, etc) uses the relation name. This is why we have:
Admin::Accbook
.joins(purchase: :offer)
# ^^^^^ relation name
.where(offers: { seller_id: 123 })
# ^^^^^^ table name
Try Adding following association in users.rb
has_many :accbooks, through: :purchases
So your problem is user is acting as 2 roles for same accounts. You can try something like below stuff
class User < ApplicationRecord
has_many :offers, foreign_key: :seller_id
has_many :purchases, foreign_key: :buyer_id
has_many :offers_purchases,
through: :offers,
:class_name => 'Purchase',
:foreign_key => 'offer_id',
:source => :purchases
end

Merge has_many and has_many through queries together

This seems like a typical problem, but it is difficult to search for.
I want to select projects that a user owns via a has_many and projects that a user is associated to via a has_many through.
Consider the following models:
class User < ActiveRecord::Base
has_many :projects,
inverse_of: :owner
has_many :project_associations,
class_name: 'ProjectUser',
inverse_of: :user
has_many :associated_projects,
through: :project_associations,
source: :project
end
class Project < ActiveRecord::Base
belongs_to :owner,
class_name: 'User',
foreign_key: :owner_id,
inverse_of: :projects
has_many :user_associations,
class_name: 'ProjectUser',
inverse_of: :project
has_many :associated_users,
through: :user_associations,
source: :user
end
class ProjectUser < ActiveRecord::Base
belongs_to :project,
inverse_of: :user_associations
belongs_to :user,
inverse_of: :project_associations
end
It is trivial to do this with multiple queries:
user = User.find(1)
all_projects = user.projects + user.associated_projects
But I suspect it could be optimised using Arel into a single query.
Edit:
My first attempt at a solution using the find_by_sql method is:
Project.find_by_sql([
'SELECT "projects".* FROM "projects" ' \
'INNER JOIN "project_users" ' \
'ON "projects"."id" = "project_users"."project_id" ' \
'WHERE "project_users"."user_id" = :user_id ' \
'OR "projects"."owner_id" = :user_id',
{ user_id: 1 }
])
This produces the result I am expecting, but I would like to avoid using find_by_sql and instead let Arel build the SQL.
This should work:
Project.joins(:user_associations)
.where("projects.owner_id = ? OR project_users.user_id = ?", id, id)
You could put that in a method called User#all_projects.
You could also use UNION, although it seems like overkill here.
Just one warning: In my experience this structure of two alternate paths to connect your models, where one path is a join table and the other is a direct foreign key, causes a lot of trouble. You're going to have to deal with both cases all the time, and in complex queries the OR can confuse the database query planner. If you're able to, you might want to reconsider. I think you will have fewer headaches if you remove the owner_id column and identify owners with a role column on project_users instead.
EDIT or in Arel:
Project.joins(:user_associations)
.where(Project.arel_table[:owner_id].eq(id).or(
ProjectUser.arel_table[:user_id].eq(id)))
You can use the includes directive as in:
User.includes(:projects, :associated_projects).find(1)
Check http://apidock.com/rails/ActiveRecord/QueryMethods/includes
Then if you call projects and associated_projects on the found instance you'll not fire additional queries.
I have my solution, the model structure remains the same, and I add a method on the User model called all_projects.
class User < ActiveRecord::Base
has_many :projects,
inverse_of: :owner
has_many :project_associations,
class_name: 'ProjectUser',
inverse_of: :user
has_many :associated_projects,
through: :project_associations,
source: :project
def all_projects
query = Project.arel_table[:owner_id].eq(id).or(ProjectUser.arel_table[:user_id].eq(id))
Project.joins(:user_associations).where(query)
end
end
Calling #all_projects builds and executes this query:
SELECT "projects".*
FROM "projects"
INNER JOIN "project_users" ON "project_users"."project_id" = "products"."id"
WHERE ("projects"."owner_id" = '1' OR "project_users"."user_id" = '1')
The solution isn't as elegant as I would like, ideally I would like to replace Project.arel_table[:owner_id].eq(id) with the query that is built from the has_many projects association, adding the .or(ProjectUser.arel_table[:user_id].eq(id)) onto it, but this works well for now.

Multiple Inner Joins with Polymorphic Association

I have the following polymorphic association...
class Activity < ActiveRecord::Base
belongs_to :owner, polymorphic: true
end
class User < ActiveRecord::Base
has_many :activities, as: :owner
end
class Manager < ActiveRecord::Base
has_many :activities, as: :owner
end
I am trying to make a query whereby it only pulls out the activities where the owner (user or manager) has visible set to true.
I have figured out that if I want to do this for one of the owners, I can do this as follows...
Activity.joins("INNER JOIN users ON activities.owner_id = users.id").where(:activities => {:owner_type => 'User'}).where(:users => {:visible => true})
But I cannot figure out how to do it for both. Can anyone help?
This should work:
Activity.
joins("LEFT JOIN users ON activities.owner_type = 'User' AND
activities.owner_id = users.id").
joins("LEFT JOIN managers ON activities.owner_type = 'Manager' AND
activities.owner_id = managers.id").
where("users.visible = ? OR managers.visible = ?", true, true)

Translating JOIN Query into Rails Active Record Associations

I have 3 different Models
class GroupMember < ActiveRecord::Base
attr_accessible :group_id, :user_id, :owner_id
has_one :user
end
class Group < ActiveRecord::Base
attr_accessible :name, :owner, :permission
has_many :groupMembers
end
class User < ActiveRecord::Base
end
and when im in the groups_controller.rb i want realize the following query with associations
SELECT * FROM groups
LEFT JOIN group_members ON groups.id = group_members.group_id
LEFT JOIN users ON group_members.user_id = users.id WHERE users.id = 1
or the joins query
Group.joins('LEFT JOIN group_members ON groups.id = group_members.group_id LEFT JOIN users ON group_members.user_id = users.id').where('users.id = ?', current_user.id );
Is this possible?
Group.joins(:user_id => current_user.id)
I believe it is possible but it is a bit difficult to test as soon as I don't have a DB with these tables. Probably something like:
Groups.joins(:group_members, :user).merge(User.where(:id => current_user.id))
But the design is a bit strange. I suppose you want to fetch all groups for a certain user, and it is best put like this:
#user = User.find_by_id(current_user.id)
#groups = #user.groups
for this to work you should have groups association in your User model.
It is also unclear for me why you have GroupMember has_one :user. I understand that group member is related to only one user, but can a user be represented as several group members, of different groups? If yes, the Rails way to design it would be:
class GroupMember < ActiveRecord::Base
attr_accessible :group_id, :user_id, :owner_id
belongs_to :user
belongs_to :group
end
class Group < ActiveRecord::Base
attr_accessible :name, :owner, :permission
has_many :group_members
has_many :users, :through => :group_members
end
class User < ActiveRecord::Base
has_many :group_members
has_many :groups, :through => :group_members
end

Resources