Query through HABTM with an exact set - sql

In Rails 4, I have a vanilla has_and_belongs_to_many relationship, say between Cars and Colors.
Given a set of colors I'd like to find the cars that have exactly those colors. The closest I can get is: Car.joins(:colors).where('colors.id' => colors), but that returns all Cars with any of the colors.
I'd like to do this entirely in ActiveRecord since both tables are liable to be huge, so something like Car.joins(:colors).where('colors.id' => colors).to_a.select{|car| car.colors == colors} is less than ideal.
Any idea how to accomplish this?

I was able to get it with having and some gnarly string interpolated SQL. I've made this into a scope you can use like so:
# Car.with_exact(colors: colors)
class ActiveRecord::Base
class << self
def with_exact(associations={})
scope = self
associations.each do |association_name, set|
association = reflect_on_association(association_name)
scope = scope.joins(association_name)
.group("#{table_name}.id")
.having("COUNT(\"#{association.join_table}\".*) = ?", set.count)
.having(%{
COUNT(\"#{association.join_table}\".*) = SUM(
CASE WHEN \"#{association.join_table}\".\"#{association.association_foreign_key}\"
IN (#{set.to_a.map(&:quoted_id).join(', ')})
THEN 1 ELSE 0 END
)
}.squish)
end
scope
end
end
end

Related

Filtering records based on all members of association

I have a model called Story, which has — and belongs to many — Tags. I'm trying to create functionality to display only certain stories based on the story attributes. I do this by chaining where()s:
q = Story.where(condition1)
q = q.where(condition2)
...et cetera. One of the things I want to be able to filter on is tags, which at first I tried to do as follows:
q = q.joins(:tags)
q = q.where(tagCondition1)
q = q.where(tagCondition2)
...
However, this only finds stories that have a single tag that matches all conditions. I want to find all stories that have at least one tag that matches each condition. That is, currently if I have the conditions LIKE %al% and LIKE %be%, it will match a story with the tag 'alpha beta'; I want it to also match a story with the tag 'alpha' and the tag 'beta'.
Maybe you need the below to match multiple conditions:
q.where([tagCondition1, tagCondition2, ...])
You can use HAVING with a count.
class Story < ActiveRecord::Base
has_and_belongs_to_many :tags
def self.with_tags(*tags, min: 1)
joins(:tags)
.where(tags: { name: tags })
.group('story.id')
.having("count(*) = ?", min)
end
end
Usage:
params[:tag] = "foo bar baz"
Story.with_tags(params[:tag], *params[:tag].split)
Would include stories with any of the tags ["foo bar baz", "foo", "bar", "baz"].
The query is wrong. Since you are using AND and you want to match the first OR the second condition. Maybe you should use something like
where("tags.field like '%al% or tags.field like '%be%'")
Okay, so here's what I ended up doing: (added for anyone who's googling and has a similar problem):
conditions.each do |condition|
q_part = select('1').from('stories_tags')
q_part = q_part.where('stories_tags.story_id = stories.id')
q_part = q_part.where('stories_tags.name SIMILAR TO ?', condition)
q = q.where("EXISTS (#{q_part.to_sql})")
end

Rails 4 - query on association count is greater than an attribute

