Query that joins child model results item erroneously shown multiple times - ruby-on-rails

I have the following models, each a related child of the previous one (I excluded other model methods and declarations for brevity):
class Course < ActiveRecord::Base
has_many :questions
scope :most_answered, joins(:questions).order('questions.answers_count DESC') #this is the query causing issues
end
class Question < ActiveRecord::Base
belongs_to :course, :counter_cache => true
has_many: :answers
end
class Answer < ActiveRecord::Base
belongs_to :question, :counter_cache => true
end
Right now I only have one Course populated (so when I run in console Course.all.count, I get 1). The first Course currently has three questions populated, but when I run Course.most_answered.count (most_answered is my scope method written in Course as seen above), I get 3 as the result in console, which is incorrect. I have tried various iterations of the query, as well as consulting the Rails guide on queries, but can't seem to figure out what Im doing wrong. Thanks in advance.

From what I can gather, your most_answered scope is attempting to order by the sum of questions.answer_count.
As it is there is no sum, and since there are three answers for the first course, your join on to that table will produce three results.
What you will need to do is something like the following:
scope :most_answered, joins(:questions).order('questions.answers_count DESC')
.select("courses.id, courses.name, ..., SUM(questions.answers_count) as answers_count")
.group("courses.id, courses.name, ...")
.order("answers_count DESC")
You'll need to explicitely specify the courses fields you want to select so that you can use them in the group by clause.
Edit:
Both places where I mention courses.id, courses.name, ... (in the select and the group), you'll need to replace this with the actual columns you want to select. Since this is a scope it would be best to select all fields in the courses table, but you will need to specify them individually.

Related

How to get scope with polymorphic association

