Complex query, use :includes and :joins at the same time? - sql

(Using Rails 3.1.3)
I have an app that manages products. I import the products from several resellers and they all name their categories different. Because of this I have resellercategories that are mapped to my own subcategories.
Categories
Subcategories (belongs_to Category)
Resellercategories (belongs_to Subcategory)
Products (belongs_to Resellercategory)
You can see the models and how the relations are defined here:
http://snipt.net/Linuus/category-and-subcategory?key=38ba590408ac4233927a06046eeca30d
On my site I want to display the categories and their subcategories, easy.
If a user filters the products for, say, only 'female' products I want to filter also the categories and subcategories so that only categories and subcategories that have 'female' products are displayed. The gender is stored in the products.
So, how can I do this?
I tried to create a query like this:
http://snipt.net/Linuus/categories-1/?key=2d5d54fd573f0afe60eaa3c47a23fd4d
which (I think) filters the correct Categories. However, when I do something like:
#menu_categories.each do |c|
c.subcategories.each do |sc|
# do something...
end
end
It still queries all the subcategories whether or not they have female products. So, I got a suggestion over at the Ruby on Rails Google Group to eagerly load the :subcategories using .includes(). So, something like this:
Category.includes(:subcategories)
.joins("INNER JOIN resellercategories AS r ON subcategories.id = r.subcategory_id")
.joins("INNER JOIN products AS p ON r.id = p.resellercategory_id")
.group("categories.id")
.order("categories.name ASC")
.where("p.gender = 'unisex' OR p.gender = 'female'")
.where("subcategories.id > 0") # Dummy to trigger eager loading
However, when mixing .includes() and .joins() the includes seems to fail to eager load anything. Thus throwing the error below:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: subcategories.id:
SELECT "categories".* FROM "categories"
INNER JOIN resellercategories AS r ON subcategories.id = r.subcategory_id
INNER JOIN products AS p ON r.id = p.resellercategory_id
WHERE (p.gender = 'unisex' OR p.gender = 'female')
GROUP BY categories.id
ORDER BY categories.name ASC
Is this behavior expected? Is it a bug?
Am I trying to do this the right way or is there a better way to do it?
Any help is very appreciated.
(The discussion on RoR Google Group: https://groups.google.com/forum/?pli=1#!topic/rubyonrails-talk/UkCF7jbehHk)
Solution:
Ok, so the solution is to use eager_load() instead of includes(). I also had to remove group()
This seems to work for me:
Category.eager_load(:subcategories)
.joins("INNER JOIN resellercategories AS r ON subcategories.id = r.subcategory_id")
.joins("INNER JOIN products AS p ON r.id = p.resellercategory_id")
.order("categories.name ASC")
.where("p.gender = 'unisex' OR p.gender = 'female'")

Rails does not always use joins to realise an include. You can force it too by doing eager_load rather than includes.

This AR chain looks a lot cleaner.
Category.joins({:subcategories =>
{:resellercategories =>
:products}})
.includes(:subcategories)
.where('products.gender = unisex OR
products.gender = ?', gender)
BUT I don't think it will solve your original problem of getting all the subcategories. To solve that you'll actually have to query the association.
#menu_categories.each do |c|
c.subcategories.joins({:resellercategories =>
:products}})
.where('products.gender = unisex OR
products.gender = ?', gender)
.each do |sc|
# do something...
end
end

Related

Ruby On Rails : Empty array but data in SQL

I've this models inmy code:
car.rb, model.rb, brand.rb
car belongs_to model
model belongs_to brand
I do this request :
select('brands.label, brands.id, count(ads.model_id) AS nbr_car').order('nbr_car DESC').joins(:model).joins('INNER JOIN brands AS brands ON brands.id = models.brand_id').group('brands.id').published.map{|c| [c.label, c.id]}
Which results in follwing SQL :
SELECT brands.label, brands.id, count(ads.model_id) AS nbr_car
FROM `ads`
INNER JOIN `models` ON `models`.`id` = `ads`.`model_id`
INNER JOIN brands AS brands ON brands.id = models.brand_id
WHERE `ads`.`type` IN ('Car')
GROUP BY brands.id
ORDER BY nbr_car DESC
In rails, I am getting empty array, but from SQL I am getting results!
What is wrong?
I think the culprit can be published here, for which I could not found any counterpart in you SQL query. Try to print result of following query:
select('brands.label, brands.id, count(ads.model_id) AS nbr_car').
order('nbr_car DESC').joins(:model).joins('INNER JOIN brands AS brands ON brands.id = models.brand_id').
group('brands.id')

Ransack no implicit conversion of Ransack::Search into Array

I am currently implementing Ransack for searching functionality.
I have a model Campaigns which collaborates campaigns that the user directly created as well as others so long as the user belongs to the same vendor.
I can combine the results as such:
#search = current_user.campaigns + current_user.vendor.campaigns.where.not(:user_id => current_user.id)
Problem with this is that Ransack will not accept this combination and spits out
no implicit conversion of Ransack::Search into Array
Can someone point me in the direction on how to refactor this code?
TIA
Adding Addition Data
When looking at my console I can see *current_user.campaigns*:
Campaign Load (0.3ms)
SELECT DISTINCT "campaigns".* FROM "campaigns"
WHERE "campaigns"."user_id" = ? [["user_id", 2]]
Running *current_user.vendor.campaigns* give me:
Campaign Load (0.4ms)
SELECT DISTINCT "campaigns".* FROM "campaigns"
INNER JOIN "weeks" ON "campaigns"."id" = "weeks"."campaign_id"
INNER JOIN "products" ON "weeks"."product_id" = "products"."id"
INNER JOIN "locations" ON "products"."location_id" = "locations"."id"
WHERE "locations"."vendor_id" = ? [["vendor_id", 2]]
I can get the first filter of current_user achieved with:
#search = Campaign.where("campaigns.user_id" => current_user.id).search(params[:q])
But I am lost of how I go about building the rest of the join tables to include both elements of data
Solved
#search = Campaign.includes(:weeks).where('(campaigns.user_id LIKE ?) OR (weeks.vendor_id LIKE ?)', current_user.id, current_user.vendor.id).search(params[:q])

rails 4 complex SQL scope

I have a model Users which has_many EventLogs.
I would like create a scope which will order Users by those with the most occurrences of EventLogs they have.
scope :highest_completed_events, .....
How can I count the number of EventLogs with a status of 2, and then order the users with the highest occurrence of that type of event.
User.joins(:event_logs).where("event_logs.status_id = 2")#... COUNT, then ORDER BY
Hope that makes sense.
Here's a query you can execute to get your users ordered by the number of events they have:
#users = User.
select("users.*, COUNT(event_logs.id) as event_logs_count").
joins('LEFT JOIN event_logs ON event_logs.user_id = users.id').
group('users.id').
order('event_logs_count DESC')
You should use a LEFT JOIN since you'll want to include users who don't have any events.
If you were to write it as a scope:
scope(:highest_completed_events, {
select: 'users.*, COUNT(event_logs.id) as event_logs_count',
joins: 'LEFT JOIN event_logs ON event_logs.user_id = users.id',
group: 'users.id',
order: 'event_logs_count DESC'
})
#users = User.highest_completed_events
In order to filter the events by a particular status, simply use a where().
#users = User.
select("users.*, COUNT(event_logs.id) as event_logs_count").
joins('LEFT JOIN event_logs ON event_logs.user_id = users.id').
where('event_logs.status = ?', STATUS_COMPLETE).
group('users.id').
order('event_logs_count DESC')
As an aside, sometimes you'll run into issues with ActiveRecord stripping out your custom select() statement when doing something like #users.count. What I normally do is nest this kind of thing in a custom from() statement.
_from = User.
select("users.*, COUNT(event_logs.id) as event_logs_count").
joins('LEFT JOIN event_logs ON event_logs.user_id = users.id').
group('users.id').
order('event_logs_count DESC').to_sql
#users = User.from("(#{_from}) as users")
#users.count # will work
Try:
User.all.sort_by{|u| u.event_logs.select{|l| l.status_id = 2}.count}.reverse
Or is it 'eventlogs'? Schouldn't your line be has_many :event_logs ?
BTW, my solution is not very efficient but DB-agnostic.

using a placeholder with joins

I'm attempting to avoid any SQL injection vulnerabilities by substituting with my params on a join.
Category.joins("LEFT OUTER JOIN incomes ON incomes.category_id = categories.id AND incomes.dept_id = ?", params[:Dept])
This attempts to execute the query with a question mark in it, instead of substituting it for the param. What is the proper way to do this?
EDIT:
Query needs to return this:
SELECT categories.*
FROM "categories"
LEFT OUTER JOIN incomes
ON incomes.category_id = categories.id AND incomes.dept_id = 86
not
SELECT categories.*
FROM "categories"
LEFT OUTER JOIN incomes
ON incomes.category_id = categories.id
WHERE incomes.dept_id = 86
Very different results!
One option is to use the sanitize_sql_array method. It is, however, a protected method so on your Category model you could do:
class Category < ActiveRecord::Base
def self.income_for_dept(dept)
Category.joins(sanitize_sql_array(["LEFT OUTER JOIN incomes ON incomes.category_id = categories.id AND incomes.dept_id = ?", dept]))
end
end
Then you would call it like:
Category.income_for_dept(params[:Dept])
Ruby provides some other methods, if need be, to get at that method without making a class method in Category.
Try
Category.joins(:incomes).where(:incomes => { :dept_id => params[:Dept] })
And check out the Rails documentation for joining tables.

How do I write a named scope to filter by all of an array passed in, and not just by matching one element (using IN)

I have two models, Project and Category, which have a many-to-many relationship between them. The Project model is very simple:
class Project < ActiveRecord::Base
has_and_belongs_to_many :categories
scope :in_categories, lambda { |categories|
joins(:categories).
where("categories.id in (?)", categories.collect(&:to_i))
}
end
The :in_categories scope takes an array of Category IDs (as strings), so using this scope I can get back every project that belongs to at least one of the categories passed in.
But what I'm actually trying to do is filter (a better name would be :has_categories). I want to just get the projects that belong to all of the categories passed in. So if I pass in ["1", "3", "4"] I only want to get the projects that belong to all of the categories.
There are two common solutions in SQL to do what you're describing.
Self-join:
SELECT ...
FROM Projects p
JOIN Categories c1 ON c1.project_id = p.id
JOIN Categories c3 ON c3.project_id = p.id
JOIN Categories c4 ON c4.project_id = p.id
WHERE (c1.id, c3.id, c4.id) = (1, 3, 4);
Note I'm using syntax to compare tuples. This is equivalent to:
WHERE c1.id = 1 AND c3.id = 3 AND c4.id = 4;
In general, the self-join solution has very good performance if you have a covering index. Probably Categories.(project_id,id) would be the right index, but analyze the SQL with EXPLAIN to be sure.
The disadvantage of this method is that you need four joins if you're searching for projects that match four different categories. Five joins for five categories, etc.
Group-by:
SELECT ...
FROM Projects p
JOIN Categories cc ON c.project_id = p.id
WHERE c.id IN (1, 3, 4)
GROUP BY p.id
HAVING COUNT(*) = 3;
If you're using MySQL (I assume you are), most GROUP BY queries invoke a temp table and this kills performance.
I'll leave it as an exercise for you to adapt one of these SQL solutions to equivalent Rails ActiveRecord API.
It seems like in ActiveRecord you would do it like so:
scope :has_categories, lambda { |categories|
joins(:categories).
where("categories.id in (?)", categories.collect(&:to_i)).
group("projects.id HAVING COUNT(projects.id) = #{categories.count}")
}