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

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

Related

LEFT Join onto two tables in doctrine2 query builder

I have an entity called tracking. It relates to a User and a Course.
I also have an entity called credential which officially links User and Course
User <- Tracking -> Course || User <- Credential -> Course
Tracking is a joining entity of sorts, but it's not the primary join between the two. I have a query where I already have the user and the course joined, and I want to left-join the tracking if it exists. I've tried to simplify my example.
$q->select(
'user.id',
'user.firstname',
'user.lastname',
'count(t) as courses',
'count(t.completed) as completed'
);
$q->from(Credential::class, 'c');
$q->from(Course::class, 'course');
$q->from(User::class, 'user');
$q->leftJoin(Tracking::class, 't', 'WITH', 't.user = user and t.course = course');
$q->where('c.user = user and c.object = course');
$q->groupBy('user');
What I'm trying to achieve here, is a list of users who are enrolled in courses, the number of courses, and where possible the number of completed courses.
Unfortunately, doctrine can only seem to join to either the user table or the course table, but not to both. This might even be a mysql limitation. I've debugged this over and over - and I've run into the problem several times with different examples - and I just can't seem to find a solution other than using ->from(Tracking) which would eliminate students who haven't started any courses, and stats from courses they haven't started. I've googled over and over again, but It's so hard to search for this problem and not get 'How to join two tables with Doctrine'.
I get the error Column not found: 1054 Unknown column 'c1_.id' in 'on clause' which I assume means it can join on t.user = user but not t.course = course
Here is the actual code and error
$q = $this->em->createQueryBuilder();
$q->select(
'user.id',
'user.firstname',
'user.lastname',
'count(sc.id) as courses',
'count(ct.commenced) as commenced',
'count(ct.completed) as completed',
'avg(ct.scorePercent) as avgscore',
'avg(ct.totalTime) as avgtime'
);
$q->from(Security\Credential::class, 'c');
$q->from(Security\SecurableCourse::class, 'sc');
$q->from(Security\AccreditableInheritance::class, 'ai');
$q->from(Security\AccreditableUser::class, 'au');
$q->from(User::class, 'user');
$q->join(Tracking\CourseTracking::class, 'ct', 'WITH', 'ct.objectIdentity = sc and ct.user = user');
$q->where('sc = c.securable and ai.parent = c.accreditable and au = ai.child and user = au.user');
$q->andWhere('c.action = :action and sc.course in (:courses)');
$q->setParameter('action', 'study')->setParameter('courses', $courses);
$q->groupBy('user.id');
$users = $q->getQuery()->getScalarResult();
Doctrine\DBAL\Exception\InvalidFieldNameException(code: 0): An exception occurred while executing 'SELECT u0_.id AS id_0, u0_.firstname AS firstname_1, u0_.lastname AS lastname_2, count(s1_.id) AS sclr_3, count(t2_.commenced) AS sclr_4, count(t2_.completed) AS sclr_5, avg(t2_.scorePercent) AS sclr_6, avg(t2_.totalTime) AS sclr_7 FROM Credential c3_ INNER JOIN Tracking t2_ ON (t2_.objectIdentity_id = s1_.id AND t2_.user_id = u0_.id) AND t2_.dtype IN ('coursetracking') AND ((t2_.deleted IS NULL OR t2_.deleted > '2016-04-26 08:33:31')), SecurableIdentity s1_, AccreditableInheritance a4_, AccreditableIdentity a5_, User u0_ WHERE (((s1_.id = c3_.securable_id AND a4_.parent_id = c3_.accreditable_id AND a5_.id = a4_.child_id AND u0_.id = a5_.user_id) AND (c3_.action = ? AND s1_.course_id IN (?, ?, ?))) AND ((u0_.deleted IS NULL OR u0_.deleted > '2016-04-26 08:33:31'))) AND (s1_.dtype IN ('securablecourse') AND a5_.dtype IN ('accreditableuser')) GROUP BY u0_.id' with params [\"study\", \"46\", \"45\", \"160\"]:\n\nSQLSTATE[42S22]: Column not found: 1054 Unknown column 's1_.id' in 'on clause'
This is just a hint how to achieve it. I cannot give you the correct answer as you don't give enough details. But this will help you to achieve what you need.
$q->select(u, t, co, ce);
$q->from('User', 'u');
$q->leftJoin('u.tracking', 't');
$q->leftJoin('t.course', 'co');
$q->leftJoin('u.credential', 'ce');

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.

Converting SQL to ActiveRecord Query

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 }

How to scope by_month from belongs_to associated model?

ShiftNote belongs_to Shift has_many ShiftNote
I want to have scope ShiftNote.by_month("2013-10")
How to implement that?
Right now I am trying:
ShiftNote.includes(:shift)
.where("shifts.starts_at <= ? AND shifts.starts_at >= ?", "2013-10-01".to_date, "2013-10-30".to_date)
But I get
ShiftNote.includes(:shift).where("shifts.starts_at <= ? AND shifts.starts_at >= ?", "2013-10-01".to_date, "2013-10-30".to_date)
DEPRECATION WARNING: It looks like you are eager loading table(s) (one
of: shift_notes, shifts) that are referenced in a string SQL
snippet. For example:
Post.includes(:comments).where("comments.title = 'foo'")
Currently, Active Record recognizes the table in the string, and knows
to JOIN the comments table to the query, rather than loading comments
in a separate query. However, doing this without writing a full-blown
SQL parser is inherently flawed. Since we don't want to write an SQL
parser, we are removing this functionality. From now on, you must
explicitly tell Active Record when you are referencing a table from a
string:
Post.includes(:comments).where("comments.title = 'foo'").references(:comments)
If you don't rely on implicit join references you can disable the
feature entirely by setting
config.active_record.disable_implicit_join_references = true. SQL
(1.6ms) SELECT shift_notes.id AS t0_r0, shift_notes.state AS
t0_r1, shift_notes.user_id AS t0_r2, shift_days.shift_id AS
t0_r3, shift_notes.created_at AS t0_r4, shift_notes.updated_at
AS t0_r5, shifts.id AS t1_r0, shifts.starts_at AS t1_r1,
shifts.ends_at AS t1_r2, shifts.rate AS t1_r3,
shifts.limit AS t1_r4, shifts.created_at AS t1_r5,
shifts.updated_at AS t1_r6, shifts.state AS t1_r7,
shifts.shift_notes_count AS t1_r8 FROM shift_notes LEFT OUTER
JOIN shifts ON shifts.id = shift_notes.shift_id WHERE
(shifts.starts_at <= '2013-10-01' AND shifts.starts_at >=
'2013-10-30')
You can use a BETWEEN query, which is achieved using a range of dates:
class ShiftNote < ActiveRecord::Base
scope :by_month, ->(year = Date.today.year, month = Date.today.month) do
first_day = Date.new(year, month, 1)
last_day = Date.new(year, month + 1, 1) - 1
joins(:shifts)
.where('shifts.starts_at' => first_day..last_day)
end

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