Rails "where" method to find parent by child attribute - ruby-on-rails

I have a Rails app where I'm trying to create a list of parent classes based on the date of their child classes.
right now I have:
orders = Order.where("order_reminders.date < ?", 1.month.from_now)
But I'm receiving an error.
"no such column: order_reminders.date"
I'm sure my problem is with my query but I'm not sure how to fix it.

You need to use methods like references, joins, includes or eager_load depending on what you need. The simplest way to determine what to select is to go to the documentation/api and read them.
As for the answer, if order_reminders is defined as an association in Order, just use joins which uses an INNER JOIN by default, meaning it won't return orders without order_reminders which is most probably what you want when you're searching for something.
Order.joins(:order_reminders).where('order_reminders.date < ?', 1.month.from_now)

Is order_reminders another table in your app?
If order has_many order_reminders then try this query
Order.joins(:order_reminders).where("order_reminders.date < ?", 1.month.from_now)

Related

What is the "Rails Way" to eager loading mutliple associations with multiple aggregations?

I have a simple rails application for a competition with these models: User, Team, and Trip. The associations are: team has_many users, and user has_many trips. Trips has a distance_miles field.
I'm am working on a simple view to display a table of teams with their stats that have multiple aggregations.
My initial approach was the following but results in N+1 queries.
#teams.each do |team|
#team.user.joins(:trips).count()
#team.user.joins(:trips).sum(:distance_miles)
end
The following works, but seems ugly as I want to add pagination, sorting and filtering eventually.
#teams = Team.left_joins(users: :trips)
.select('teams.id, teams.name, SUM(trips.distance_miles) as num_miles, COUNT(trips.id) as num_trips')
.group(:id)
I've been reading preload and includes but cant seem to get it to also get multiple aggregations. The following gets me part way there, but it is now missing fields from Team and still need the other aggregation:
#teams = Team.includes(users: :trips).group(:id).sum('trips.distance_miles')
Is there a "rails way" that I'm missing?
ActiveRecord::Calculations which provides .sum, .count is only really useful in very simple cases as it always creates a separate database query.
You're on the right track with .select but it could be cleaned up by extracting the code into the model:
class Team
def self.with_trips
trips = Trip.arel_table
self.left_joins(users: :trips)
.select(
self.arel.projections, # adds the existing select clause
trips[:distance_miles].sum.as(:num_miles),
trips[:id].count.as(:distance_miles)
)
.group(:id)
end
end
Using .eager_load instead of .left_joins will result in a PG::GroupingError on Postgres as the results would be ambiguous. Instead you need use a window function.

Can I use scopes on an included model?

