Find associated model of all objects in an array - ruby-on-rails

Using Rails 4, I have the following:
class Driver < ActiveRecord::Base
has_and_belongs_to_many :cars, dependent: :destroy
end
class Car < ActiveRecord::Base
has_and_belongs_to_many :drivers
end
I have a join table cars_drivers with car_id and patient_id.
I want to find drivers who are 30 years old and above (driver.age > 30), drives a Honda (car.brand = "Honda"), and sum the number of drivers found.

In a raw SQL fashion:
sql = "
SELECT
SUM (driver), age, brand
FROM cars_drivers
JOIN cars, drivers
ON cars_drivers.car_id = cars.id
ON cars_drivers.driver_id = drivers.id
where
age > 30
and brand = "Honda"
"
records_array = ActiveRecord::Base.connection.execute(sql)

This should count the cars:
Car.where(brand: BRAND).includes(:drivers).where('drivers.age > ?', AGE).count
This should count the drivers:
Driver.where('age > ?', AGE).includes(:cars).where('cars.brand = ?', BRAND).count
I do recommend not using has_and_belongs_to_many, it seems like you have a lot of logic between the Driver and Car but with this setup you cannot have validations, callbacks or extra fields.
I would create a join model called CarDriver or if there is something even more describing like Job or Delivery and save them in their own table. Here is an article on the subject.

Ended up with this:
#drivers =
Driver.
joins(:cars).
where(cars_drivers: {
driver_id: Driver.where('age > 30'),
complication_id: Car.find_by_model("Honda").id
}).
size

Related

Count number of associations with a status in Ruby on Rails

