I would like to get all customers and their products that are under category 'Beverages'.
Here are my relations:
Customer has_many orders
Order has_many products through order_details
Product has_many Orders through order_details
Product belongs to Category
I tried this but it doesn't give all customers. It gives only customers who order Beverages.
Customer.includes(orders: {products: :category}).where(categories: { category_name: "Beverages"})
Note: I don't want to filter Customer records. I need to apply filter on products because loading all records are useless in this case.
Active Record doesn't have an API to do this, unfortunately.
If you only need a few known categories, you can do it by adding a custom association:
class Customer ..
has_many :beverage_orders, -> { includes(products: :categories).where(categories: { category_name: "Beverages" }) }, class_name: "Order"
(and then Customer.includes(:beverage_orders))
If you need to support arbitrary categories, your options get even worse... if there are few enough customers, I'd consider just skipping the include and accepting the N+1 query.
Otherwise, the next least bad option I can suggest is to do the query yourself:
customers = Customer.all # or whatever
beverage_orders = Order.includes(products: :categories).
where(categories: { category_name: "Beverages" }).
where(customer_id: customers).
group_by(&:customer_id)
customers.each do |customer|
puts "#{customer.name} ordered #{beverage_orders[customer.id].size} beverages"
# i.e., use `beverage_orders[customer.id]` instead of `customer.orders`
end
If you need to filter your records, why you are using left join?
Try
Customer.joins(:orders).where(order: { product_id: Category.find_by(name: 'Beverages').products.pluck(:id) })
UPDATE:
As where clause will filter the base record, so it should filter your Customer model you need to customize joins to select proper products instead of filtering customers.
category_id = Category.find_by(name: "Beverages").id
Customer.joins(orders).joins("LEFT OUTER JOIN products ON products.id = orders.product_id AND products.category_id = #{category_id}")
Related
I need to make a consolidated list of payable services, with filtering, ordering and pagination (with willpaginate gem) the payable services are stored in two separate models, Application and Support, both have similar fields (that I need to show in the columns) but I would need to have both in the same query.
here's my situation:
class Application < ActiveRecord::Base
// name, created_at, price ...
has_many :supports
end
class Support < ActiveRecord::Base
// name, application_id, created_at, price ...
belongs_to :application
end
and the controller:
class PaymentsController < ApplicationController
FILTER_STRINGS = {
requested: { reference_status: 'requested' },
sent: { reference_status: 'sent' },
completed: { reference_status: 'completed' }
}
ORDER_STRINGS = {
name: 'name %1$s',
price: 'price %1$s',
date: 'created_at %1$s'
}
def payable_list
#applications = Application.all
#applications = #applications.where(FILTER_STRINGS[filter_params[:filter].to_sym]) if filter_params[:filter]
order_string = format(ORDER_STRINGS[sort_params[:sort].downcase.to_sym], sort_params[:dir])
#applications = #applications.order(order_string)
#applications = #applications.paginate(page: params[:page] || 1, per_page: 20)
end
...
end
The controller currently only loads the Application, but I need to include the Support items as well, in the view I will append the string '(Support)' for support items, to differentiate the items.
How could I include both models in the same ActiveRecord_Relation, so that I can sort them together? I would have no issues with using Ruby to sort the lists, but then I'd have problems with will paginate who seems to only work with ActiveRecord_Relation.
I tried using a join clause, but that still returns an Application object
Application.joins('JOIN supports ON "supports"."application_id" = "applications"."id"')
I tried union as well, with this command:
Application.find_by_sql("SELECT
'application' AS role,
applications.id AS id,
applications.price AS price,
applications.application_id AS application_or_support,
to_char(applications.created_at, 'DD Month YYYY, HH24:MI') AS created_at,
FROM applications
UNION
select
'support' AS role,
supports.id AS id,
supports.price AS price,
supports.support_id AS application_or_support,
to_char(supports.created_at, 'DD Month YYYY, HH24:MI') AS created_at,
from supports;")
but it's only giving me the columns that have a match in the applications model.
I'm thinking that maybe I could make a new model not backed by a DB table, where the rows would originate from that union query, but I wouldn't know how to go about the syntax to get it done.
I have the following two models in Rails application
class Students < ApplicationRecord
has_many :courses
attr_accessor :name
end
class Courses < ApplicationRecord
has_many :students
attr_accessor :name, :course_id
end
I would like to get a a list of all courses shared for each student who has been in the same class as a selected student in an efficient manner.
Given the following students:
Jerry's courses ["math", "english", "spanish"],
Bob's courses ["math", "english", "french"],
Frank's courses ["math", "basketweaving"]
Harry's courses ["basketweaving"]
If the selected student was Jerry, I would like the following object returned
{ Bob: ["math", "english"], Frank: ["math"] }
I know this will be an expensive operation, but my gut tells me there's a better way than what I'm doing. Here's what I've tried:
# student with id:1 is selected student
courses = Student.find(1).courses
students_with_shared_classes = {}
courses.each do |course|
students_in_course = Course.find(course.id).students
students_in_course.each do |s|
if students_with_shared_classes.key?(s.name)
students_with_shared_classes[s.name].append(course.name)
else
students_with_shared_classes[s.name] = [course.name]
end
end
end
Are there any ActiveRecord or SQL tricks for a situation like this?
I think you're looking to do something like this:
student_id = 1
courses = Student.find(student_id).courses
other_students = Student
.join(:courses)
.eager_load(:courses)
.where(courses: courses)
.not.where(id: student_id)
This would give a collection of other students that took courses with only two db queries, and then you'd need to narrow down to the collection you're trying to create:
course_names = courses.map(&:name)
other_students.each_with_object({}) do |other_student, collection|
course_names = other_student.courses.map(&:name)
collection[other_student.name] = course_names.select { |course_name| course_names.include?(course_name) }
end
The above would build out collection where the key is the student names, and the values are the array of courses that match what student_id took.
Provided you setup a join model (as required unless you use has_and_belongs_to_many) you could query it directly and use an array aggregate:
# given the student 'jerry'
enrollments = Enrollment.joins(:course)
.joins(:student)
.select(
'students.name AS student_name',
'array_agg(courses.name) AS course_names'
)
.where(course_id: jerry.courses)
.where.not(student_id: jerry.id)
.group(:student_id)
array_agg is a Postgres specific function. On MySQL and I belive Oracle you can use JSON_ARRAYAGG to the same end. SQLite only has group_concat which returns a comma separated string.
If you want to get a hash from there you can do:
enrollments.each_with_object({}) do |e, hash|
hash[e.student_name] = e.course_names
end
This option is not as database independent as Gavin Millers excellent answer but does all the work on the database side so that you don't have to iterate through the records in ruby and sort out the courses that they don't have in common.
In the following setup a customer has many tags through taggings.
class Customer
has_many :taggings
has_many :tags, through: :taggings
end
class Tagging
belongs_to :tag
belongs_to :customer
end
The query I'm trying to perform in Rails with postgres is to Find all customers that have at least one tag but don't have either of the tags A or B.
Performance would need to be taken into consideration as there are tens of thousands of customers.
Please try the following query.
Customer.distinct.joins(:taggings).where.not(id: Customer.joins(:taggings).where(taggings: {tag_id: [tag_id_a,tag_id_b]}).distinct )
Explanation.
Joins will fire inner join query and will make sure you get only those customers which have at least one tag associated with them.
where.not will take care of your additional condition.
Hope this helps.
Let tag_ids is array of A and B ids:
tag_ids = [a.id, b.id]
Then you need to find the Customers, which have either A or B tag:
except_relation = Customer.
joins(:tags).
where(tags: { id: tag_ids }).
distinct
And exclude them from the ones, which have at least one tag:
Customer.
joins(:tags).
where.not(id: except_relation).
distinct
INNER JOIN, produced by .joins, removes Customer without Tag and is a source of dups, so distinct is needed.
UPD: When you need performance, you probably have to change your DB schema to avoid extra joins and indexes.
You can search examples of jsonb tags implementation.
Get ids of tag A and B
ids_of_tag_a_and_b = [Tag.find_by_title('A').id, Tag.find_by_title('B').id]
Find all customers that have at least one tag but don't have either of the tags A or B.
#Customer.joins(:tags).where.not("tags.id in (?)", ids_of_tag_a_and_b)
Customer.joins(:tags).where.not("tags.id = ? OR tags.id = ?", tag_id_1, tag_id_2)
How can I run a joins query to find only the records that contain all has_many relationships?
For example:
Products has many Filters and Filters has many Products. (product_filter_sets is the join table for the two many to many relationships.)
I want to run a query to find the products that contain all the filters using filter IDs.
Currently I have this query
Product.joins(:product_filter_sets)
.where(:product_filter_sets => { product_filter_id: [1,2,4,5] } )
But this returns all products that contain at least one of the filters. What I want is products that contain all of the filters.
Join products to filters, group by product and select products that appeared as many times in the join as the number of filters you specified:
required_filter_ids = [1, 2, 4, 5]
Product
.joins(:product_filter_sets)
.where(product_filter_sets: { product_filter_id: required_filter_ids })
.group('products.id')
.having("count(*) = #{required_filter_ids.length}")
I have a model Category, which has_many Products, and a Product in turn has_many Categories. When a user searches for a Category, I'd like to return the products of the matching Categories without losing my Arel object. Here's what I have so far:
Category.where("upper(title) like ?", search_term.upcase).map {|category| category.products}.flatten
This does the trick of returning the products, but of course what I have is an array and not Arel. I can get as far as adding an :includes(:products) clause, so I do indeed get the products back but I still have them attached to their categories. How do I adjust my query so that all I get back is an Arel that only addresses products?
If it is products that you want then you should probably start with the Product object when you are searching. For example ,you could do it like this:
Product.joins(:categories).where("upper(categories.title) like ?", search_term.upcase)
The reason I use joins instead of includes is that joins will perform an INNER JOIN instead of LEFT OUTER JOIN which is what you need to only return the products that are actually associated with the found categories.
To make it a little more elegant you could wrap it all up in a scope in your Product model like this:
# In Product.rb
scope :in_categories_like, Proc.new{ |search_term|
joins(:categories).where("upper(categories.title) like ?", search_term.upcase)
}
# In use
#products = Product.in_categories_like(params[:search_term])