I have scopes defined on my Job model, and I want to use them when including jobs in an Active Record query, rather than writing out long-hand the conditions and ordering.
Here is the code I have that works but is very verbose:
#employees_and_jobs = supervisor.direct_reports.alphabetical \
.includes(:jobs) \
.where('jobs.active = true') \
.order('jobs.record_number asc, jobs.effective_date asc')
Here is the code I wish would work:
#employees_and_jobs = supervisor.direct_reports.alphabetical.includes(:jobs).active.sorted
The scopes direct_reports and alphabetical work, but the others (active and sorted) are interpreted as belonging to the Employee model, and give me an error. I want active and sorted to be interpreted as belonging to the Job model. How can I change the query to show that active and sorted are scopes for Job and not Employee?
The active and sorted scopes are of course defined on the Job model, and are done with an explicit reference to jobs (but of course that is not enough):
scope :sorted, -> { order('jobs.record_number asc, jobs.effective_date asc') }
scope :active, -> { where('jobs.active = true') }
(I didn't expect the explicit reference to jobs inside the scope to make it work, but I tried it just in case, and mention it in case someone else thinks it might work.)
How can I specify in my query that the final scopes are meant to apply to the included jobs, and not to the employees?
(I realize I can solve the problem with a default scope, but that can create new problems later, and I'm trying to avoid that. I would prefer the verbose version above over using a default scope.)
Similar (But Different) Questions
The answers to this question don't answer my question, but simply instead offer an alternative approach to dealing with the situation. (But I already have an alternative approach, given above. I have working code, but I'm trying to improve readability in a very particular way by using scopes on not just the main model but also the included model.)
I'm asking for a way to use scopes on the included model but those answers explain how to use a scope on the main model that in turn includes the other model. Two very different things. They are similar in that they both make the controller code simpler but the other approach makes the controller potentially less clear. It just moving all of the complexity into a single scope which would (in my case) be on the Employee model. I'm aiming to have have very specific scopes that I can compose together, which each have a very clear and clearly defined purpose.
scope is really just syntactic sugar for defining class methods. So like any other class method you can just call your scopes on the class which defines them:
class Job < ApplicationRecord
belongs_to :employee
scope :active, -> { where(active: true) }
scope :sorted, -> { order('jobs.record_number asc, jobs.effective_date asc') }
end
class Employee < ApplicationRecord
has_many :jobs
scope :with_active_jobs, ->{ include(:jobs).merge(Job.active).merge(Job.sorted) }
end
ActiveRecord::SpawnMethods#merge is probably one of the most underused features of AR. It lets you mash different scopes together programatically.
ActiveRecord is smart enough to specify the table with .where so there is not problem in using it in a join (.where('jobs.active = true') will also work fine too). Unfortunately .order is not as smart and .order(record_number: :asc, effective_date: :asc) will generate ORDER BY record_number ASC, effective_date ASC which will give an error.
There is no technical reason you have to do this in the model either. You can just do Employee.include(:jobs).merge(Job.active).merge(Job.sorted) or whatever in the controller if you want to compose the scopes there. But remember that controllers are really difficult to test compared to models.

"Index" page to display all items associated with current_user using '.where' but result is ActiveRecord, and I need to run each do on it

Sorry for the long title, I wanted to make sure I was being as specific as possible. This is my first question so please be patient!
I am building an index page that displays all the items in the consultations model that are associated with the current user. This used to be easy when I set #consultations = Consultation.where(user_id: current_user.id) in my controller. However, because consultations should be able to have more than one consultant assigned to each one, I changed the Consultation.user_id field to be a string array (I wasn't able to change to an integer array). I also connected the User and Consultation models via has_and_belongs_to_many and a join table.
I am now trying to query them with #consultation = Consultation.where {|a| a.user_id == ['current_user.id']} so that I can display all consultations in which the Consultation.user_id array contains the current_user.id.
This works as expected in pry, but on the actual view page I'm getting an error: NoMethodError: undefined method /'each/' for #<ActiveRecord::QueryMethods::WhereChain:0x007f82c23fe840>.
I understand that this is to be expected, since .where returns an ActiveRecord, which does not support methods such as each do, which is what I'm using on the index page to show all the consultations.
Any idea on how I can properly display all the consultations associated with the current_user.id? Thanks in advance.
you can do it like
#consultations = current_user.consultations
use associations instead of query
BTW
#consultation = Consultation.where {|a| a.user_id == ['current_user.id']}
this will not work because 'current_user.id' is string, it isn't id
it means that you try find all consultations where user_id == 'current_user.id'.
Do you see what I mean?
It sounds like you have a many-to-many relationship between consultants and clients. You can solve this by having a join table in the middle that holds both the client_id and the consultant_id. This would be a 3rd model to the consultant model and client models that you already have. You shouldn't try to store arrays as fields in ActiveRecord. ActiveRecord is just helping you organize data that is being written to a database.
Once you have a join table with both foreign_id's you can easily set up has_many relationships for both the client and consultants and then a has_may_through association. Then you won't need to worry about any #where calls because you can just call the associations.
The problem is that the syntax where you pass a block to where is something that you've just made up. Because no actual arguments are passed to where you don't get an ActiveRecord::Relation
You could change relationship between consultation and users to be has many through. If that join model was called assignments, then you could write a query such as
Consultation.joins(:assignments).where(assignments: {user_id: current_user.id})
If you keep your current approach with a user_id array column then, assuming this is Postgres, you can use the array query methods:
Consultation.where("user_id #> ARRAY[?]", current_user.id)

Order Rails Active Record by Count in Another Table

I have a comment model which has likes associated with it. I keep the likes in a separate table and the comment model has_many likes. However, I want to order the comments by most popular and I do not know how to do an active directory query to return the order I want. I have tried:
Comment.order(:likes.count)
This isn't working. Any advice?
This is working for me currently with Rails 5:
Comment.joins(:likes)
.select('comments.*, count(likes) as like_count')
.group('comments.id')
.order('like_count desc')
It even adds a like_count attribute to each returned comment, so you can call comment.like_count.
Try this:
Comment.joins(:likes).order('count(likes.*) desc')

Rails scope / class method to select items where association is present

Can't seem to wrap my head around this problem. I have a message model below
Message
# content:string
# original_id:integer
# sender_id:integer
# receiver_id:integer
has_one :reply, class_name: "Message", foreign_key: "original_id"
belongs_to :original, class_name: "Message"
Each message can only have one reply and the reply message will have its corresponding original message.
What I'd like to do is create a scope or a class method that allows me to pull replied messages in one batch and unreplied messages in another.
Something like
# return messages that have a reply present
def self.replied
where(reply.present?)
end
# return messages that have no reply
def self.unreplied
where(reply.nil?)
end
so I can chain the methods and ultimately pull messages with
user1.messages.replied
It doesn't currently work because I can't use the where clause unless it's a DB column...so I was thinking about adding a "replied" boolean column into the DB so I could use the where clause but there's probably a solution to this that I'm just not thinking about. A scope with a lambda? I'm stuck right now.
Any help much appreciated
To find those that have been replied is fairly straightforward:
scope :replied, joins(:reply)
as anything without a reply will be filtered out with an INNER JOIN. To find those without replies is a bit more complex - you can either use a LEFT JOIN or an EXISTS subquery to accomplish this. includes is a simple way to force a LEFT JOIN:
scope :unreplied, includes(:reply).
where(replies_messages: {id: nil}).
where(original_id: nil)
An EXISTS subquery may be somewhat more efficient, but more complex to write (at this time), as it would involve invoking Arel tables (or Squeel). For most cases a LEFT JOIN would be 'good enough', and includes is a quick-and-dirty way to force the API to use one.

Resources