Converting SQL to ActiveRecord Query - mysql

I'm having troubles converting the following SQL query to an ActiveRecord query.
This would be the query in raw SQL:
SELECT customers.name, customers.email, customers.address, (sales.quantity * sales.price AS total_spent)
FROM customers JOIN sales ON customers.id = sales.customer_id
GROUP BY customers.id
ORDER BY total_spent DESC
I've done this to start working on that for ordering the top customers but it doesn't work:
Customer.joins(:buys).group("customers.id").select("id", "name", "price"*"stock" as "total_buys")
Any help would be really appreciated.
Thanks.

First set up your model associations and add some convenience methods
class Customer < ActiveRecord::Base
has_many :sales
# if you add a subtotal field that is autocalculated you could also do this
def total_spent
self.sales.all.map(|sale| sale.total ).sum
end
end
class Sale < ActiveRecord::Base
belongs_to :customer
def total
self.price * self.amount
end
end
Then in your app or the console you can type:
Customer.first.sales.map(&:total).sum
Or all Customers:
Customer.all.each{ |c| puts c.sales.map(&:total).sum }

Related

Use distinct method with order on a many to many relation with ActiveRecord

I have a classic many to many relationship defined like this in ActiveRecord:
class Developer < ApplicationRecord
has_many :developers_code_reviews
has_many :code_reviews, through: :developers_code_reviews
end
class DevelopersCodeReview < ApplicationRecord
belongs_to :code_review
belongs_to :developer
end
class CodeReview < ApplicationRecord
has_many :developers_code_reviews
has_many :developers, through: :developers_code_reviews
end
and I basically want to have a Developer array sorted by code_review.created_at without doubles.
My first attempt was the basic one: Developer.order('code_reviews.created_at': :asc) which triggers this error: ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'code_reviews.created' in 'order clause.
After a few googling I understood the join was not automatically performed by ActiveRecord so I added it: Developer.joins(:code_reviews).order('code_reviews.created_at': :asc). This one works but has doubles inside. I need to have a developer to appear only once in this array.
If I try to create a distinct on that query, ActiveRecord/MySQL complains that the ORDER BY is not performed on a column that is in the SELECT. How can I solve this problem?
I googled it quite a lot I can't find anything.
NOTE
The MYSQL query which works but with doubles looks like this:
SELECT `developers`.*
FROM `developers`
INNER JOIN `developers_code_reviews` ON `developers_code_reviews`.`developer_id` = `developers`.`id`
INNER JOIN `code_reviews` ON `code_reviews`.`id` = `developers_code_reviews`.`code_review_id`
ORDER BY `code_reviews`.`created_at` ASC
and I'd like to have a distinct on the developers.
I found the answer thanks to this clear example on MySQL sub queries: http://www.mysqltutorial.org/mysql-subquery/
With this example, I understood that I needed a sub query looking like this:
SELECT *
FROM developers
LEFT OUTER JOIN (
SELECT developer_id, max(updated_at) max_updated_at
FROM developers_code_reviews
GROUP BY developer_id
) dcr
ON developers.id = dcr.developer_id
ORDER BY maxupdt
Translated to ruby in my case:
class DevelopersCodeReview < ApplicationRecord
belongs_to :code_review
belongs_to :developer
class << self
def developer_queue
select('developer_id, max(updated_at) max_updated_at').
group(:developer_id)
end
end
end
class Developer < ApplicationRecord
belongs_to :slack_workspace
belongs_to :project, optional: true
class << self
def queue
developer_queue = DevelopersCodeReview.developer_queue.to_sql
joins("LEFT OUTER JOIN (#{developer_queue}) dcr ON id = dcr.developer_id").
order(max_updated_at: :asc)
end
end
end

Rails 5 left outer join using includes() and where()

