Rails ActiveRecord : search in multiple values with multiple values - ruby-on-rails

So let's say I have Post, Category and Categorizations models.
A post can have many categories through categorizations.
Now, how can I pull out all the posts that match at least one item of an array of categories?
Example:
Post 1 has categories 2,5,6
Post 2 has categories 1,5,9
Post 3 has categories 2,4,8
Find posts that match 3,5
I want the posts 1 and 2 to be returned.
Thanks!

Assuming that Categorization is a join model for Post and Category:
Post.joins(:categorizations).where(:categorizations => {:category_id => [3, 5]})
If it's not, and Categorization actually has_many :categories then:
Post.joins(:categories).where(:categories=> {:id => [3, 5]})
Note that the second method will work in the first case as well, however it will require 2 SQL joins and thus may not perform as well.

Related

Group models with exact same has_many through relationships

Forgive me if this has been asked before, I had a hard time thinking of good search queries.
Lets say I have 2 models, Posts and Tags. Posts have many tags through a pivot model, PostTags.
What I'd like to do is group posts that have the exact same combination of tags. I know how to group posts that have any of the same tags, but I've been having a harder time with this.
For example, if I have a post with and ID of 1, and the post has two Tags- one with an ID of 5, another with an ID of 7. I would have 2 PostTags, one with a post_id of 1, and a tag_id of 5, and then another with a post_id of 1 and a tag_id of 7. I have another Post with an id of 3, and it also has 2 PostTags - one with a post_id of 3 and a tag_id of 5, and another with a post_id of 3 and a tag_id of 7. I'd like to group these together so that I can get a count of how many posts have both of these tags, and no others.
Thanks, and I hope I was able to explain this properly.
I think you could probably do something like this in a nested query:
SELECT tag_ids,
string_agg(post_id, ',')
FROM (
SELECT post_id,
string_agg(tag_id, ',') as tag_ids
FROM post_tags
GROUP BY post_id)
GROUP BY tag_ids;
Explanation:
First in the inside query, you concatenate tag_ids grouped by post_id, so you can get the combination of tags for each post.
Then in the outside query, you concatenate post_ids by the combination of tag_ids, so you get all the post_ids for each tag combination.
This might not be the end yet, you could further process the post ids, or modify the query to fetch whatever data you need.
Hope this help!
Hope the Model relationships are sets properly.
# Post Model
class Post
has_many :post_tags
has_many :tags, through: :post_tags
end
# Fetch Tags to match with posts collection
tag_ids = []
# Query to fetch posts
Post.joins(:tags).where(tags: { id: tag_ids }).
group("posts.id").having("count(posts.id) >= ?", tag_ids.size)
First line ensure that only posts having tags included in the specified
tag list are fetched
Second line ensure that posts are having both of the tags.
If you want to match for exact tags (no other tags should present), then
change the condition to = instead of >=
Happy Hacking!

includes with multiple levels of foreign tables

