How this active record query could be optimized? - ruby-on-rails

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]]

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?

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)

Active record query for a query in rails

I am working on a rails app which has three models 'request', 'service' and 'price'. Request and services have a has_many_through relationship, and service and price have a has_one-belongs-to relationship i.e a service has one price and price belongs to service.
The model with relationship
class Request < ActiveRecord::Base
has_many :request_services
has_many :services, through: :request_services
end
class RequestService < ActiveRecord::Base
belongs_to :service
belongs_to :request
end
class Service < ActiveRecord::Base
has_many :request_services
has_many :requests, through: :request_services
has_one :price
end
class Price < ActiveRecord::Base
belongs_to :service
end
Now a user will create a new request which can have many services. So, I want to write a query to select the prices of the services that a user has selected in his request.
So far I have written this query,
1) To find the services in the request
Request.find(1).services
2) After this I want to get the prices of selected services, so I am writing
Request.find(1).services.prices
But this query is throwing error.
The error i am getting when running this query in rails console
Can anybody please help me in writing the query to fetch the prices of all the selected services
Request Load (0.2ms) SELECT "requests".* FROM "requests" WHERE "requests"."id" = ? LIMIT 1 [["id", 1]]
NoMethodError: Service Load (0.3ms) SELECT "services".* FROM "services" INNER JOIN "request_services" ON "services"."id" = "request_services"."service_id" WHERE "request_services"."request_id" = ? [["request_id", 1]]
undefined method `prices' for #<Service::ActiveRecord_Associations_CollectionProxy:0x007f4ec4c41bb0>
Maybe you can tackle it the other way around:
Price.joins(service: { request_services: :request}).where('requests.id = ?', request_id)
This gets all prices related to the request with the given id
You need to call Request.find(1).services.price (price is singular) since each service has only one price
Essentially your query states: "For this request (1) give me the Price of each related Service"
But chaining doesn't work that way. I'm unsure why you're using has_many-through called RequestService, but regardless, you have several options to get the price data for a request, a join, iterate-and-add-to-array, or the most simplistic add has-many-through on prices.
e.g.
Iterate & Add to Array
prices = []
Request.find(1).services.each { |s| prices << s.price }
Has Many Through
class Request < ActiveRecord::Base
has_many :request_services
has_many :services, through: :request_services
has_many :prices, through: :services
end
Which would let you do:
Request.find(1).prices

rails 3 has_many through has_one

Suppose you have the following models:
class Category < ActiveRecord::Base
has_one :current_heat, class_name: 'Heat'
has_many :scores, :through => :current_heat
end
class Heat < ActiveRecord::Base
belongs_to :category
has_many :scores
end
class Score < ActiveRecord::Base
belongs_to :heat
end
Surprisingly, when I invoke Category.first.scores ActiveRecord produces the following queries:
SELECT `categories`.* FROM `categories` LIMIT 1
SELECT * FROM `scores` INNER JOIN `heats` ON `scores`.`heat_id` = `heats`.`id` WHERE `heats`.`category_id` = 1
The above query ignores the has_one nature of Category#current_heat. I would have expected something more like:
SELECT `categories`.* FROM `categories` LIMIT 1
SELECT `heats`.* FROM `heats` WHERE `heats`.`category_id` = 1 LIMIT 1
SELECT * FROM `scores` WHERE `scores`.`heat_id` = 6
which is produced only when you explicitly traverse the has_one association from the root with Category.first.current_heat.scores.
It's as if ActiveRecord is silently treating my has_one as a has_many. Can someone explain this behavior to me? Is there an elegant workaround or a "right way" to do it?
Maybe you could remove the
has_many :scores, :through => :current_heat
and instead just delegate :scores through the has_one:
delegate :scores, :to => :current_heat
that would preserve your desired access method Category.first.scores.
has_one doesn't really exist to babysit your database in this fashion. It won't throw errors if there is more than one record that matches the foreign_key, it will just choose the first one. It assumes you haven't errantly added extra records which would break the has_one relation on your own.
In conclusion, the sql that it generates is fine as long as there is only one record attached to the Category. If somehow you've added extra records which shouldn't exist since it is a has_one, then it won't work, but it's not the job of activerecord to tell you that this has happened.

Rails active record query: Find records on one side of a has_many :through that haven't been used with a particular record on the other side

I have a has_many :through relationship set up like so
class Situation < ActiveRecord::Base
has_many :notifications
has_many :notiftypes, through: :notifications
end
class Notification < ActiveRecord::Base
belongs_to :situation
belongs_to :notiftype
end
class Notiftype < ActiveRecord::Base
has_many :notifications
has_many :situations, through: :notifications
end
So, a Situation has many Notifications, which can be of many types (Notiftype).
My problem is trying to query for the notiftypes that have not been set for a particular situation.
Want to find records with no associated records in Rails 3
The answers in that question get me close, but only to the point of finding Notiftypes that have not been set AT ALL.
If this were the standard :situation has_many :notiftypes I could just do a Left Outer Join like so
myquery = Notiftype.joins('LEFT OUTER JOIN situations ON situations.notiftype_id = notiftype.id').where('notiftype_id IS NULL')
but I'm really not sure how to do this with the intermediate table between them.
I have been trying consecutive joins but it's not working. I'm not sure how to join the two separated tables.
Can anyone explain the right way to query the db? I am using SQLite, Rails 3.1, Ruby 1.9.2 right now, but likely Postgresql in the future.
Try this:
class Situation < ActiveRecord::Base
# ...
has_many :notiftypes, through: :notifications do
def missing(reload=false)
#missing_notiftypes = nil if reload
#missing_notiftypes ||= proxy_owner.notiftype_ids.empty? ?
Notiftype.all :
Notiftype.where("id NOT IN (?)", proxy_owner.notiftype_ids)
end
end
end
Now to get the missing Notiftype
situation.notiftypes.missing
If you want to further optimize this to use one SQL rather than two you can do the following:
class Situation < ActiveRecord::Base
# ...
has_many :notiftypes, through: :notifications do
def missing(reload=false)
#missing_notiftypes = nil if reload
#missing_notiftypes ||= Notiftype.joins("
LEFT OUTER JOIN (#{proxy_owner.notiftypes.to_sql}) A
ON A.id = notiftypes.id").
where("A.id IS NULL")
end
end
end
You can access the missing Notifytypes as:
situation.notiftypes.missing

Resources