I'm having a heck of a time getting the intended behavior using includes() and where().
Result I want:
- All students (even if they have zero check-ins)
- All check-ins in the Library
Result I'm getting:
- Only students with check-ins in the library
- All check-ins in the library, for those students
Currently my code is based off of this:
http://edgeguides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations
Which describes the behavior I want:
Article.includes(:comments).where(comments: { visible: true })
If, in the case of this includes query, there were no comments for any
articles, all the articles would still be loaded.
My code:
#students = Student.includes(:check_ins)
.where(check_ins: {location: "Library"})
.references(:check_ins)
.
class CheckIn < ApplicationRecord
belongs_to :student
end
.
class Student < ApplicationRecord
has_many :check_ins, dependent: :destroy
end
The generated SQL query:
SELECT "students"."id" AS t0_r0,"check_ins"."id" AS t1_r0, "check_ins"."location" AS t1_r1, "check_ins"."student_id" AS t1_r6 FROM "students" LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id" WHERE "check_ins"."location" IN ('Library')
This SQL query gives the join behavior I want:
SELECT first_name, C.id FROM students S LEFT OUTER JOIN check_ins C ON C.student_id = S.id AND location IN ('Library');
Tried a new approach using Scopes with relations, expecting to preload everything and filter it out, but was pleasantly surprised that Scopes actually give me the exact behavior I want (right down to the eager loading).
Here's the result:
This ActiveRecord Call pulls in the full list of students and eager loads the check-ins:
#students = Student.all.includes(:check_ins)
The scope of check_ins can be limited right in the has_many declaration:
Class Student < ApplicationRecord
has_many :check_ins, -> {where('location = 'Library'}, dependent: :destroy
end
Resulting in two clean, efficient queries:
Student Load (0.7ms) SELECT "students".* FROM "students"
CheckIn Load (1.2ms) SELECT "check_ins".* FROM "check_ins" WHERE location = 'Library') AND "check_ins"."student_id" IN (6, 7, 5, 3, 1, 8, 9, 4, 2)
Bingo!
p.s. you can read more about using scopes with assocations here:
http://ducktypelabs.com/using-scope-with-associations/
What you want in terms of pure SQL is:
LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id"
AND location IN ('Library')
However it is not possible (afaik) to get ActiveRecord to mark the association as loaded without trickery*.
class Student < ApplicationRecord
has_many :check_ins
def self.joins_check_ins
joins( <<~SQL
LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id"
AND location IN ('Library')
SQL
)
end
end
So if we iterate though the result it will cause a N+1 query issue:
irb(main):041:0> Student.joins_check_ins.map {|s| s.check_ins.loaded? }
Student Load (1.0ms) SELECT "students".* FROM "students" LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id"
AND location IN ('Library')
=> [false, false, false]
irb(main):042:0> Student.joins_check_ins.map {|s| s.check_ins.size }
Student Load (2.3ms) SELECT "students".* FROM "students" LEFT OUTER JOIN "check_ins" ON "check_ins"."student_id" = "students"."id"
AND location IN ('Library')
(1.2ms) SELECT COUNT(*) FROM "check_ins" WHERE "check_ins"."student_id" = $1 [["student_id", 1]]
(0.7ms) SELECT COUNT(*) FROM "check_ins" WHERE "check_ins"."student_id" = $1 [["student_id", 2]]
(0.6ms) SELECT COUNT(*) FROM "check_ins" WHERE "check_ins"."student_id" = $1 [["student_id", 3]]
To be honest, I never like preloading only a subset of association
because some parts of your application probably assume that it is
fully loaded. It might only make sense if you are getting the data to
display it.
- Robert Pankowecki, 3 ways to do eager loading (preloading) in Rails 3 & 4
So in this case you should consider preloading all the data and using something like a subquery to select the count of check_ins.
I would also advise you to create a separate table for locations.
I think this is the only way to create the query you want.
Student.joins("LEFT OUTER JOIN check_ins ON check_ins.student_id = students.id AND check_ins.location = 'Library'")
Reference : http://apidock.com/rails/ActiveRecord/QueryMethods/joins

Rails 4: order by attribute of has_many table, then join uniquely to that table, then group by that attribute

I have something like the following (obfuscated names and simplified from my work) in rails models:
Show < ActiveRecord::Base
has_many :airings
end
Airing < ActiveRecord::Base
has_many :viewers
#a DateTime
def start_time
super
end
end
Viewer < ActiveRecord::Base
has_many :ratings
end
Rating < ActiveRecord::Base
end
I need to assign each Show a date by the start_time it first airs, and then get some data: the number of viewers for each show date in a month, the number of ratings for each show date in a month. I'd like to use the groupdate gem to group by date.
I've gotten this far:
scope :by_start_time, -> {
joins(
"inner join airings st on shows.id = st.show_id",
airings: {viewers: :ratings}
).where("st.id in (?)",
ShowTime.group("st.show_id"
).having("st.start_time = MIN(st.start_time)").select("st.id"
)).select(
"shows.*, st.start_time, st.start_time as show_start"
).distinct

Ordering questions by most recently answered and filtering those unanswered by a particular user

I'm having trouble writing an Active Record query that returns the results I want. I have the following setup:
abridged User model:
class User < ActiveRecord::Base
has_many :answers
end
abridged Answer model:
class Answer < ActiveRecord::Base
belongs_to :question
belongs_to :user
end
abridged Question model:
class Question < ActiveRecord::Base
has_many :answers
def self.unanswered_by(user)
where(
'id NOT IN (SELECT question_id FROM answers WHERE user_id = ?)',
user.id
)
end
def self.recently_answered
includes(:answers).order('answers.updated_at DESC')
end
end
I'm trying to get an ActiveRecord::Relation back that orders the questions by those that have been most recently answered and then filters that result so it only contains questions a current_user has yet to answer.
Ideally, I'd like to write
Question.recently_answered.unanswered_by current_user
but this doesn't appear to work and I'm struggling to understand why with my limited understanding of SQL.
This is the result I get when I run this in the Rails console:
me = User.find(8)
Question.recently_answered.unanswered_by me
=> SQL (0.5ms) SELECT `questions`.`id` AS t0_r0,
`questions`.`question_text` AS t0_r1,
`questions`.`example_answer` AS t0_r2,
`questions`.`created_at` AS t0_r3,
`questions`.`updated_at` AS t0_r4,
`answers`.`id` AS t1_r0,
`answers`.`question_id` AS t1_r1,
`answers`.`user_id` AS t1_r2,
`answers`.`answer_text` AS t1_r3,
`answers`.`created_at` AS t1_r4,
`answers`.`updated_at` AS t1_r5
FROM `questions` LEFT OUTER JOIN `answers`
ON `answers`.`question_id` = `questions`.`id`
WHERE (id NOT IN (SELECT question_id FROM answers WHERE user_id = 8))
ORDER BY answers.updated_at DESC
#<ActiveRecord::Relation:0x3fd42e362a80>
Running Question.recently_answered.unanswered_by(me).to_sql gives me this:
=> "SELECT `questions`.*
FROM `questions`
WHERE (id NOT IN (SELECT question_id
FROM answers WHERE user_id = 8))
ORDER BY answers.updated_at DESC"
I'm working around this right now by doing
Question
.recently_answered
.reject { |q| q.answers.map(&:user_id).include? current_user.id }
but this returns an Array of Question objects instead of the ActiveRecord::Relation that I'd prefer.
Could someone help me understand why I can't chain recently_answered and unanswered_by as written and how I could go about rewriting this so I can get the result I want? Thanks.
You should add the table's name in the SQL query of the unanswered_by method:
def self.unanswered_by(user)
where('questions.id NOT IN (SELECT question_id FROM answers WHERE user_id = ?)', user.id)
#^^^^^^^^^ table's name added here
end
Because if you use this combined with a joins/includes, your DB adapter will not know from which table you select the id (error message like column id is ambiguous).
Also, you should probably use scope instead for these 2 methods.

Need array of self, children and children childrens in rails

I have a table like following
well,here in this table every user has a parent user,then if we select a user then its id ,children ids and children childrens ids should return as array.I need a query to get this values in rails with out using any gem.Thanx for your help:->
class User << ActiveRecord::Base
def self.build_tree(reverse=false)
g = Array.new
self.find(:all).each do |p|
if reverse
g << [p.id,p.parent]
else
g << [p.parent,p.id]
end
end
g
end
def self.subordinates(man_id)
g = self.build_tree false
g.map{|i| i[1] if i[0]==man_id.to_i}.compact
end
def self.superiors(user_id)
g = self.build_tree true
g.map{|i| i[1] if i[0]==user_id.to_i}.compact
end
end
When call either Superiors(parents) or Subordinates(childrends) it will gives required result
Ex:- [2,4,6,8]
If you want to get either children->childrends or parent->parents just do iterate call function either superior or subordinates until get the nil or [] array .
You are stranding into SQL anti-pattern. Performing operations on trees constructed like that is very inefficient. I don't say, that you should use a gem for that, but consider using some smarter method of keeping this data (searching for sql tree structure should yield some meaningful results).
Query you are looking for needs two self joins:
SELECT t1.id user_ids, t2.id children_ids, t3.id children_children_ids FROM users t1
LEFT JOIN users t2 ON t2.parent = t1.id
LEFT JOIN users t3 ON t3.parent = t2.id
On the other hand, if your rails models have defined self-relation, you could easily write:
user.children #=> (array of children)
user.children.flat_map(&:children) #=> (array of grandchildren)
Definition of this relation should look like:
class User << ActiveRecord::Base
has_many :children, class_name: User, foreign_key: 'parent'
end