How to eager load with a condition - ruby-on-rails

I have the classic Post with a has_many relationship on comments. I want to fetch all of the approved posts as well as the related comments which also have been approved. I have an approved scope in place for each but I am not sure how to enable it on the comments.
This is what I have so far which returns all the comments for approved postings. What is the best way without having to specify this condition on the has_many which will limit my use of this association for other queries.
Post.approved.includes(:comments)

Your example is used in the documentation. You should use another association:
class Post < ActiveRecord::Base
has_many :approved_comments, :class_name => 'Comment', :conditions => ['approved = ?', true]
end
Post.includes(:approved_comments)

Related

ActiveRecord Query Interface - Association with conditions

I have a Post model that has_many :comments. The question is: how can I build a query using ActiveRecord Query Interface that retrieves all posts and the last comment of each posts?
All I have right now is the following, but it doesn't filter the comments and I have no idea of what to do next:
Post.includes(:comments)
Well, the simplest thing comes to mind, which may not be the best is creating a scope.
class Post < ActiveRecord::Base
# has_many or maybe a has_one ? I don't have time to fire up console again to check, sorry!
has_many :last_comment, :class_name => 'Comment', :limit => 1, :order => 'comments.created_at DESC'
end
And then use
Post.includes(:last_comment)
Be sure you have indexes in your SQL.

Ordering by a related classes created_at in Rails

I've got a model User with a related class (User has_many Posts, Post belongs_to User). I want to display Users in a list, with those who have posted most recently at the top. So basically I want to order the List of Users by the created_at date of their last post. How is the best way to query this in Rails 3?
Thanks!
I think I would just do this by pulling the Posts, ordering by created_at and doing a group by on the user_id.
Maybe try something like this.
class User < ActiveRecord::Base
has_many :posts
has_one :last_post, :order => 'created_at DESC', :class_name => "Post"
scope :sort_by_last_post_desc, :include => :last_post, :order => ('posts.created_at DESC')
end
NOTE: not tested

Rails: Display products based on multiple params

I'm learning rails and trying to set up a product library where the products will be displayed based on three elements: location, category and expiry date (products can have multiple locations and categories but just one expiry date). Products will be shown as long as their expiry date hasn't passed and location and category selection will be via dropdown menus.
I started writing this question while having difficulty with incorporating the location and category selection criteria which i found a solution to but any help on what could be done better is greatly appreciated.
I've used has_many through connections to create the connections between the products, location and categories.
Here's the models:
class Product < ActiveRecord::Base
has_many :categorizations
has_many :categories, :through => :categorizations
has_many :localizations
has_many :locations, :through => :localizations
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :categorizations
has_many :products, :through => :categorizations
end
class Localization < ActiveRecord::Base
belongs_to :product
belongs_to :location
end
class Location < ActiveRecord::Base
has_many :localizations
has_many :products, :through => :localizations
end
Here's my controller. Location & category ID's are passed as params and the expiry date of the products must be greater than the current time:
class LibraryController < ApplicationController
def index
#products = Product.find(:all, include => [ :locations, :categories ],
:conditions => ['expiry_date > ? AND locations.id = ? AND categories.id = ?',
Time.now, params[:location_id],params[:category_id]])
end
end
So by passing the location_id and category_id params in the URL I can list products by a combination of both.
Is there a better way of achieving what I'm trying to do?
This will also do what you want:
#products = Product.find_all_by_category_id_and_location_id(params[:category_id], params[:location_id])
You can also user Product.where which is supposedly better than find.
For more information, Google "dynamic finders".
Ok. No, I don't think there is a "better" way in this case. There certainly are "different" ways of doing what you want, but on the face of it, what you're doing is fine, and it doesn't scream out "this code is terrible!" or anything.
Questions of advice/style are tough to answer here, because ultimately the answer to them is, "search the web for what other people are doing in your situation, and evaluate/make the decision yourself if your solution seems conventional/logical," or these kinds of questions are answered via study of relevant books on the topic.
It's nearly impossible to answer a qualitative question like this, because:
There's several ways to solve every problem, many of which are neither "right" or "wrong"
There's always edge cases where people break the "rules", in which case even unconventional solutions can absolutely be the best way to do something
You're the developer, the one building the thing. To some extent you're expected to take a leadership role, and decide what's best
The reason I ask you to define "better" is primarily because of #1 - unless you give us a specific outcome you're trying to achieve, all you'll get are (a) answers that are full of opinion, and not directed toward a specific goal or (b) simply a different way of doing something which may or may not help you. Therefore, they aren't very useful in practical terms.
You could also improve upon your solution by using, "Product.where" (preferred over find in rails 3.1) and also turn them into named_scopes in Rails like and chain them as required.
scope :not_expired, where('expiry_date > ?', Time.now)

In Rails 3 how can I select items where the items.join_model.id != x?

