Conditions in "includes" in Activerecord - ruby-on-rails-3

If I have a model called "Article" and another called "Comment", with each Article having zero or more Comments and each Comment having an associated user, how do I do the following:
Find all articles on the site and any comments that the given user has made
In SQL:
SELECT * FROM articles LEFT OUTER JOIN comments ON articles.id = comments.article_id AND comments.user_id = 2
I have tried doing this:
Article.joins('LEFT OUTER JOIN comments ON articles.id = comments.article_id AND comments.user_id = 2)
The result here is that result.first.comments give me all the comments for the article. I can solve this by adding conditions to the last part, but then it won't be eager

If you want all the comments made by a particular user and corresponding articles, this should work:
Comment.joins(:articles).where("comments.user_id" => 2).select("comments.*, articles.*")

Can you try with Article.joins(:comments).where("comments.user_id" => 2)?
Look here for more info.

Related

Filtering model with HABTM relationship

I have 2 models - Restaurant and Feature. They are connected via has_and_belongs_to_many relationship. The gist of it is that you have restaurants with many features like delivery, pizza, sandwiches, salad bar, vegetarian option,… So now when the user wants to filter the restaurants and lets say he checks pizza and delivery, I want to display all the restaurants that have both features; pizza, delivery and maybe some more, but it HAS TO HAVE pizza AND delivery.
If I do a simple .where('features IN (?)', params[:features]) I (of course) get the restaurants that have either - so or pizza or delivery or both - which is not at all what I want.
My SQL/Rails knowledge is kinda limited since I'm new to this but I asked a friend and now I have this huuuge SQL that gets the job done:
Restaurant.find_by_sql(['SELECT restaurant_id FROM (
SELECT features_restaurants.*, ROW_NUMBER() OVER(PARTITION BY restaurants.id ORDER BY features.id) AS rn FROM restaurants
JOIN features_restaurants ON restaurants.id = features_restaurants.restaurant_id
JOIN features ON features_restaurants.feature_id = features.id
WHERE features.id in (?)
) t
WHERE rn = ?', params[:features], params[:features].count])
So my question is: is there a better - more Rails even - way of doing this? How would you do it?
Oh BTW I'm using Rails 4 on Heroku so it's a Postgres DB.
This is an example of a set-iwthin-sets query. I advocate solving these with group by and having, because this provides a general framework.
Here is how this works in your case:
select fr.restaurant_id
from features_restaurants fr join
features f
on fr.feature_id = f.feature_id
group by fr.restaurant_id
having sum(case when f.feature_name = 'pizza' then 1 else 0 end) > 0 and
sum(case when f.feature_name = 'delivery' then 1 else 0 end) > 0
Each condition in the having clause is counting for the presence of one of the features -- "pizza" and "delivery". If both features are present, then you get the restaurant_id.
How much data is in your features table? Is it just a table of ids and names?
If so, and you're willing to do a little denormalization, you can do this much more easily by encoding the features as a text array on restaurant.
With this scheme your queries boil down to
select * from restaurants where restaurants.features #> ARRAY['pizza', 'delivery']
If you want to maintain your features table because it contains useful data, you can store the array of feature ids on the restaurant and do a query like this:
select * from restaurants where restaurants.feature_ids #> ARRAY[5, 17]
If you don't know the ids up front, and want it all in one query, you should be able to do something along these lines:
select * from restaurants where restaurants.feature_ids #> (
select id from features where name in ('pizza', 'delivery')
) as matched_features
That last query might need some more consideration...
Anyways, I've actually got a pretty detailed article written up about Tagging in Postgres and ActiveRecord if you want some more details.
This is not "copy and paste" solution but if you consider following steps you will have fast working query.
index feature_name column (I'm assuming that column feature_id is indexed on both tables)
place each feature_name param in exists():
select fr.restaurant_id
from
features_restaurants fr
where
exists(select true from features f where fr.feature_id = f.feature_id and f.feature_name = 'pizza')
and
exists(select true from features f where fr.feature_id = f.feature_id and f.feature_name = 'delivery')
group by
fr.restaurant_id
Maybe you're looking at it backwards?
Maybe try merging the restaurants returned by each feature.
Simplified:
pizza_restaurants = Feature.find_by_name('pizza').restaurants
delivery_restaurants = Feature.find_by_name('delivery').restaurants
pizza_delivery_restaurants = pizza_restaurants & delivery_restaurants
Obviously, this is a single instance solution. But it illustrates the idea.
UPDATE
Here's a dynamic method to pull in all filters without writing SQL (i.e. the "Railsy" way)
def get_restaurants_by_feature_names(features)
# accepts an array of feature names
restaurants = Restaurant.all
features.each do |f|
feature_restaurants = Feature.find_by_name(f).restaurants
restaurants = feature_restaurants & restaurants
end
return restaurants
end
Since its an AND condition (the OR conditions get dicey with AREL). I reread your stated problem and ignoring the SQL. I think this is what you want.
# in Restaurant
has_many :features
# in Feature
has_many :restaurants
# this is a contrived example. you may be doing something like
# where(name: 'pizza'). I'm just making this condition up. You
# could also make this more DRY by just passing in the name if
# that's what you're doing.
def self.pizza
where(pizza: true)
end
def self.delivery
where(delivery: true)
end
# query
Restaurant.features.pizza.delivery
Basically you call the association with ".features" and then you use the self methods defined on features. Hopefully I didn't misunderstand the original problem.
Cheers!
Restaurant
.joins(:features)
.where(features: {name: ['pizza','delivery']})
.group(:id)
.having('count(features.name) = ?', 2)
This seems to work for me. I tried it with SQLite though.

Rails, order by number of matched tags and then by name

Here's what I want to do: Listing has a many-to-many relationship with Tag through Taggings. I want to allow a user to search for listings by title (of the listing) and name (of zero or more tags). I want to order the number of results first by the listings with the greatest number of tags matched, and then by title.
It seems like this question has been done before -- it might be as simple as matching this question (Ordering items with matching tags by number of tags that match) from MySQL. However, I'm not SQL-literate at all, which is why I'm asking for help.
Update:
Here is an example of what I want.
Say I have 3 listings.
listing1 has tags "humor," "funny," and "hilarious."
listing2 = 2 has tags "funny," "silly," and "goofy."
listing3 = 3 has tags "funny," "silly," and "goofy."
listing4 = 4 has the tag "completely serious."
If I make a search with the tags "funny" and "silly", what I should get back is listing2, listing3, listing1, and listing4 (ignoring titles for now).
Interesting problem. I think you might have to use some SQL sugar to do this scope.
Something like this:
Listing
.joins("LEFT JOIN taggings ON taggings.listing_id = listings.id")
.joins('LEFT JOIN tags ON tags.id = taggings.tag_id AND tags.name IN ("funny","silly")')
.group(:id)
.order("count(tags.id), name DESC")
Does that help?
Assuming you want a solution in pure ActiveRecord so as not to touch any SQL...
Listing.order("tags.count DESC, title")
In this case you'd probably be better off using a counter cache for tags to optimize your queries.

How do I get all articles with 0 comments?

An article has 1 or many comments. How would I get only the articles with 0 comments?
This would be easier with a counter cache. However, I need to do this without using a counter cache.
class Article < ActiveRecord::Base
has_many :comments
scope :without_comments,
joins(<<-SQL
LEFT OUTER JOIN
(SELECT article_id
FROM comments GROUP BY article_id) AS rolled_up_comments
ON comments.article_id = articles.id
SQL
).
where("rolled_up_comments.article_id" => nil)
end
Use like this:
Article.without_comments.all
This could easily be adapted to return articles with a specific number or range of comments, e.g.:
class Article < ActiveRecord::Base
has_many :comments
scope :with_comment_count,
joins(<<-SQL
LEFT OUTER JOIN
(SELECT article_id, COUNT(*) AS comment_count
FROM comments GROUP BY article_id) AS rolled_up_comments
ON comments.article_id = articles.id
SQL
)
scope :with_n_comments, lambda {
with_comment_count.
where(:"rolled_up_comments.comment_count" => n)
}
end
In the latter case, n can be a specific number, like 100, or a range like 1..10 which ActiveRecord will turn into a BETWEEN query returning articles with 1 through 10 comments.
Note that in the 0-comment case, the count is NULL, so you can't use the range query for that.
I've tested this in Postgres. I don't know if it'll work in MySQL. I'm not sure how/if MySQL handles sub-selects for joins.
Edit: The solution pointed out by a previous commenter is easier, if you only need to know articles without comments. For count ranges, the above will work.
I'm interested by the answer.
Did you try with a scope?
I'm not sure but it could be the solution.
Rails doc : http://guides.rubyonrails.org/active_record_querying.html#scopes

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

(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

Getting the newest post from a topic using ICriteria (Nhibernate)

In a forum, I want to list the most recent posts from each topic.
In SQL I can do a subquery with group by Topic taking Max(Post.Date) and then make an inner join with the Posts table to get the latest Post record from each Topic.
How can I reproduce this in ICriteria?
DETAIL: The Topic class have NO Posts property.
TARGET SQL:
SELECT Post.*
FROM Posts, (SELECT IdTopic, MAX(DATE) AS Date FROM Posts GROUP BY IdTopic) AS MaxDates
WHERE MaxDates.IdTopic = Posts.IdTopic AND MaxDates.Date = Posts.Date
Tks[]
Patrick Coelho
By using CreateCriteria. This is explained very well by Ayende Rahien.
Edit changed to address OP's comments
I'm on shaky ground here, but this may get you started:
DetatchedCriteria posts = DetachedCriteria.For<Post>("p")
.SetProjection(Projections.Property("p.IdPost"));
.Add(Restrictions.EqProperty("t.IdPost", "p.IdPost"));
DetachedCriteria postMax = DetachedCriteria.For<Post>, "p2")
.SetProjection(Projections.Max("Date"));
var topics Session.CreateCriteria<Topic>("t")
.Add(Subqueries.Select(postMax))
.Add(Subqueries.Exists(posts)).List<Topic>();