Constructing Query with Rails - GROUP, MAXIMUM, UNION - ruby-on-rails

I have two models:
Student
-> has_many :enrollments
Enrollment
-> belongs_to :student
The tables goes something like this:
https://i.stack.imgur.com/ilIxL.png
Now my goal is to retrieve those students which latest enrollment end date is lesser than the current date, or if the student has no enrollment yet. So given the sample table on the link above, and current date is 11/20/2019, the query should return:
John Doe (because latest end date is 11/01/2019 which is lesser than 11/20/2019)
Casper Gost (because this student has no enrollment record yet)
How do I create that query in Ruby on Rails? (Sorry, I'm still noob in rails).
I visualize the sql query is this:
select students.*
from students s
left join
( select student_id, end_date
from enrollments e1
where end_date = (select max(e2.end_date)
from enrollments e2
group by e2.student_id
having e2.student_id = e1.student_id
)
) e on e.student_id = s.id
where e.end_date < Date.today OR e.student_id IS NULL
but I can't seem to build it using Rails ActiveRecord's methods.
EDIT
I tried Roc khalil's/Catmal's solution, but the "or" method is giving an incompatible error:
(Relation passed to #or must be structurally compatible. Incompatible
values: [:includes])
Also, I revised the code a little bit to fit my needs:
This will retrieve all students that have no records in enrollments table
Student.where.not(id: Enrollment.pluck(:student_id))
And this code will retrieve all students whose last enrollment's end date is lesser than today
Student.joins(:enrollments).where('end_date IN (?)',Enrollment.all.group(:student_id).maximum(:end_date).values).where('end_date < ?', Date.today) When I executed them separately, I get no error. However if I combine them with the "or" method I get the same incompatible error:
ArgumentError (Relation passed to #or must be structurally compatible.
Incompatible values: [:joins])
I've searched it and it looks like it's a bug. :(
So close though :(
UPDATE
I have implemented both #nathanvda and #Catmal suggestion, and came up with this:
Student model:
scope :no_enrollments, -> { where.not(id: Enrollment.pluck(:student_id)) }
scope :with_expired_enrollments, -> { joins(:enrollments).merge(Enrollment.expired_enrollments) }
scope :unenrolled, -> { find_by_sql("#{no_enrollments.to_sql} UNION #{with_expired_enrollments.to_sql}") }
Enrollment model:
scope :expired_enrollments, -> { where('end_date IN (?)',Enrollment.all.group(:student_id).maximum(:end_date).values).where('end_date < ?', Date.today) }
So in my controller, to get the unenrolled students, I just have to call:
Student.unenrolled
Maybe not the optimum solution, but it works on my app. Thanks #nathanvda and #Catmal.

Try this:
#selected_students = Student.where.not(
id: Enrollment.pluck(:student_id)
).or(
Student.includes(:enrollments).where(
enrollments: {
date_end < Date.today
}
)
)

Related

Ruby on Rails Active Record query joining two tables and query based on condition

I have two tables: Transactions and Properties.
I have one condition to satisfy that doesn't require joining tables.
On my Transactions query:
rows where sales_date is in a certain month
rows where sold_or_leased is "leased"
My next condition requires joining properties to transactions so that I can:
rows where transactions.sales_date is in a certain month
rows where transactions.sold_or_leased is null AND
rows where properties.for_sale is false AND properties.for_lease is true
Basically, a new column was added to transactions called sold_or_leased and a lot of them are null. I need to an extra query to cover the null columns.
#test variables for month
date = "2019-11-01"
month = Date.parse date
# below satisfies my first part
#testobj = Transaction.where(sold_or_leased: "leased")
.where("sales_date >= ? AND sales_date < ?", month.beginning_of_month, month.end_of_month).count
But now I need to extend this query to include properties and test a property column
I'm not sure where to go from here:
#testobj = Transaction.joins(:property)
.where(sold_or_leased: "leased")
.where("sales_date >= ? AND sales_date < ?", month.beginning_of_month, month.end_of_month)
.or(
Transaction.where(sold_or_lease: nil)
).count
Also, when I add a join and then an or clause, i get an error Relation passed to #or must be structurally compatible. Incompatible values: [:joins]
I will share relevant model info:
Transaction Model:
class Transaction < ApplicationRecord
belongs_to :user
belongs_to :property
end
Property Model:
class Property < ApplicationRecord
has_one :property_transaction, class_name: 'Transaction', dependent: :destroy
end
With the help of Sebastian, I have the following (which still produces the structural error message):
Transaction.joins(:property)
.where(sales_date: month.all_month,
sold_or_leased: nil,
properties: { for_sale: false, for_lease: true })
.or(
Transaction.joins(:property)
.where(sold_or_leased: "leased")
.where("sales_date >= ? AND sales_date < ?", month.beginning_of_month, month.end_of_month)
)
In theory, you should be able to access the properties table columns after a join.
Looking to your current code and what you need to get you could try with:
Transaction
.joins(:property)
.where(sales_date: month.all_month)
.where(
"(sold_or_leased IS NULL AND properties.for_sale = false AND properties.for_lease = true) OR
(sold_or_leased = 'leased')"
)
If you're unable to use the ActiveRecord::QueryMethods#or, you can always use the SQL OR operator within a string argument to where.
Notice month.all_month produces the whole range of dates for a corresponding month, which when used with where is converted to the first and last day of the month:
SELECT ... WHERE "transactions"."sales_date" BETWEEN $1 AND $2 AND ... [["sales_date", "2019-11-01"], ["sales_date", "2019-11-30"]]
Shorter than the month.beginning_of_month and month.end_of_month variation.

Ruby, query model with aggregate with model

In my rails application, at some point, I query my model simply. I want to query customers order information like how many orders were given by this customer within three months.
Just now, I query the model in that way:
#customer = Customer.all
customer.rb
class Customer < ApplicationRecord
audited
has_many :orders
end
And customer may have orders.
order.rb
class Order < ApplicationRecord
audited
belongs_to :customer
end
What I would like to do is to query customers model and to inject aggregate function result to every customer records.
EDİT
I tried to simulate every solution but couln't achieve.
I have the following query in mysql.
How do I need to code in ruby with activerecord to create that query ?
SELECT
(SELECT
COUNT(*)
FROM
orders o
WHERE
o.customer_id = c.id
AND startTime BETWEEN '2017.12.04' AND '2018.01.04') AS count_last_month,
(SELECT
COUNT(*)
FROM
orders o
WHERE
o.customer_id = c.id
AND startTime BETWEEN '2017.10.04' AND '2018.01.04') AS count_last_three_month,
c.*
FROM
customers c;
How can I achieve that?
Thanks.
Customer.
joins(:orders).
group('customers.id').
where('orders.created_at > DATE_SUB(NOW(), INTERVAL 3 MONTH)')
select('sum(orders.id), customers.*')
As my understanding of you question. I have this solution for you question. Please have a look and try it once. In below query, 'includes' used to solve N+1 problem.
Customer.includes(:orders).where('created_at BETWEEN ? AND ?', Time.now.beginning_of_day, Time.now.beginning_of_day-3.months).group_by{|c|c.orders.count}
If you are looking for particular customer's order count then you can try this one.
#customer.orders.where('created_at BETWEEN ? AND ?', Time.now.beginning_of_day, Time.now.beginning_of_day-3.months).group_by{|c|c.orders.count}

Exclude object if one of the has_many related entities has the attribute with value x

I came across about the problem excluding data, if the attribute x of one of the associated data has the value 'a'.
Example:
class Order < ActiveRecord::Base
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :order
validate_presence_of :status
end
The query should return all Orders that don't have an Item with status = 'paid' (status != 'paid').
Because of the 1:n association an Order can have many Items. And one of the Itmes can have the status = 'paid'. These Orders must be excluded from the result of my query even if the order has other items with status different from 'paid'.
How would I solve this problem:
paid_items = Items.where(status: 'paid').pluck(:order_id)
orders_wo_paid = Order.where('id NOT IN (?)', paid_items)
Is there an ActiveRecord solution, that solves this problem in one query.
Or are there other ways to solve this question?
I 'm not looking for ruby solution such as:
Order.select do |order|
!order.items.pluck(:status).include?('paid')
end
thx for ideas and inspirations.
You can do:
Order.where('orders.id NOT IN (?)', Item.where(status: 'paid').select(:order_id))
If you're using Rails 4.x then:
Order.where.not(id: Item.where(status: 'paid').select(:order_id))
The query you are interested in is the following, but creating with activerecord will be hard/no very readable:
SELECT
orders.*
FROM
orders
LEFT JOIN
order_items ON orders.id = order_items.order_id
GROUP BY
order_items.order_id
HAVING
COUNT(DISTINCT order_items.id) = COUNT(DISTINCT order_items.status <> 'paid')
Sorry for the sql indentation, I have no idea which are the conventions for it.
A way (not the best one at all) to it with rails (unfortunately writing sql for the most important parts) would be the following:
Order.group(:order_id).joins("LEFT JOIN order_items ON orders.id = order_items.order_id")
.having("COUNT(DISTINCT order_items.id) = COUNT(DISTINCT order_items.status <> 'paid')")
Of course you can play with AREL to get rid of the hard coded sql, but in my opinion it will not be easier to read.
You can have an example of creating lefts joins in this gist: https://gist.github.com/mildmojo/3724189

How can I find records by "count" of association using rails and mongoid?

With these models:
class Week
has_many :proofs
end
class Proof
belongs_to :week
end
I want to do something like:
Week.where(:proof.count.gt => 0)
To find only weeks that have multiple proofs.
There is one answer that seems to address this:
Can rails scopes filter on the number of associated classes for a given field
But in this example, there is no such attribute as proof_ids in Week since the ids are stored with the proofs. This does not work for example:
Week.where(:proof_ids.gt => 0)
How is this query possible? Conceptually simple but I can't figure out how to do this with mongo or mongoid.
Similarly, I'd like to order by the number of proofs for example like:
Week.desc(:proofs.size)
But this also does not work.
I do realize that a counter-cache is an option to both my specific questions but I'd also like to be able to do the query.
Thanks in advance for any help.
With rails (and without counter_cache), you could do:
class Week < ActiveRecord::Base
has_many :proofs
def self.by_proofs_size
sort_by { |week| week.proofs.size }
end
def self.with_at_least_n_proofs(n = 1)
select { |week| week.proofs.size >= n }
end
end
Even though each of those operations produces 2 queries, this is far from ideal.
The pair of queries is repeated (=> 4 queries for each operation) with scopes (bug?):
scope :with_at_least_n_proofs, -> (n = 1) { select { |w| w.proofs.size >= n } }
scope :by_proofs_size, -> { sort_by { |w| w.proofs.size } }
The ideal is probably to use counter_cache
scope :with_at_least_n_proofs, -> (n = 1) { where('proofs_count >= ?', n) }
scope :by_proofs_size, -> { order(proofs_count: :desc) }
I don't know if this is the best solution, as it maps it through a array, but this does the job: (the other solutions mentioned here gives me exceptions)
class Week < ActiveRecord::Base
scope :has_proofs, -> { any_in(:_id => includes(:proofs).select{ |w| w.proofs.size > 0 }.map{ |r| r.id }) }
end
Pardon me if I'm way off - but would you be able to use a simple counter_cache in the weeks table? Then you could do something like week.proofs_count.

Rails: How do you sort a model by a column in a tabel two associations away?

The problem I'm having is like this: The model to sort is SchoolClass which has_many Students which in turn has_many Projects and each project has an end_date. I need to sort the SchoolClasses four ways: First by the earliest project end_date sort ascending and descending, and second by the latest project end_date sort ascending and descending. Does this make sense?
class SchoolClass < ActiveRecord::Base
has_many :students
end
class Student < ActiveRecord::Base
has_many :projects
belongs_to :school_class
end
class Project < ActiveRecord::Base
belongs_to :student
end
The only way I can think of doing it is very brute force and involves having a methods in the SchoolClass model that return the earliest and latest project dates for that instance like so:
students.collect(&:projects).flatten.select(&:end_date).sort.last
to find the latest project end_date for that class and then fetching out all the classes of the database and sorting them by that method. Surely this is just awful though, right? I would really like to find the rails way to get this ordering (with scopes maybe?). I thought something like SchoolClasses.joins(:students).joins(:projects).order('projects.end_date ASC') might work but that will crash rails (and looking at it now the logic is wrong anyway i think).
Any suggestions?
Try this:
scs = SchoolClass.joins({:students => :projects}).
select("school_classes.id,
MIN(projects.end_date) AS earliest_end_date,
MAX(projects.end_date) AS latest_end_date").
group("school_classes.id").
order("earliest_end_date ASC")
The objects in the scs array has following attributes:
id
earliest_end_date
latest_end_date
If you need additional attributes you can do the following
1) Add the additional attributes to the group and select methods
2) Query the full SchoolClass object using the id
3) Rewrite the query to use a nested JOIN
scs = SchoolClass.joins(
"JOIN (
SELECT a.id,
MIN(c.end_date) AS earliest_end_date,
MAX(c.end_date) AS latest_end_date
FROM school_classes a
JOIN students b ON b.class_id = a.id
JOIN projects c ON c.student_id = b.id
GROUP BY a.id
) d ON d.id = school_classes.id
").select("school_classes.*,
d.earliest_end_date AS earliest_end_date,
d.latest_end_date AS latest_end_date").
order("earliest_end_date ASC")

Resources