Vacancies have matchings, matchings have rooms, rooms have messages
I need to get the vacancies that follow the specific criteria of a matching attribute and then filter them again based on wether they have messages from an employee.
Vacancy.created_this_week
.includes(:matchings, :rooms, :messages)
.where(matchings: {state: ["applied", "accepted", "denied"]})
.where(messages: {from_employee: false}.count
Although I get following:
Can't join 'Vacancy' to association named 'rooms'; perhaps you misspelled it?
I understand the association is based on a matching but how else would I get this to fit in one query since I need to filter out an amount of vacancies too?
EDIT
Based on an answer below I tried
.includes(matchings: { rooms: :messages })
Which gives me
Can't join 'Matching' to association named 'room'; perhaps you misspelled it?
Sanity check:
>> Matching.first.room.messages
=> #<ActiveRecord::Associations::CollectionProxy []>
Try nesting the includes in Hash format:
.includes(matchings: { rooms: :messages })

join in rails 4 with scope

I have Blog and Category models in my rails 4 application. there is a many to many relationship between these two models. i have multiple checkbox. I want to get all the blogs which belongs to that category. I have this in my Blog model
scope :by_categories, lambda{|category_ids| joins(:blog_categories).where("blog_categories.category_id in (?)", category_ids) if category_ids.present?}
and this in my controller
def search_blogs
#blogs = Blog.by_categories(params[:category_ids])
end
but whenever i choose multiple categories like category_ids => [1,2,3] , I am getting blogs for only category_id 1 and not for 2 and 3
Since, you are getting blogs having by categories. So use following code:
scope :by_categories, lambda{|category_ids| joins(:blog_categories).where("blogs.category_id in (?)", category_ids) if category_ids.present?}

Rails query a has_many :through conditionally with multiple ids

I'm trying to build a filtering system for a website that has locations and features through a LocationFeature model. Basically what it should do is give me all the locations based on a combination of feature ids.
So for example if I call the method:
Location.find_by_features(1,3,4)
It should only return the locations that have all of the selected features. So if a location has the feature_ids [1, 3, 5] it should not get returned, but if it had [1, 3, 4, 5] it should. However, currently it is giving me Locations that have either of them. So in this example it returns both, because some of the feature_ids are present in each of them.
Here are my models:
class Location < ActiveRecord::Base
has_many :location_features, dependent: :destroy
has_many :features, through: :location_features
def self.find_by_features(*ids)
includes(:features).where(features: {id: ids})
end
end
class LocationFeature < ActiveRecord::Base
belongs_to :location
belongs_to :feature
end
class Feature < ActiveRecord::Base
has_many :location_features, dependent: :destroy
has_many :locations, through: :location_features
end
Obviously this code isn't working the way I want it to and I just can't get my head around it. I've also tried things such as:
Location.includes(:features).where('features.id = 5 AND features.id = 9').references(:features)
but it just returns nothing. Using OR instead of AND give me either again. I also tried:
Location.includes(:features).where(features: {id: 9}, features: {id: 1})
but this just gives me all the locations with the feature_id of 1.
What would be the best way to query for a location matching all the requested features?
When you do an include it makes a "pseudo-table" in memory which has all the combinations of table A and table B, in this case joined on the foreign_key. (In this case there's already a join table included (feature_locations), to complicate things.)
There won't be any rows in this table which satisfy the condition features.id = 9 AND features.id = 1. Each row will only have a single features.id value.
What i would do for this is forget about the features table: you only need to look in the join table, location_features, to test for the presence of specific feature_id values. We need a query which will compare feature_id and location_id from this table.
One way is to get the features, then get a collection of arrays if associated location_ids (which just calls the join table), then see which location ids are in all of the arrays: (i've renamed your method to be more descriptive)
#in Location
def self.having_all_feature_ids(*ids)
location_ids = Feature.find_all_by_id(ids).map(&:location_ids).inject{|a,b| a & b}
self.find(location_ids)
end
Note1: the asterisk in *ids in the params means that it will convert a list of arguments (including a single argument, which is like a "list of one") into a single array.
Note2: inject is a handy device. it says "do this code between the first and second elements in the array, then between the result of this and the third element, then the result of this and the fourth element, etc, till you get to the end. In this case the code i'm doing between the two elements in each pair (a and b) is "&" which, when dealing with arrays, is the "set intersection operator" - this will return only elements which are in both pairs. By the time you've gone through the list of arrays doing this, only elements which are in ALL arrays will have survived. These are the ids of locations which are associated with ALL of the given features.
EDIT: i'm sure there's a way to do this with a single sql query - possibly using group_concat - which someone else will probably post shortly :)
I would do this as a set of subqueries. You can actually also do it as a scope if you wish.
scope :has_all_features, ->(*feature_ids) {
where( ( ["locations.id in (select location_id from location_features where feature_id=?)"] * feature_ids.count).join(' and '), *feature_ids)
}

Querying active record based on the value of an associations attributes

I have a Post model that has_many :feedbacks, :through => another_model. The Feedback model has a :name attribute.
I need the Posts that have feedbacks with more than 2 instances of a name.
For example:
Post One has feedbacks with names of [Like, Like, Like, Spam]
Post Two has feedbacks with names of [Dislike, Spam, Close].
I want just Post One
The best I have gotten so far is...
Posts.joins(:feedbacks).where
I know I need to have a group("name") and a having count > 2 but I cannot string together all of the clauses correctly.
EDIT WITH CORRECT QUERY
Posts.joins(:another_models).group("posts.id", "another_models.feedback_id")
.having("COUNT(another_models.feedback_id) >= ?", 2)
Thanks for the help.
Post.joins(:feedbacks).group("posts.id").having("COUNT(feedbacks.id) > 2")
Try following
Post.joins(:feedbacks).group("posts.id").having("COUNT(DISTINCT(feedbacks.name)) < COUNT(feedbacks.id)")

Resources