Rails and Postgres: Getting the latest entry for each customer - ruby-on-rails

I have a model (which has been simplified here) that looks like this:
class Customer < ActiveRecord::Base
attr_accessor :name
has_many :orders
end
class Order < ActiveRecord::Base
attr_accessor :description, :cost, :date
end
I'm using Postgres – what's the best way to run a query that will return me a single result that contains the latest order for each customer?
If I do something like this:
Order.order('date DESC').group('customer')
Then I get a Postgres error:
PGError: ERROR: column must appear in the GROUP BY clause or be used in an aggregate function
What's the cleanest way to do this? I'm a Rails newbie, so let me know if I've left out any vital information here.

as question says Getting the latest entry for each customer
Customer.all.map{|C| [c.name, c.orders.last]}
will return an array with customer name and latest order. In view it will look like this:
<% #customers.each do |c| %>
<%= c.name %> - <%= c.orders.last.cost %>
<% end %>
result:
John Travolta - $5454
Johnny Depp - $6849

Order.order('date DESC').group('customer_id')
I guess your Order model does not have customer field by convention it should be customer_id

For a more SQL oriented method, which ought to be better performing:
Order.where("id in (select max(id) from orders group by customer_id)")
It relies on the most recent order having the highest id.

Related

Filter in Rails a table with information of another table that belongs_to

I have a report that belongs_to a machine, and the machine belongs_to a client.
I have a page where I see all the reports.
#report_controller.rb
def index
#reports = Report.all
end
#index.html.erb
<% #reports.each do |report| %>
<tr>
<td><%= report.machine.client.name %></td>
<td><%= report.machine.code %></td>
<td><%= report.observations %></td>
</tr>
<% end %>
Now I need to show all the reports of a particular Client. If in the controller I do
#reports = Report.where(observations: "xxx")
works fine, but I need filter by the client name something like
#reports = Report.Machine.Client.where(name: "Joe")
I try similar things, read about scopes and joins but I Can't find something that work.
PS: I have Pundit install maybe there's an approach with policies
ASSUMING (since your question leaves us guessing at table structure) that a Report belongs to a Machine (reports table has machine_id), and a Machine belongs to a Client (machines table has a client_id), and you want to get the client by something like :name you could do something like:
Report.joins(machine: :client).where(clients: {name: 'Joe'})
Will give you SQL like:
SELECT "reports".*
FROM "reports"
INNER JOIN "machines"
ON "machines"."id" = "reports"."machine_id"
INNER JOIN "clients"
ON "clients"."id" = "machines"."client_id" # Note joins to the previous join
WHERE "clients"."name" = 'Joe'
The syntax of this query is different than
Report.joins(:machine, :client).where(clients: {name: 'Joe'})
which will give you:
SELECT "reports".*
FROM "reports"
INNER JOIN "machines"
ON "machines"."id" = "reports"."machine_id"
INNER JOIN "clients"
ON "clients"."id" = "reports"."client_id" # Joins on the original table of reports
WHERE "clients"."name" = 'Joe'
Very similar rails code but the first one follows the query of client -> machines -> reports and the second one is expecting a client_id column on your reports, which you may or may not have. You probably don't. But if you had some OTHER table that had a foreign key in the reports table, you would use the second syntax to do inner joins on the two different tables.
Also note that in your original code you are doing:
<% #reports.each do |report| %>
<tr>
<td><%= report.machine.client.name %></td>
<td><%= report.machine.code %></td>
<td><%= report.observations %></td>
</tr>
<% end %>
Every time you call each of the <%= report.machine.client.name %> you will execute three queries. And then the next line report.machine.code will execute two queries. So multiply that times a 1000 reports and you've got thousands of queries. If cache that table info in the original query you can get rid of all those extra queries:
#reports = Report.all.includes(machine: :client)
You will get 3 queries loaded at once and every time that loop processes no new queries will get fired. Will scale much better.
The same is true of your original question. The inner join query is going to return a list of reports and every time you call something like the loop above you'll be executing several queries. So you should switch to includes:
Report.includes(machine: :client).where(clients: {name: 'Joe'})
The correct way to handle this is to setup indirect assocations so that you don't need to violate the Law of Demeter:
class Report < ApplicationRecord
belongs_to :machine
has_one :client, though: :machine
end
class Client < ApplicationRecord
belongs_to :machine
has_many :reports, through: :machine
end
You can then do a LEFT INNER JOIN to filter it down to reports that have a match in the joined table:
Report.joins(:client)
.where(clients: { name: 'Joe' })
Now I need to show all the reports of a particular Client.
You're most likely approaching this wrong. Instead of identifying the client by name you should by passing around an id and use the association off the record.
The Rails way to do this unless you want to display the reports on the clients#show page is to create a nested route:
resources :clients do
resources :reports,
module: :clients,
only: [:index]
end
# app/controllers/clients/reports_controller.rb
module Clients
# Handles reports for a specific client
class ReportsController < ApplicationController
before_action :set_client
# Displays the clients reports
# GET /clients/1/reports
def index
#reports = #client.reports
end
private
def set_client
#client = Client.eager_load(:reports)
.find(params[:client_id])
end
end
end
Using a separate controller and not just ::ReportsController is optional but helps separate the concerns if you also have a unnested /reports route which shows all the reports. You can then link to this with:
<%= link_to "Show Reports", [client, :reports] # this assumes that there is a 'client' local %>