I am building a Rails 5 app and in this app I got two models.
First one is called Timeoff and second one is called Approval.
I want to get all Timeoff objects that got no approvals.
The time off model
class Timeoff < ApplicationRecord
scope :not_approved, -> { self.approvals.size > 0 }
has_many :approvals, as: :approvable, dependent: :destroy
end
The Approval model
class Approval < ApplicationRecord
belongs_to :approvable, polymorphic: true
end
I am calling it like this
Timeoff.not_approved
I get the error
NoMethodError: undefined method `approvals' for #<Class:0x007f9698587830>
You're trying to call approvals in the class context, but it actually belongs to an instance of Timeoff. For example:
Timeoff.approvals # doesn't work
Timeoff.first.approvals # works
That's why you get the undefined method error.
But I think you want a database query here. You could go two ways - that I know of:
Make two queries: find the timeoffs that have approvals and then query for the other ones using NOT IN
timeoff_ids = Approval.where(approvable_type: 'Timeoff').pluck(:approvable_id)
Timeoff.where.not(id: timeoff_ids)
This may get really slow if your tables are big.
Or you could do a join on the approvals table and filter to where the id is null:
Timeoff.joins("LEFT JOIN approvals ON timeoffs.id = approvals.approvable_id AND approvals.approvable_type = 'Timeoff'").where("approvals.id IS NULL")
This should also work, and may be faster - but you should measure with your own data to be sure.
Also, take a look at this question: How to select rows with no matching entry in another table? there is a complete explanation of the second query and some other ways to solve it.

Search for model by multiple join record ids associated to model by has_many in rails

I have a product model setup like the following:
class Product < ActiveRecord::Base
has_many :product_atts, :dependent => :destroy
has_many :atts, :through => :product_atts
has_many :variants, :class_name => "Product", :foreign_key => "parent_id", :dependent => :destroy
end
And I want to search for products that have associations with multiple attributes.
I thought maybe this would work:
Product.joins(:product_atts).where(parent_id: params[:product_id]).where(product_atts: {att_id: [5,7]})
But this does not seem to do what I am looking for. This does where ID or ID.
So I tried the following:
Product.joins(:product_atts).where(parent_id: 3).where(product_atts: {att_id: 5}).where(product_atts: {att_id: 7})
But this doesn't work either, it returns 0 results.
So my question is how do I look for a model by passing in attributes of multiple join models of the same model type?
SOLUTION:
att_ids = params[:att_ids] #This is an array of attribute ids
product = Product.find(params[:product_id]) #This is the parent product
scope = att_ids.reduce(product.variants) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE product_id=products.id AND att_id=?)', att_id)
end
product_variant = scope.first
This is a seemingly-simple request made actually pretty tricky by how SQL works. Joins are always just joining rows together, and your WHERE clauses are only going to be looking at one row at a time (hence why your expectations are not working like you expect -- it's not possible for one row to have two values for the same column.
There are a bunch of ways to solve this when dealing with raw SQL, but in Rails, I've found the simplest (not most efficient) way is to embed subqueries using the EXISTS keyword. Wrapping that up in a solution which handles arbitrary number of desired att_ids, you get:
scope = att_ids_to_find.reduce(Product) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE parent_id=products.id AND att_id=?)', att_id)
end
products = scope.all
If you're not familiar with reduce, what's going on is it's taking Product, then adding one additional where clause for each att_id. The end result is something like Product.where(...).where(...).where(...), but you don't need to worry about that too much. This solution also works well when mixed with scopes and other joins.

Get last comment of all posts in one category

Comment belongs to Post.
Post belongs to Category.
How would I get a collection of every lastly updated comment for each post, all belonging to one single category?
I've tried this but it just gives me one post:
category.posts.joins(:comments).order('updated_at DESC').first
Update
What I want is to fetch one commment per post, the last updated comment for each post.
Rails doesn't do this particularly well, especially with Postgres which forbids the obvious solution (as given by #Jon and #Deefour).
Here's the solution I've used, translated to your example domain:
class Comment < ActiveRecord::Base
scope :most_recent, -> { joins(
"INNER JOIN (
SELECT DISTINCT ON (post_id) post_id,id FROM comments ORDER BY post_id,updated_at DESC,id
) most_recent ON (most_recent.id=comments.id)"
)}
...
(DISTINCT ON is a Postgres extension to the SQL standard so it won't work on other databases.)
Brief explanation: the DISTINCT ON gets rid of all the rows except the first one for each post_id. It decides which row the first one is by using the ORDER BY, which has to start with post_id and then orders by updated at DESC to get the most recent, and then id as a tie-breaker (usually not necessary).
Then you would use it like this:
Comment.most_recent.joins(:post).where("posts.category_id" => category.id)
The query it generates is something like:
SELECT *
FROM comments
INNER JOIN posts ON (posts.id=comments.post_id)
INNER JOIN (
SELECT DISTINCT ON (post_id) post_id,id FROM comments ORDER BY post_id,updated_at DESC,id
) most_recent ON (most_recent.id=comments.id)
WHERE
posts.category_id=#{category.id}
Single query, pretty efficient. I'd be ecstatic if someone could give me a less complex solution though!
If you want a collection of every last updated Comment, you need to base your query on Comment, not Category.
Comment.joins(:post).
where("posts.category_id = ?", category.id).
group("posts.id").
order("comments.updated_at desc")
What you're basically asking for is a has_many :through association.
Try setting up your Category model something like this:
class Category < ActiveRecord::Base
has_many :posts
has_many :comments, through: :posts
end
Then you can simply do this to get the last 10 updated comments:
category.comments.order('updated_at DESC').limit(10)
You could make this more readable with a named scope on your Comment model:
class Comment < ActiveRecord::Base
scope :recently_updated, -> { order('updated_at DESC').limit(10) }
end
Giving you this query to use to get the same 10 comments:
category.comments.recently_updated
EDIT
So, a similar solution for what you actually wanted to ask for, however it requires you to approach your associations from the Comment end of things.
First of all, set up an association on Comment so that it has knowledge of its Category:
class Comment < ActiveRecord::Base
belongs_to :post
has_one :category, through: :post
end
Now you can query your comments like so:
Comment.order('updated_at desc').joins(:post).where('posts.category' => category).group(:post_id)
Somewhat long-winded, but it works.
.first is grabbing only one for you. The first one to be exact. So drop the .first. So instead do:
category.posts.joins(:comments).order('updated_at DESC')

Condition for association Rails 4

There's a way to condition something to an associative table of ActiveRecord?
I retrieve segments this way:
#segments = Segment.all
But, a Segment has_many products. See:
models/product.rb:
class Product < ActiveRecord::Base
belongs_to :segment, dependent: :destroy
end
models/segment.rb:
class Segment < ActiveRecord::Base
has_many :products
end
The problem is: I just want to retrieve products whose its status is equals to 1. I can condition something like this using where on Segment model, but how can I achieve this for products?
What I already tried
I found a solution. Take a look:
#segments = Segment.find(:all, include: :products, conditions: {products: {status: 1}})
It worked, but I think the code can be better.
Why I think the code can be better
Well, why should I use include: :products if the association is already live within the models? We're associating things through the model and I'm sure that is something near to enough.
Ideas?
Segment.joins(:products).where("products.status = 1")
You can also use includes instead of joins. But rails will convert it into a join internally since you are using the products table attribute in the query
A few tips, that might help you.
For easy naming purposes, I am considering the status==1 as being active. Of course I have no idea what it means in your specific case.
class Product
ACTIVE=1
def self.active
where(status: ACTIVE)
end
end
Now you write something like:
segment.products.active
and this will return only the active products for the given segment.
The solution you found, which will retrieve all segments with (active) products, could be written differently as follows:
Segment.includes(:products).where(products: {status: 1})
Now, why so elaborate: this actually translates to a sql query, so you have to be a little more explicit about it.
If you only ever want those with a status of 1
class Segment < ActiveRecord::Base
has_many :products, :conditions => { :status => 1 }
end
In rails 3 or
class Segment < ActiveRecord::Base
has_many :products, -> { where status: 1 }
end
In rails 4
Obviously can use status: true if it's a boolean
Then
#segments = Segment.includes(:products)
The association has_many :products makes it possible to use include: :products in your scope. Therefore you shouldn't doubt in your solution. It is right, and it is just the same as solutions presented in the other answers but by other syntacsis.
This should do the job - and it's compatibile with AREL syntax:
#segments = Segment.joins(:products).where(products: {status: 1})
It's quite different that solution with include (or includes, as it would be Rails 3/4), because it generates query with INNER JOIN, while includes generates LEFT OUTER JOIN. Also, includes is usually used for eager loading associated records, not for queries with JOIN.

Mongoid: How do I query for all object where the number of has_many object are > 0

I have a Gift model:
class Gift
include Mongoid::Document
include Mongoid::Timestamps
has_many :gift_units, :inverse_of => :gift
end
And I have a GiftUnit model:
class GiftUnit
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :gift, :inverse_of => :gift_units
end
Some of my gifts have gift_units, but others have not. How do I query for all the gifts where gift.gift_units.size > 0?
Fyi: Gift.where(:gift_units.exists => true) does not return anything.
That has_many is an assertion about the structure of GiftUnit, not the structure of Gift. When you say something like this:
class A
has_many :bs
end
you are saying that instance of B have an a_id field whose values are ids for A instances, i.e. for any b which is an instance of B, you can say A.find(b.a_id) and get an instance of A back.
MongoDB doesn't support JOINs so anything in a Gift.where has to be a Gift field. But your Gifts have no gift_units field so Gift.where(:gift_units.exists => true) will never give you anything.
You could probably use aggregation through GiftUnit to find what you're looking for but a counter cache on your belongs_to relation should work better. If you had this:
belongs_to :gift, :inverse_of => :gift_units, :counter_cache => true
then you would get a gift_units_count field in your Gifts and you could:
Gift.where(:gift_units_count.gt => 0)
to find what you're looking for. You might have to add the gift_units_count field to Gift yourself, I'm finding conflicting information about this but I'm told (by a reliable source) in the comments that Mongoid4 creates the field itself.
If you're adding the counter cache to existing documents then you'll have to use update_counters to initialize them before you can query on them.
I tried to find a solution for this problem several times already and always gave up. I just got an idea how this can be easily mimicked. It might not be a very scalable way, but it works for limited object counts. The key to this is a sentence from this documentation where it says:
Class methods on models that return criteria objects are also treated like scopes, and can be chained as well.
So, get this done, you can define a class function like so:
def self.with_units
ids = Gift.all.select{|g| g.gift_units.count > 0}.map(&:id)
Gift.where(:id.in => ids)
end
The advantage is, that you can do all kinds of queries on the associated (GiftUnits) model and return those Gift instances, where those queries are satisfied (which was the case for me) and most importantly you can chain further queries like so:
Gift.with_units.where(:some_field => some_value)

Resources