ActiveRecord join through two associations - ruby-on-rails

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?

Related

How do I get the records with exact has_many through number of entries on rails

I have a many to many relationship through a has_many through
class Person < ActiveRecord::Base
has_many :rentals
has_many :books, through rentals
end
class Rentals < ActiveRecord::Base
belongs_to :book
belongs_to :person
end
class Book < ActiveRecord::Base
has_many :rentals
has_many :persons, through rentals
end
How can I get the persons that have only one book?
If the table for Person is called persons, you can build an appropriate SQL query using ActiveRecord's query DSL:
people_with_book_ids = Person.joins(:books)
.select('persons.id')
.group('persons.id')
.having('COUNT(books.id) = 1')
Person.where(id: people_with_book_ids)
Although it's two lines of Rails code, ActiveRecord will combine it into a single call to the database. If you run it in a Rails console, you may see a SQL statement that looks something like:
SELECT "persons".* FROM "persons" WHERE "deals"."id" IN
(SELECT persons.id FROM "persons" INNER JOIN "rentals"
ON "rentals"."person_id" = "persons"."id"
INNER JOIN "books" ON "rentals"."book_id" = "books"."id"
GROUP BY persons.id HAVING count(books.id) > 1)
If this is something you want to do often, Rails offers what is called a counter cache:
The :counter_cache option can be used to make finding the number of belonging objects more efficient.
With this declaration, Rails will keep the cache value up to date, and then return that value in response to the size method.
Effectively this places a new attribute on your Person called books_count that will allow you to quite simply filter by the number of associated books:
Person.where(books_count: 1)

Multiple joins with count and having with ActiveRecord

My application is about Profiles that have many Wishes, that are related to Movies:
class Profile < ApplicationRecord
has_many :wishes, dependent: :destroy
has_many :movies, through: :wishes
end
class Wish < ApplicationRecord
belongs_to :profile
belongs_to :movie
end
class Movie < ApplicationRecord
has_many :wishes, dependent: :destroy
has_many :profiles, through: :wishes
end
I would like to return all the Movies that are all "wished" by profiles with id 1,2, and 3.
I managed to get this query using raw SQL (postgres), but I wanted to learn how to do it with ActiveRecord.
select movies.id
from movies
join wishes on wishes.movie_id = movies.id
join profiles on wishes.profile_id = profiles.id and profiles.id in (1,2,3)
group by movies.id
having count(*) = 3;
(I'm relying on count(*) = 3 because I have an unique index that prevents creation of Wishes with duplicated profile_id-movie_id pairs, but I'm open to better solutions)
At the moment the best approach I've found is this one:
profiles = Profile.find([1,2,3])
Wish.joins(:profile, :movie).where(profile: profiles).group(:movie_id).count.select { |_,v| v == 3 }
(Also I would begin the AR query with Movie.joins, but I didn't manage to find a way :-)
Since belongs_to puts the foreign key in the wishes table, you should be able to just query it for your profiles like so:
Wish.where("profile_id IN (?)", [1,2,3]).includes(:movie).all.map{|w| w.movie}
This should get you an array of all of the movies by those three profiles, eager loading the movies.
Since what I want from the query is a collection of Movies, the ActiveRecord query needs to start from Movie. What I was missing was that we can specify the table in the query, like where(profiles: {id: profiles_ids}).
Here it is the query I was looking for. (Yes, using count might sound a little bit brittle, but the alternative was an expensive SQL subquery. Also, I think it's safe if you're using a multiple-column unique index.)
profiles_ids = [1,2,3]
Movie.joins(:profiles).where(profiles: {id: profiles_ids}).group(:id).having("COUNT(*) = ?", profiles_ids.size)

How to combine two has_many associations for an instance and a collection in Rails?

I'm having trouble combining two has_many relations. Here are my associations currently:
def Note
belongs_to :user
belongs_to :robot
end
def User
has_many :notes
belongs_to :group
end
def Robot
has_many :notes
belongs_to :group
end
def Group
has_many :users
has_many :robots
has_many :user_notes, class_name: 'Note', through: :users, source: :notes
has_many :robot_notes, class_name: 'Note', through: :robots, source: :notes
end
I'd like to be able to get all notes, both from the user and the robots, at the same time. The way I currently do that is:
def notes
Note.where(id: (user_notes.ids + robot_notes.ids))
end
This works, but I don't know a clever way of getting all notes for a given collection of groups (without calling #collect for efficiency purposes).
I would like the following to return all user/robot notes for each group in the collection
Group.all.notes
Is there a way to do this in a single query without looping through each group?
Refer Active record Joins and Eager Loading documentation for detailed and efficient ways.
For example, You could avoid n+1 query problem here in this case as follows,
class Group
# Add a scope to eager load user & robot notes
scope :load_notes, -> { includes(:user_notes, :robot_notes) }
def notes
user_notes & robot_notes
end
end
# Load notes for group collections
Group.load_notes.all.notes
You can always handover the querying to the db which is built for such purposes. For example, your earlier query for returning all the notes associated with users and robots can be achieved by:
Notes.find_by_sql ["SELECT * FROM notes WHERE user_id IN (SELECT id FROM users) UNION SELECT * FROM notes WHERE robot_id IN (SELECT id FROM robots)"]
If you want to return the notes from users and robots associated with a given group with ID gid(say), you'll have to modify the nested sql query:
Notes.find_by_sql ["SELECT * FROM notes WHERE user_id IN (SELECT id FROM users WHERE group_id = ?) UNION SELECT * FROM notes WHERE robot_id IN (SELECT id FROM robots WHERE group_id = ?)", gid, gid]
Note:
If you want your application to scale then you may want as many DB transactions executed within a given period as possible, which means you run shorter multiple queries. But if you want to run as little queries as possible from ActiveRecord using the above mentioned method, then it will effect the performance of you DB due to larger queries.

ActiveRecord - nested includes

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.

How this active record query could be optimized?

There are these models:
Patient
Patient has_many MedicalOrders
MedicalOrder
MedicalOrder belongs_to Patient
MedicalOrder has_many Tasks
Task
Task belongs_to MedicalOrder
Task has_many ControlMedicines
ControlMedicine
ControlMedicine belongs_to Task
And there's this block of code to get the actual #patient's control_medicines:
def index
#control_medicines = []
#patient.medical_orders.each do |mo|
mo.tasks.order(created_at: :desc).each do |t|
t.control_medicines.each do |cm|
#control_medicines << cm
end
end
end
end
I know it's not the best way to query associated models but haven't figured out how to do it using .includes() method. Mostly because .includes() only works being called to a Class (eg, Patient.includes()) and they're not suitable for nested models, like in this situation.
I've read about preloading, eager_loading and includes but all the examples are limited to get data from two associated models.
You can use the has_many through in Rails to allow ActiveRecord to make your joins for you.
class Patient < ActiveRecord::Base
has_many :medical_orders
has_many :tasks, through: :medical_orders
has_many :control_medicines, through: :tasks
end
Writing your query like:
#patient.control_medicines
Generates SQL like:
SELECT "control_medicines".* FROM "control_medicines"
INNER JOIN "tasks" ON "tasks"."id" = "control_medicines"."task_id"
INNER JOIN "medical_orders" ON "medical_orders"."id" = "tasks"."medical_order_id"
WHERE "medical_orders.patient_id" = $1 [["id", 12345]]

Resources