Rails check_box_tag - save array into Postgres

Looks like my form does what is expected to do - sends the right value, but its not being saved in db. Check_box_tag takes data from enum (I use enum because I use same data for select field):
class UserProfile < ApplicationRecord
enum locations: { Kursenai: 0, Papiskes: 1, Versiai: 2 }
And in form_for:
<% UserProfile.locations.each do |key, val| %>
<%= f.label key %>
<%= check_box_tag('user_profile[locations][]', val, false, id: val) %>
<% end %>
But it fails to update:
'["0", "1"]' is not a valid locations
Postgres:
t.integer "locations", array: true
So I thought it fails because row type is integer, but this:
<%= check_box_tag('user_profile[locations][].to_i', val.to_i, false, id: val) %>
removed error but user field :locations is still nil. What do I miss?
Strong params:
..permit(locations: [])
p.s. if you think this could be done in a better way - please feel free to show.
Why?
Because '["0", "1"]' is considered as string and it is not among values you mentioned in enum i.e 0,1,2.
You can't achieve it directly as enum requires field type to hold single value.But in your case it's an array.
How to achieve?
class UserProfile < ApplicationRecord
# value should store as array not as string.
serialize :locations, Array
# define your own enum by creating static var.U can use Array or Hash structure.
# Here I am using Hash.
# ENUM_LOCATIONS = ["Kursenai", "Papiskes", "Versiai"]
ENUM_LOCATIONS = {"Kursenai": 0, "Papiskes": 1, "Versiai": 2}
# Now modify you getter little bit to return enumed values
def locations
res = []
self[:locations].each{|v| ENUM_LOCATIONS.is_a?(Array) ? res << ENUM_LOCATIONS[v.to_i] : res << ENUM_LOCATIONS.key(v.to_i).to_s}
res
end
end
That's it.
Why are you using enum? I think it's better to create a new model Location and connect with UserProfile via HABTM relation. It would fullfill the Database normalization and easier to work with.
Edit:
class UserProfile < ApplicationRecord
has_and_belongs_to_many :locations
end
class Location < ApplicationRecord
has_and_belongs_to_many :user_profiles
end
and you need to create 3 location records
Location.create(name: 'Kursenai')
Location.create(name: 'Papiskes')
Location.create(name: 'Versiai')
Use any standart queries, joins. You can built a form like here:
Rails 4 - checkboxes for has_and_belongs_to_many association
or Multiple select issue with a HABTM relationship using Rails 4

Hash with 'nil' value when using `group` and `count` in query

