ActiveRecord - nested includes - ruby-on-rails

I'm trying to perform the following query in Rails 5 in a way that it doesn't trigger N+1 queries when I access each events.contact:
events = #company.recipients_events
.where(contacts: { user_id: user_id })
I tried some combinations of .includes, .references and .eager_loading, but none of them worked. Some of them returned an SQL error, and other ones returned a nil object when I access events.contact.
Here's a brief version of my associations:
class Company
has_many :recipients
has_many :recipients_events, through: :recipients, source: :events
end
class Recipient
belongs_to :contact
has_many :events, as: :eventable
end
class Event
belongs_to :eventable, polymorphic: true
end
class Contact
has_many :recipients
end
What would be the correct way to achieve what I need?

If you already know user_id when you load #company, I'd do something like this:
#company = Company.where(whatever)
.includes(recipients: [:recipients_events, :contact])
.where(contacts: { user_id: user_id })
.take
events = #company.recipients_events
OR, if not:
events = Company.where(whatever)
.includes(recipients: [:recipients_events, :contact])
.where(contacts: { user_id: user_id })
.take
.recipients_events
The ActiveRecord query planner will determine what it thinks is the best way to get that data. It might be 1 query per table without the where, but when you chain includes().where() you will probably get 2 queries both with left outer joins on them.

Related

ActiveRecord join through two associations

How do I get an ActiveRecord collection with all the unique Jobs that exist for a particular User given that there are two possible associations?
class User
has_many :assignments
has_many :jobs, through: assignments
has_many :events
has_many :jobs_via_events, through: :events, source: :jobs
end
class Assignment
belongs_to :user
belongs_to :job
end
class Event
belongs_to :user
belongs_to :job
end
class Job
has_many :assignments
has_many :events
end
The best I could come up with so far is using multiple joins, but it isn't coming back with correct results:
Job.joins("INNER JOIN assignments a ON jobs.id = a.job_id").joins("INNER JOIN events e ON jobs.id = e.job_id").where("a.user_id = ? OR e.user_id = ?", userid, userid)
Any advice?
A simple approach is to collect all job-ids first and then fetch all jobs for those ids.
We first ask for an array of all job-ids for a user:
event_job_ids = Event.where(user_id: user).select(:job_id).map(&:job_id)
assignment_job_ids = Assignment.where(user_id: user).select(:job_id).map(&:job_id)
all_job_ids = (event_job_ids + assignment_job_ids).uniq
note that, depending on the rails version you use, it is better to replace the select(:job_id).map(&:job_id) by a .pluck(:job_id) (which should be more efficient, but the select.map
Getting the jobs for the assembled ids is then straightforward:
jobs = Job.where(id: all_job_ids)
This is the naive approach, how can we approve upon this?
If I would write a query I would write something like the following
select * from jobs
where id in (
select job_id from assignments where user_id=#{user_id}
union
select job_id from events where user_id=#{user_id}
)
so how do we convert this to a scope?
class User
def all_jobs
sql = <<-SQL
select * from jobs
where id in (
select job_id from assignments where user_id=#{self.id}
union
select job_id from events where user_id=#{self.id}
)
SQL
Job.find_by_sql(sql)
end
Of course this is untested code, but this should get you started?

Rails: Creating/updating has_many relationships for existing has_many records

Given:
class Group < ApplicationRecord
has_many :customers, inverse_of: :group
accepts_nested_attributes_for :customers, allow_destroy: true
end
class Customer < ApplicationRecord
belongs_to :group, inverse_of: :customers
end
I want to create/update a group and assign existing customers to the group e.g.:
Group.new(customers_attributes: [{ id: 1 }, { id: 2 }])
This does not work though because Rails will just throw ActiveRecord::RecordNotFound: Couldn't find Customer with ID=1 for Group with ID= (or ID=the_group_id if I'm updating a Group). Only way I've found to fix it is just extract customers_attributes and then do a separate Customer.where(id: [1,2]).update_all(group_id: 'groups_id') after the Group save! call.
Anyone else come across this? I feel like a way to fix it would be to have a key like _existing: true inside customers_attributes (much like _destroy: true is used to nullify the foreign key) could work. Or does something like this violate a Rails principle that I'm not seeing?
Actually, you don't need to use nested attributes for this, you can instead set the association_ids attribute directly:
Group.new(customer_ids: [1, 2])
This will automatically update the group_id on each referenced Customer when the record is saved.

How can I join across a has_many, a has_many :through and a HABTM?

