Filtering records based on all members of association - sql

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

Related

Combine 2 results in one in SQL

Update merge
Cama::PostType.first.posts.joins(:custom_field_values)
.where("cama_custom_fields_relationships.custom_field_slug = ? AND
cama_custom_fields_relationships.value LIKE ?","localization",
"%Paris%").merge(Cama::PostType.first.posts.joins(:custom_field_values)
.where("cama_custom_fields_relationships.custom_field_slug = ? AND
cama_custom_fields_relationships.value = ?","type-localization", "2"))
This also doesnt work. When executed seperatelty, it returns same AssociationRelation. I guess it only works for ActiveRecord:Relation
Update
I think Im looking for INTERSECT but don't know how to use it with where
There is another topic that I created and still can't find answer how to optimize it.
It goes likes this
I need to find "posts" by "other_model" values. Other model has relationship with posts throught another table but lets keep it simple. When i do
Foo.joins(:other_model).where("other_model.value = ? AND other_model.value = ?", "one", "two")
This of course won't find me any result because it contradicts itself.
When I do with OR instead of AND
Foo.joins(:other_model).where("other_model.value = ? OR other_model.value = ?", "one", "two")
It finds posts for me but... either it has one value or either has second value and...
I want to find posts based on other_model.value = one and other_model.value = two
Which means it looks for 2 seperate results and then I need to just return ids that covers each other... Does it make sense ?
I think you are looking for a query like:
Foo
.joins(:other_model)
.where('other_model.value = ? OR other_model.value = ?', 'one', 'two')
.group('foos.id')
.having('COUNT(other_models.id) >= 2')

How to make an ActiveRecord request to get an item common to several other items

I am trying to modify Sharetribe, a Ruby on Rails framework for online communities. There is this method that returns me relevant search filters.
For now, it returns me a filter if it is present in any one of the categories (identified by category_ids ) .
I would like it to return a filter if and only if it is present in ALL of the categories identified by category_ids.
Being new to Rails and ActiveRecord, I'm a bit lost. Here is the method returning relevant filters :
# Database select for "relevant" filters based on the `category_ids`
#
# If `category_ids` is present, returns only filter that belong to
# one of the given categories. Otherwise returns all filters.
#
def select_relevant_filters(category_ids)
relevant_filters =
if category_ids.present?
#current_community
.custom_fields
.joins(:category_custom_fields)
.where("category_custom_fields.category_id": category_ids, search_filter: true)
.distinct
else
#current_community
.custom_fields.where(search_filter: true)
end
relevant_filters.sort
end
Is there a way to change the SQL request, or should I retrieve all the fields as it is doing right now and then delete the ones I am not interested in ?
Try the following
def select_relevant_filters_if_all(category_ids)
relevant_filters =
if category_ids.present?
#current_community
.custom_fields
.joins(:category_custom_fields)
.where("category_custom_fields.category_id": category_ids, search_filter: true)
.group("category_custom_fields.id")
.having("count(category_custom_fields.id)=?", category_ids.count)
.distinct
else
#current_community
.custom_fields.where(search_filter: true)
end
relevant_filters.sort
end
This is a new method in your HomeController, pay attention the name is different, just to omit monkeypatching. Comments are welcome.
So I solved my problem by selecting filters that are pesent in all of the subcategories of the selected category. For that I select all filters of all subcategory, and only keep the ones that are returned a number of times exactly equal to the number of subcategory.
all_relevant_filters = select_relevant_filters(m_selected_category.own_and_subcategory_ids.or_nil)
nb_sub_category = m_selected_category.subcategory_ids.size
if nb_sub_category.none?
relevant_filters = all_relevant_filters
else
relevant_filters = all_relevant_filters.select{ |e| all_relevant_filters.count(e) == nb_sub_category.get }.uniq
end

Selecting related model: Left join, prefetch_related or select_related?

Considering I have the following relationships:
class House(Model):
name = ...
class User(Model):
"""The standard auth model"""
pass
class Alert(Model):
user = ForeignKey(User)
house = ForeignKey(House)
somevalue = IntegerField()
Meta:
unique_together = (('user', 'property'),)
In one query, I would like to get the list of houses, and whether the current user has any alert for any of them.
In SQL I would do it like this:
SELECT *
FROM house h
LEFT JOIN alert a
ON h.id = a.house_id
WHERE a.user_id = ?
OR a.user_id IS NULL
And I've found that I could use prefetch_related to achieve something like this:
p = Prefetch('alert_set', queryset=Alert.objects.filter(user=self.request.user), to_attr='user_alert')
houses = House.objects.order_by('name').prefetch_related(p)
The above example works, but houses.user_alert is a list, not an Alert object. I only have one alert per user per house, so what is the best way for me to get this information?
select_related didn't seem to work. Oh, and surely I know I can manage this in multiple queries, but I'd really want to have it done in one, and the 'Django way'.
Thanks in advance!
The solution is clearer if you start with the multiple query approach, and then try to optimise it. To get the user_alerts for every house, you could do the following:
houses = House.objects.order_by('name')
for house in houses:
user_alerts = house.alert_set.filter(user=self.request.user)
The user_alerts queryset will cause an extra query for every house in the queryset. You can avoid this with prefetch_related.
alerts_queryset = Alert.objects.filter(user=self.request.user)
houses = House.objects.order_by('name').prefetch_related(
Prefetch('alert_set', queryset=alerts_queryset, to_attrs='user_alerts'),
)
for house in houses:
user_alerts = house.user_alerts
This will take two queries, one for houses and one for the alerts. I don't think you require select related here to fetch the user, since you already have access to the user with self.request.user. If you want you could add select_related to the alerts_queryset:
alerts_queryset = Alert.objects.filter(user=self.request.user).select_related('user')
In your case, user_alerts will be an empty list or a list with one item, because of your unique_together constraint. If you can't handle the list, you could loop through the queryset once, and set house.user_alert:
for house in houses:
house.user_alert = house.user_alerts[0] if house.user_alerts else None

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" %>

How do I join records together and seperate them with "-"

I want to join records together and separate them with "-"
I know how to join one table records together like this:
#keywords = #tweet.hash_tags.join("-")
But what if it's HABTM associated tables.
For example.
// BRAND MODEL
has_and_belongs_to_many :categories
// CATEGORY MODEL
has_and_belongs_to_many :brands
If I do this:
#brands = Brand.all
#brand_categories = #brands.categories.join("-")
I get this result:
#<Category:0x0000010445c928>,#<Category:0x0000010445c7c0>,#<Category:0x0000010445c5e0>,#<Category:0x0000010445c400>,#<Category:0x0000010445c270>
Hope you understand my question - thanks.
#join will call #to_s on the items in the Array returned by #brands.categories by default, and it doesn't look like you've defined a custom Category#to_s. Either do so, or be more explicit about the string representation you want; if, for example, a Category has a title attribute, you could use:
#brands_categories = #brands.categories.map(&:title).join("-")
Assuming your Category table has a name field:
#brand_categories = #brands.categories.collect(&:name).join("-")
This will put all of the name values into an array, and then join those.