In Rails, I get a hash using includes:
<% #teste = UserProfile.includes(:mobile_models).group(:name).count %>
The problem is that includes generates a hash like the following:
{nil=>4774, "2610"=>7, "2626"=>4, "2630"=>5, "2760"=>4, "3250"=>3, "355"=>5, "3I607 BlackJack"=>5, "5230"=>13, "5235"=>4, "5310"=>5, "5500"=>5, "5800 Xpress Music"=>16, "6020"=>4, "6120c"=>4, "6131"=>4, "7210"=>5, "A1200r"=>5, "A1900"=>5, "AIKO 70"=>5, "B3410W Ch#t"=>4, "beTouch E100"=>4, "BlackBerry 8320 (Curve)"=>10,....
In my database, I don't find any mobile record with the name "nil". Checking my database, I can't find what might be producing this nil.
The other goal is to sum all values, like this:
<%= sum = #teste.values.sum %>
But when I do this, the 'nil' is added too.
---Update
models/UserProfile
class UserProfile < ActiveRecord::Base
has_and_belongs_to_many :mobile_models, join_table: 'user_profiles_mobile_models', order: 'name'
models/MobileModel
class MobileModel < ActiveRecord::Base
belongs_to :mobile_maker
Because you are grouping by :name, some of the MobileModel or UserProfile objects have the name attribute set to nil. You will need to check both as without seeing the model definition, I can't tell which model has the :name property you are grouping on. If you can share the model code, I can be more explicit.
If both models have a name attribute, you can be more explicit in your group statement:
UserProfile.includes(:mobile_models).group('mobile_models.name')
or...
UserProfile.includes(:mobile_models).group('user_profiles.name')
Also, if a number of your users do not have any mobile_models to include, I believe they will get dumped into the nil grouping as well.
You are getting that hash because of group(:name).
That means you have 4774 records who's name is nil.

How to group an object by its associated model when using a has_many :through relationship in rails?

I am working on an application that helps a local restaurant track the hours worked each day. The user enters data through a model called a "Checkout" at the end of each shift. They select an employee that is associated with a has_many :through => employment, where the employment model is a join table. Granted, Checkout is a confusing term, but that is the DSL that the restaurant wanted to use.
How do you group the hours worked by each employee on a given day? There might be 2 checkouts in the database for that employee on that day – one from lunch and one from dinner.
I want to create a list of hours that each employee worked on a given day when their hours from that day might be stored in separate checkouts in the database.
Today's Date
Joe Employee
Hours worked: 12
Jill Employee
Hours worked: 4
etc.
How do I group the checkouts by employee when they are not an attribute on the checkouts model, but rather are an association through my employment model? Essentially, I'm trying to do something like this in my reports helper:
def checkouts_today
current_user.checkouts.where( :date => Date.today )
end
def employee_hours_today
checkouts_today.where( :employment => Employee.find_by(id: params[:id]) ).sum(:hours)
end
But I can't get it to work with the has_many :through association. I can only figure out how to total all hours from that day, not the total hours per employee.
Here are the relevant files:
models/checkout.rb - https://gist.github.com/leemcalilly/a2a0578adaf8841d4d5e
models/employee.rb - https://gist.github.com/leemcalilly/2df5e7fb7db0ac0f602c
models/employment.rb - https://gist.github.com/leemcalilly/d3a028b6effe5f245b2a
helpers/reports_helper.rb - https://gist.github.com/leemcalilly/e2503188bb26df64ad20
view/payroll_processing.html.erb - https://gist.github.com/leemcalilly/868b1272128e75dc60d0
db/schema.rb - https://gist.github.com/leemcalilly/f9ce764d16161b7b3017
Update:
I can loop through each employee and display their checkouts with this:
<% #employees.each do |employee| %>
<% if employee.checkouts.present? %>
<p><%= employee.full_name %></p>
<% employee.checkouts.each do |checkout| %>
<%= checkout.date %>
<%= checkout.shift %>
<%= checkout.hours %>
<% end %>
<% end %>
<% end %>
But, I can't figure out how to sum their hours and display and return it in one block. It returns each checkout for each day under each employee's name. It also seems like this kind of logic should not be in the view.
1st method
in checkout.rb:
scope :checkouts_today, -> {where(date: Date.today)}
in your view or controller:
employee.checkouts.checkouts_today.sum(:hours)
2nd method
in employee.rb
has_many :today_checkouts, class_name: "Checkout", conditions: {date: Date.today}
in your view or controller:
employee.today_checkouts.sum(:hours)
Not sure the 2nd way would work as you have a join table to get the relation between Employee and Checkout. As we discussed in chat, you can drop the join table, in your case I don't see where you'll need it, give your app more space.

rails order by association through another association?

As a simple example, let's say a bookstore has books which have one author. The books has many sales through orders. Authors can have many books.
I am looking for a way to list the authors ordered by sales. Since the sales are associated with books, not authors, how can I accomplish this?
I would guess something like:
Author.order("sales.count").joins(:orders => :sales)
but that returns a column can't be found error.
I have been able to connect them by defining it in the Author model. The following displays the correct count for sales, but it does ping the database for each and every author... bad. I'd much rather eager load them, but I can't seem to get it to work properly since it will not list any authors who happen to have 0 sales if I remove the self.id and assign the join to #authors.
class Author < ActiveRecord::Base
def sales_count
Author.where(id: self.id).joins(:orders => :sales).count
end
end
And more specifically, how can I order them by the count result so I can list the most popular authors first?
Firstly, let's have all associations available on the Author class itself to keep the query code simple.
class Author < AR::Base
has_many :books
has_many :orders, :through => :books
has_many :sales, :through => :orders
end
The simplest approach would be for you to use group with count, which gets you a hash in the form {author-id: count}:
author_counts = Author.joins(:sales).group("authors.id").count
=> {1 => 3, 2 => 5, ... }
You can now sort your authors and lookup the count using the author_counts hash (authors with no sales will return nil):
<% Author.all.sort_by{|a| author_counts[a.id] || 0}.reverse.each do |author| %>
<%= author.name %>: <%= author_counts[author.id] || 0 %>
<% end %>
UPDATE
An alternative approach would be to use the ar_outer_joins gem that allows you get around the limitations of using includes to generate a LEFT JOIN:
authors = Author.outer_joins(:sales).
group(Author.column_names.map{|c| "authors.#{c}").
select("authors.*, COUNT(sales.id) as sales_count").
order("COUNT(sales.id) DESC")
Now your view can just look like this:
<% authors.each do |author| %>
<%= author.name %>: <%= author.sales_count %>
<% end %>
This example demonstrates how useful a LEFT JOIN can be where you can't (or specifically don't want to) eager load the other associations. I have no idea why outer_joins isn't included in ActiveRecord by default.

Resources