I want to do something like "Find all Books where book.pages.count < books.max_pages".
So the models are:
class Book
has_many :pages
end
class Page
belongs_to :book
end
I know I can find books w/ a set number of pages. eg:
# Get books w/ < 5 pages.
Book.joins(:pages).group("books.id").having("count(pages.id) < ?", 5)
Is there a good way to do this with a dynamic page count? eg:
Book.joins(:pages).group("books.id").select(.having("count(pages.id) <= book.max_pages")
If not I can always just store something inside the Book model (eg book.is_full = false until a save causes it to be full), but this is a bit less flexible if max_pages gets updated.
You could create a scope like this:
def self.page_count_under(amount)
joins(:pages)
.group('books.id')
.having('COUNT(pages.id) < ?', amount)
end
UPDATE
This should work if max_pages is an attribute of the Book model.
def self.page_count_under_max
joins(:pages)
.group('books.id')
.having('COUNT(pages.id) < books.max_pages')
end
Use counter_cache!
http://guides.rubyonrails.org/association_basics.html 4.1.2.3 :counter_cache

Scope with association and ActiveRecord

I have an app that records calls. Each call can have multiple units associated with it. Part of my app has a reports section which basically just does a query on the Call model for different criteria. I've figured out how to write some scopes that do what I want and chain them to the results of my reporting search functionality. But I can't figure out how to search by "unit". Below are relevant excerpts from my code:
Call.rb
has_many :call_units
has_many :units, through: :call_units
#Report search logic
def self.report(search)
search ||= { type: "all" }
# Determine which scope to search by
results = case search[:type]
when "open"
open_status
when "canceled"
cancel
when "closed"
closed
when "waitreturn"
waitreturn
when "wheelchair"
wheelchair
else
scoped
end
#Search results by unit name, this is what I need help with. Scope or express otherwise?
results = results. ??????
results = results.by_service_level(search[:service_level]) if search[:service_level].present?
results = results.from_facility(search[:transferred_from]) if search[:transferred_from].present?
results = results.to_facility(search[:transferred_to]) if search[:transferred_to].present?
# If searching with BOTH a start and end date
if search[:start_date].present? && search[:end_date].present?
results = results.search_between(Date.parse(search[:start_date]), Date.parse(search[:end_date]))
# If search with any other date parameters (including none)
else
results = results.search_by_start_date(Date.parse(search[:start_date])) if search[:start_date].present?
results = results.search_by_end_date(Date.parse(search[:end_date])) if search[:end_date].present?
end
results
end
Since I have an association for units already, I'm not sure if I need to make a scope for units somehow or express the results somehow in the results variable in my search logic.
Basically, you want a scope that uses a join so you can use a where criteria in against the associated model? Is that correct?
So in SQL you're looking for something like
select * from results r
inner join call_units c on c.result_id = r.id
inner join units u on u.call_unit_id = c.id
where u.name = ?
and the scope would be (from memory, I haven't debugged this) something like:
scope :by_unit_name, lambda {|unit_name|
joins(:units).where('units.unit_name = ?', unit_name)
}
units.name isn't a column in the db. Changing it to units.unit_name didn't raise an exception and seems to be what I want. Here's what I have in my results variable:
results = results.by_unit_name(search[:unit_name]) if search[:unit_name].present?
When I try to search by a different unit name no results show up. Here's the code I'm using to search:
<%= select_tag "search[unit_name]", options_from_collection_for_select(Unit.order(:unit_name), :unit_name, :unit_name, selected: params[:search].try(:[], :unit_name)), prompt: "Any Unit" %>

Sort Active Admin index by named scope

I have a model Slider with a named scope:
scope :positioned, order("CASE WHEN position = 0 THEN 0 ELSE 1 END DESC").order("position ASC").order("created_at DESC")
This adds a somewhat complex ordering. I'd like my ActiveAdmin to re-use that scope.
So far, I can only order by a column, like so:
ActiveAdmin.register Slider do
config.sort_order = "position_asc"
end
How can I import, re-use or force the ordering from the named scope in the active-admin index?
Depending on your version of ActiveAdmin try putting this in your Slider model:
default_scope :order("CASE WHEN position = 0 THEN 0 ELSE 1 END DESC").order("position ASC").order("created_at DESC")
Make sure you read this https://github.com/gregbell/active_admin/issues/352
good luck!

Complex Query with Has many and belongs to for RAILS 3

I am trying to do a QUERY in my controller to get a list of suppliers with a category ID.
I have my models set up like this.
class Supplier < ActiveRecord::Base
has_and_belongs_to_many :sub_categories
end
class Category < ActiveRecord::Base
has_many :sub_categories
end
class SubCategory < ActiveRecord::Base
belongs_to :category
has_and_belongs_to_many :suppliers
end
A supplier can have Many sub_categories that are under one single category. So i can grab the category of a supplier by doing this.
#supplier.sub_categories.first.category.name
This returns the category that the supplier comes under because they have to have at least 1 sub category which is then linked to a category.
What i am trying to do is by passing a category_id i wish to return all suppliers that come under that category.
I had it written like this but it doesnt seem to be working.
#category = Category.find(params[:category_id])
#suppliers = Supplier.where('sub_category.first.category.id = ?', #category.id)
i get the following sql error
Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '.id = 20)' at line 1: SELECT `suppliers`.* FROM `suppliers` WHERE (sub_category.first.category.id = 20)
Well, that's certainly an sql error. The stuff inside the where() call gets translated directly to SQL, and that's not sq;l. :)
You need to join tables together. I'm assuming there's a sub_category_suppliers table that completes the habtm association. (BTW, I much prefer to use has_many :through exclusively)
I think it would be something like this:
Supplier.joins(:sub_category_suppliers => :sub_categories).
where('sub_categories.category_id =?', #category.id).
group('suppliers.id')
As Caley Woods suggested, this should be placed in the Supplier model as a scope:
scope :by_category, lambda { |category_id|
joins(:sub_category_suppliers => :sub_categories).
where('sub_categories.category_id =?', category_id).
group('suppliers.id')
}
and then called as Supplier.by_category(#category.id)