In my Rails models I have:
class Song < ActiveRecord::Base
has_many :flags
has_many :accounts, :through => :flags
end
class Account < ActiveRecord::Base
has_many :flags
has_many :songs, :through => :flags
end
class Flag < ActiveRecord::Base
belongs_to :song
belongs_to :account
end
I'm looking for a way to create a scope in the Song model that fetches songs that DO NOT have a given account associated with it.
I've tried:
Song.joins(:accounts).where('account_id != ?', #an_account)
but it returns an empty set. This might be because there are songs that have no accounts attached to it? I'm not sure, but really struggling with this one.
Update
The result set I'm looking for includes songs that do not have a given account associated with it. This includes songs that have no flags.
Thanks for looking.
Am I understanding your question correctly - you want Songs that are not associated with a particular account?
Try:
Song.joins(:accounts).where(Account.arel_table[:id].not_eq(#an_account.id))
Answer revised: (in response to clarification in the comments)
You probably want SQL conditions like this:
Song.all(:conditions =>
["songs.id NOT IN (SELECT f.song_id FROM flags f WHERE f.account_id = ?)", #an_account.id]
)
Or in ARel, you could get the same SQL generated like this:
songs = Song.arel_table
flags = Flag.arel_table
Song.where(songs[:id].not_in(
flags.project(:song_id).where(flags[:account_id].eq(#an_account.id))
))
I generally prefer ARel, and I prefer it in this case too.
If your where clause is not a typo, it is incorrect. Code frequently uses == for equality, but sql does not, use a single equals sign as such:
Song.joins(:accounts).where('account_id = ?', #an_account.id)
Edit:
Actually there is a way to use activerecord to do this for you, instead of writing your own bound sql fragments:
Song.joins(:accounts).where(:accounts => {:id => #an_account.id})

Loading ONE record for has_many and checking it

I'm implementing a Blog with Post and votable Comments.
When loading a Post, I want to eagerly load all votes by the current user for the Post's Comments.
Something like this (which doesn't work):
#post.comments.all(:joins => :votes, :conditions => ['votes.user_id = ?', current_user.id])
Each Comment has a method called rated_by?
def rated_by?(actor)
votes.find_by_user_id(actor.id)
end
The problem is that ActiveRecord will run a query for each rated_by? call, even though my #post.comments finder joined all the relevant votes.
I had a look at the act_as_rateable plugin but it has the same problem, running a query for each record, not using joins.
Double Secret Edit: I was answering another question and came across something that should work for you. It's a bit of a crazy hack involving the Thread.current global hash. And probably not advised at all, but it works.
It involves creating a second has_many votes association on Comments
class Comment < ActiveRecord::Base
has_many :votes
belongs_to :post
has_many :current_user_votes, :class_name => "Vote",
:conditions => '`#{Vote.table_name}`.user_id = \
#{Thread.current[:current_user].id}'
end
It also requires you to set Thread.current[:current_user] = current_user in the controller where you're going to be calling these methods.
Then you should be able to do
#post.comments.find(:all, :include => :current_user_votes)
To get a list of comments, that have eager loaded only the :current_user_votes. All in one query. If you're getting multiple posts at once, you can do this.
Post.find(:all, :include => { :comments => :current_user_votes},
:conditions => ...)
Which will populate a list of posts, and eager load their comments which in turn will each have their current_user_votes eager loaded.
Original Answer (preserved for posterity)
I don't think it's possible to select all of one model eager load only the relevant associations in one query.
The best you're going to get is pretty much what you've done. Select all of one model and then for each them load only the relevant association with a named scope or finder.
This statement that doesn't work is only selecting comments the user has voted on.
#post.comments.all(:joins => :votes,
:conditions => ['votes.user_id = ?', current_user.id])
This statement selects the same set of comments, but also eager loads all votes for the comments it selects.
#post.comments.all(:include => :votes,
:conditions => ['votes.user_id = ?', current_user.id])
Really what you're going to have to do is call rated_by? on each comment. You might be able to minimize database impact by using a named scope. But I honestly don't think it's going to make an improvement.
If you're so worried about hitting the database so hard you could do something like this:
class Post < ActiveRecord::Base
has_many :comments
has_many :votes, :through => :comments
...
end
class Vote < ActiveRecord::Base
belongs_to :comments
...
named_scope :made_by_user, lambda {|user|
{:conditions => {:user_id => user}}
}
end
#users_votes = #post.votes.made_by_use(current_user)
#comments = #post.comments.find(:all, :include => :votes)
#comments.each{|comment|
user_voted_this_on_this_comment = comment.votes & #user_votes
...
}
Honestly I don't think it's worth the effort.
P.S. There's a Ruby convention regarding methods names that end in a question mark should always return a boolean value.
you need to use
:include => :votes
joins doesn't load your data, it just join the query in the db.

Resources