I have a model named Project and Project has many Tasks
Task can have 3 different status(integer).
I want to get a list of Projects with counts of associated Tasks in status = 1, 2 and 3.
The best i can get to is have a method on Project
def open_tasks
self.tasks.where(:status => 1).count
end
But this will make another SQL for each count and it is very bad performance when loading 100 projects.
Is there a way to get it out in one SQL statement?
I can think of a couple of ways to do this...
(It's not a single sql statement but two, still quite performant though)...
Task.where(status: 1).group(:project_id).count
will give you a hash where the keys are project ids and the values are the task counts. You can then combine this with the list of projects.
You can use the ActiveRecord counter_cache to save in the project records a value for the number of open tasks. ActiveRecord will automatically update this for you. I believe you will need to add an association to the project model like this:
# app/models/project.rb
# needs to include a column called open_task_count
class Project < ActiveRecord::Base
has_many :open_tasks, class_name: Task, -> { where status: 1 }
end
class Task < ActiveRecord::Base
belongs_to :project, counter_cache: true
end
Project.select(
'projects.*',
'(SELECT COUNT(tasks.*) FROM tasks WHERE tasks.project_id = projects.id AND tasks.status = 0) AS status_0_count',
'(SELECT COUNT(tasks.*) FROM tasks WHERE tasks.project_id = projects.id AND tasks.status = 1) AS status_1_count'
).left_joins(:tasks)
Although there are more elegant ways (like lateral joins and CTEs) subqueries work on most DBs. If statuses is an ActiveRecord::Enum you can construct the subqueries by looping over the enum mapping:
class Project < ApplicationRecord
has_many :tasks
def self.with_task_counts
# constucts an array of SQL strings
statuses = Task.statuses.map do |key, int|
sql = Task.select('COUNT(*)')
.where('tasks.project_id = projects.id')
.where(status: key)
.to_sql
"(#{sql}) AS #{key}_tasks_count"
end
select(
'projects.*',
*statuses # * turns the array into a list of args
).left_joins(:tasks)
end
end
In Rails 4 you can still do a LEFT OUTER JOIN by using a SQL string:
class Project
def self.left_joins_tasks(*args)
deprecator = ActiveSupport::Deprecation.new("5.0", "MyApp")
deprecator.deprecation_warning("left_joins_tasks is deprecated, use `.left_joins(:tasks)` instead")
joins('LEFT OUTER JOIN tasks ON tasks.project_id = projects.id')
end
end
Using .joins works as well but gives an INNER join so rows with no tasks are filtered out. You can also use .includes.
I ended up using the counter_culture gem.
https://github.com/magnusvk/counter_culture

Get count of all associations to a model in rails

I have a model A as:
class A < ActiveRecord::Base
belongs_to :bp, class_name: "B", foreign_key: :bp_id
belongs_to :cp, class_name: "B", foreign_key: :cp_id
end
I have a query where I am trying to get the size of all the associations to model B from model A.
I made it work by getting the count of all associations through bp and then cp and adding both up.
total_count = bp_size + cp_size
Is it possible to get the sum of all associations to B model from A in a single query?
My exact queries are as follows:
bp_size = E.where(f_id: g.joins(:f)).joins(bp: :s).size
cp_size = E.where(f_id: g.joins(:f)).joins(cp: :s).size
The above query goes through multiple levels of associations but then I join it through bp and cp to get the appropriate size. I want to avoid running that query twice
I have no single idea about AR so-called “helpers”, but this is perfectly doable:
query = <<-SQL
SELECT
(SELECT COUNT(*) FROM a LEFT JOIN b ON (a.bp_id = b.id)) +
(SELECT COUNT(*) FROM a LEFT JOIN b ON (a.cp_id = b.id))
SQL
A.connection.execute(query).to_a.first.first
Dynamic solution
def self.associations_counts_hash(model, association_types)
associations = model.reflections.collect do |a, b|
b.plural_name if association_types.include?(b.macro)
end.compact
query = model.all.includes(associations)
associations.map do |association|
[
association,
query.map { |u| u.send(association).size }.sum
]
end.to_h
end
associations_counts_hash(User, [:belongs_to])
Couldn't work out a good solution to avoid a query for each association though.

Rails active record scope based on count of has_many scope

I have Order model and a Container model with a scope as below:
class Order < ActiveRecord::Base
has_many :containers, inverse_of: :order, dependent: :destroy
end
class Container < ActiveRecord::Base
scope :full_pickup_ready, -> { where.not(full_pickup_ready_date: nil) }
end
The order model has a field call quantity which represents the quantity of containers the order requires but is not necessarily the size of the containers association as not all container data is entered at the time of order creation.
I would like to have a scope on the Order model based on whether the count of the containers with a full_pickup_ready_date is less than the quantity field of the order.
I know I can use merge on the order model to access the scope on the containers like this:
def self.at_origin
joins(:containers).merge(Container.full_pickup_ready).uniq
end
but how can I limit the scope to orders where the total number of containers with a full_pickup_ready_date is less that the quantity field on the order?
UPDATE:
this is reasonably close, but I don't think using the select is efficient:
includes(:containers).select {|o| o.containers.full_pickup_ready.size < o.quantity }
If you're willing to forgo reuse of the scope on Container, then you should be able to use something like:
# scope on Order
joins(:containers)
.group("orders.id")
.having("count(CASE WHEN full_pickup_ready_date THEN 1 END) < orders.quantity")
I think you need to get this SQL query to work for you. So the idea is to get the SQL query right, and then translate it to "Rails".
SQL
If I am correct, this should be the SQL query you want to achieve. Maybe you can try it in your rails db
SELECT orders.*
FROM orders JOIN containers
WHERE containers.id = orders.id
AND (
SELECT COUNT(containers.id)
FROM containers
WHERE containers.full_pickup_ready_date IS NOT NULL
) < orders.quantity;
ActiveRecord
If this is the right query, then we can do this using rails
Order.joins(:containers).where("( SELECT COUNT(containers.id) FROM containers WHERE containers.full_pickup_ready_date IS NOT NULL ) < orders.quantity")
This should return an ActiveRecord relation. You could also do this:
sql = %{
SELECT orders.*
FROM orders JOIN containers
WHERE containers.id = orders.id
AND (
SELECT COUNT(containers.id)
FROM containers
WHERE containers.full_pickup_ready_date IS NOT NULL
) < orders.quantity;
}.gsub(/\s+/, " ").strip
Order.find_by_sql(sql)
Just add this in a class method (better than scope IMHO) and you are good to go.
So, your Order class should look like this:
class Order < ActiveRecord::Base
has_many :containers, inverse_of: :order, dependent: :destroy
def self.gimme_a_query_name
joins(:containers).where("( SELECT COUNT(containers.id) FROM containers WHERE containers.full_pickup_ready_date IS NOT NULL ) < orders.quantity")
end
def self.gimme_another_query_name
sql = %{
SELECT orders.*
FROM orders JOIN containers
WHERE containers.id = orders.id
AND (
SELECT COUNT(containers.id)
FROM containers
WHERE containers.full_pickup_ready_date IS NOT NULL
) < orders.quantity;
}.gsub(/\s+/, " ").strip
find_by_sql(sql)
end
end
I have no way to try this, but it should work with few tweak to get the SQL query right.
I hope this help!

How to use `join` method to access columns from multiple tables in rails

I have two tables, and I want to display all these columns on page.
Tables:
1.Users:
name, email, sex_id
abc, abc#q.com, 0
2. Masters:
type, sex, sexn
8, 0, female
I want to display:
name, email, sex
abc, abc#q.com, femail
Models' definition:
class Master < ApplicationRecord
has_many :users
end
class User < ApplicationRecord
belongs_to :master
def self.search(search)
where("name LIKE ?", "%#{search}%")
end
end
using #users = User.joins("INNER JOIN masters ON masters.sex = users.sex_id AND masters.type = 8"), I can only access columns from Users.
I want to access data from Masters. Using #users.first.master, I just get nil.
using#users = User.find_by_sql("SELECT * FROM users INNER JOIN masters ON masters.sex = users.sex_id AND masters.type = 8"), I can access columns from these two tables. So there's no problem with my data.
How do I use join method to access columns from multiple tables?
You have missed the select statement, try this one.
#users = User.select("users.*, masters.*").joins("INNER JOIN masters ON masters.sex = users.sex_id AND masters.type = 8")
You're almost there! You can have Rails do the join for you with the .includes() method:
def self.search(search)
# :master is singular
includes(:master).where("name LIKE ?", "%#{search}%")
end
Then:
#users.each do |user|
# singular again
puts user.master.sexn
end

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