I'd like to be able to write a single query to find all Messages a user has created, viewed or received.
This is a simplified version of my User class:
class User < ActiveRecord::Base
# user_id
has_many :messages
# message_viewer_id
has_many :message_views, foreign_key: 'message_viewer_id'
has_many :viewed_messages, through: :message_views
# user_id
has_and_belongs_to_many :received_messages,
join_table: 'messages_recipients',
class_name: 'Message'
end
I've tried many variations of the following using .where and .conditions , but keep getting errors like PG::UndefinedTable: ERROR: missing FROM-clause entry for table "received_messages"
User.
joins(:messages,
:message_views,
:received_messages).
where({messages: {user_id: id},
message_views: {message_viewer_id: id},
received_messages: {user_id: id}})
(I can join :messages and :message_views without issue, but the query breaks when I introduce :received_messages. (Yes, I know I shouldn't have used HABTM.))
What it the simplest and/or most efficient way to achieve this?
Try something like this:
class User < ActiveRecord::Base
...
def all_messages
Message.where("user_id = ? OR id IN (SELECT message_id FROM message_views WHERE message_viewer_id = ?) OR id IN (SELECT message_id FROM messages_recipients WHERE user_id = ?)", self.id, self.id, self.id)
end
...
end
I would recommend putting indexes on user_id on messages, message_viewer_id on message_views, and user_id on messages_recipients, if you haven't already.

Complex has_many :through with block conditions

I'm having a bit of difficulty figuring out how to do this in the "Rails" way, if it is even possible at all.
Background: I have a model Client, which has a has_many relationship called :users_and_managers, which is defined like so:
has_many :users_and_managers, -> do
Spree::User.joins(:roles).where( {spree_roles: {name: ["manager", "client"]}})
end, class_name: "Spree::User"
The model Users have a has_many relationship called credit_cards which is merely a simple has_many - belongs_to relationship (it is defined in the framework).
So in short, clients ---has many---> users ---has many---> credit_cards
The Goal: I would like to get all the credit cards created by users (as defined in the above relationship) that belong to this client.
The Problem: I thought I could achieve this using a has_many ... :through, which I defined like this:
has_many :credit_cards, through: :users_and_managers
Unfortunately, this generated an error in relation to the join with the roles table:
SQLite3::SQLException: no such column: spree_roles.name:
SELECT "spree_credit_cards".*
FROM "spree_credit_cards"
INNER JOIN "spree_users" ON "spree_credit_cards"."user_id" = "spree_users"."id"
WHERE "spree_users"."client_id" = 9 AND "spree_roles"."name" IN ('manager', 'client')
(Emphasis and formatting mine)
As you can see in the generated query, Rails seems to be ignoring the join(:roles) portion of the query I defined in the block of :users_and_managers, while still maintaining the where clause portion.
Current Solution: I can, of course, solve the problem by defining a plain 'ol method like so:
def credit_cards
Spree::CreditCard.where(user_id: self.users_and_managers.joins(:credit_cards))
end
But I feel there must be a more concise way of doing this, and I am rather confused about the source of the error message.
The Question: Does anyone know why the AR / Rails seems to be "selective" about which AR methods it will include in the query, and how can I get a collection of credit cards for all users and managers of this client using a has_many relationship, assuming it is possible at all?
The joins(:roles) is being ignored because that can't be appended to the ActiveRecord::Relation. You need to use direct AR methods in the block. Also, let's clean things up a bit:
class Spree::Role < ActiveRecord::Base
scope :clients_and_managers, -> { where(name: %w{client manager}) }
# a better scope name would be nice :)
end
class Client < ActiveRecord::Base
has_many :users,
class_name: "Spree::User",
foreign_key: :client_id
has_many :clients_and_managers_roles,
-> { merge(Spree::Role.clients_and_managers) },
through: :users,
source: :roles
has_many :clients_and_managers_credit_cards,
-> { joins(:clients_and_managers_roles) },
through: :users,
source: :credit_cards
end
With that setup, you should be able to do the following:
client = # find client according to your criteria
credit_card_ids = Client.
clients_and_managers_credit_cards.
where(clients: {id: client.id}).
pluck("DISTINCT spree_credit_cards.id")
credit_cards = Spree::CreditCard.where(id: credit_card_ids)
As you can see, that'll query the database twice. For querying it once, check out the following:
class Spree::CreditCard < ActiveRecord::Base
belongs_to :user # with Spree::User conditions, if necessary
end
credit_cards = Spree::CreditCard.
where(spree_users: {id: client.id}).
joins(user: :roles).
merge(Spree::Role.clients_and_managers)

Rails ActiveRecord how to order by a custom named association

Ok so have created 2 models User and Following. Where User has a username attribute and Following has 2 attributes which are User associations: user_id, following_user_id. I have set up these associations in the respective models and all works good.
class User < ActiveRecord::Base
has_many :followings, dependent: :destroy
has_many :followers, :class_name => 'Following', :foreign_key => 'following_user_id', dependent: :destroy
end
class Following < ActiveRecord::Base
belongs_to :user
belongs_to :following_user, :class_name => 'User', :foreign_key => 'following_user_id'
end
Now I need to order the results when doing an ActiveRecord query by the username. I can achieve this easily for the straight-up User association (user_id) with the following code which will return to me a list of Followings ordered by the username of the association belonging to user_id:
Following.where(:user_id => 47).includes(:user).order("users.username ASC")
The problem is I cannot achieve the same result for ordering by the other association (following_user_id). I have added the association to the .includes call but i get an error because active record is looking for the association on a table titled following_users
Following.where(:user_id => 47).includes(:user => :followers).order("following_users.username ASC")
I have tried changing the association name in the .order call to names I set up in the user model as followers, followings but none work, it still is looking for a table with those titles. I have also tried user.username, but this will order based off the other association such as in the first example.
How can I order ActiveRecord results by following_user.username?
That is because there is no following_users table in your SQL query.
You will need to manually join it like so:
Following.
joins("
INNER JOIN users AS following_users ON
following_users.id = followings.following_user_id
").
where(user_id: 47). # use "followings.user_id" if necessary
includes(user: :followers).
order("following_users.username ASC")
To fetch Following rows that don't have a following_user_id, simply use an OUTER JOIN.
Alternatively, you can do this in Ruby rather than SQL, if you can afford the speed and memory cost:
Following.
where(user_id: 47). # use "followings.user_id" if necessary
includes(:following_user, {user: :followers}).
sort_by{ |f| f.following_user.try(:username).to_s }
Just FYI: That try is in case of a missing following_user and the to_s is to ensure that strings are compared for sorting. Otherwise, nil when compared with a String will crash.

Resources