Need a little help with a SQL / ActiveRecord query. Let's say I have this:
Article < ActiveRecord::Base
has_many :comments
end
Comment < ActiveRecord::Base
belongs_to :article
end
Now I want to display a list of "Recently Discussed" articles - meaning I want to pull all articles and include the last comment that was added to each of them. Then I want to sort this list of articles by the created_at attribute of the comment.
I have watched the Railscast on include /joins - very good, but still a little stumped.
I think I want to use a named_scope, something to this effect:
Article < ActiveRecord::Base
has_many :comments
named_scope :recently_commented, :include => :comments, :conditions => { some_way_to_limit_just_last_comment_added }, :order => "comments.created_at DESC"
end
Using MySQL, Rails 2.3.4, Ruby 1.8.7
Any suggestions? :)
You have two solutions for this.
1) You treat n recent as n last. Then you don't need anything fancy:
Article < ActiveRecord::Base
has_many :comments
named_scope :recently_commented, :include => :comments,
:order => "comments.created_at DESC",
:limit => 100
end
Article.recently_commented # will return last 100 comments
2) You treat recent as in last x duration.
For the sake of clarity let's define recent as anything added in last 2 hours.
Article < ActiveRecord::Base
has_many :comments
named_scope :recently_commented, lambda { {
:include => :comments,
:conditions => ["comments.created_at >= ?", 2.hours.ago]
:order => "comments.created_at DESC",
:limit => 100 }}
end
Article.recently_commented # will return last 100 comments in 2 hours
Note Code above will eager load the comments associated with each selected article.
Use :joins instead of :include if you don't need eager loading.
You're gonna have to do some extra SQL for this:
named_scope :recently_commented, lambda {{
:select => "articles.*, IFNULL(MAX(comments.created_at), articles.created_at) AS last_comment_datetime",
:joins => "LEFT JOIN comments ON comments.article_id = articles.id",
:group => "articles.id",
:conditions => ["last_comment_datetime > ?", 24.hours.ago],
:order => "last_comment_datetime DESC" }}
You need to use :joins instead of :include otherwise Rails will ignore your :select option. Also don't forget to use the :group option to avoid duplicate records. Your results will have the #last_comment_datetime accessor that will return the datetime of the last comment. If the Article had no comments, it will return the Article's created_at.
Edit: Named scope now uses lambda
Related
I have two models, Post and Comment. Each post has many comments. I would like to order my posts by which post has the most recent comment.
I am attempting to set a default_scope on my Post model as follows:
default_scope :order => 'posts.comments.last.updated_at DESC'
. . but I get a PGError when I attempt that. What should I be doing?
Use this
default_scope.joins(:comments).find(:all, :order => 'comments.updated_at DESC', :group => 'id')
This works for me.
try this:
default_scope :joins => :comments, :order => 'comments.updated_at DESC', :group => 'id'
I have models like this:
class Discussion < ActiveRecord::Base
has_many :comments
has_one :special_comment, :class_name => "Comment"
end
class Comment < ActiveRecord::Base
belongs_to :discussion
# contains author
end
How can I select every Discussion through its adjoined :special_comment 'author' association. Effectively I want to do something like:
select * from discussions
inner join comments on comments.discussion_id=discussion.id
where comments.author = 'poopface'
I get something close with this:
Discussion.find(:all, :conditions => {:author => 'poopface'}, :joins => :special_comment)
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: discussions.author: SELECT "discussions".* FROM "discussions" INNER JOIN "comments" ON comments.discussion_id = discussions.id WHERE ("discussions"."author" = 'poopface')
But it should be WHERE ("comments"."author" = 'poopface')
Thanks!
Try this:
Discussion.all(:conditions => {:comments=> {:author => "blah"}},
:joins => :comments)
OR
Discussion.all(:conditions => ["comments.author = ?", "blah"],
:joins => :comments)
Note: May be you could have used a better author name in your sample code.
Assuming you have some foreignkey on your Comments table that your has_one is referencing...
Discussion.find(:all, :conditions => {:comments=>{:author => 'blah'}},
:joins => :special_comment)
will give you all Discussions where special_comment is authored by 'blah'.
Discussion.find(:all, :conditions => {:comments=>{:author => 'blah'}},
:joins => :comments)
will give you all Discussions where they have any comments authored by 'blah'.
I don't know why I can't figure this out, I think it should be fairly simple. I have two models (see below). I'm trying to come up with a named scope for SupplierCategory that would find all SupplierCategory(s) (including :suppliers) who's associated Supplier(s) are not empty.
I tried a straight up join, named_scope :with_suppliers, :joins => :suppliers which gives me only categories with suppliers, but it gives me each category listed separately, so if a category has 2 suppliers, i get the category twice in the returned array:
Currently I'm using:
named_scope :with_suppliers, :include => :suppliers
and then in my view I'm using:
<%= render :partial => 'category', :collection => #categories.find_all{|c| !c.suppliers.empty? } %>
Not exactly eloquent but illustrates what I'm trying to achieve.
Class Definitions
class SupplierCategory < AR
has_many :suppliers, :order => "name"
end
class Supplier < AR
belongs_to :supplier
end
Here is one more approach:
named_scope :with_suppliers, :include => :suppliers,
:conditions => "suppliers.id IS NOT NULL"
This works because Rails uses OUTER JOIN for include clause. When no matching rows are found the query returns NULL values for supplier columns. Hence NOT NULL check returns the matching rows.
Rails 4
scope :with_suppliers, { includes(:steps).where("steps.id IS NOT NULL") }
Or using a static method:
def self.with_suppliers
includes(:steps).where("steps.id IS NOT NULL")
end
Note:
This solution eager loads suppliers.
categories = SupplierCategory.with_suppliers
categories.first.suppliers #loaded from memory
class SupplierCategory < AR
has_many :supliers
def self.with_supliers
self.all.reject{ |c| c.supliers.empty? }
end
end
SupplierCategory.with_supliers
#=> Array of SuplierCategories with supliers
Another way more flexible using named_scope
class SupplierCategory < AR
has_many :supliers
named_scope :with_supliers, :joins => :supliers, :select => 'distinct(suplier_categories.id), suplier_categories.*', :having => "count(supliers.id) > 0"
end
SupplierCategory.with_supliers(:all, :limit => 4)
#=> first 4 SupplierCategories with suppliers
Simpler version:
named_scope :with_suppliers, :joins => :suppliers, :group => :id
If you want to use it frequently, consider using counter_cache.
I believe it would be something like
#model SupplierCategory
named_scope :with_suppliers,
:joins => :suppliers,
:select => "distinct(supplier_categories), supplier_categories.*",
:conditions => "suppliers.supplier_categories_id = supplier_categories.id"
Let me know if it works for you.
Edit:
Using fl00r's idea:
named_scope :with_suppliers,
:joins => :suppliers,
:select => "distinct(supplier_categories), supplier_categories.*",
:having => "count(supliers.id) > 0"
I believe this is the faster way.
I want to find a ordered list of runners by their results.
models
class Race < ActiveRecord::Base
has_many :runners, :dependent => :destroy
end
class Runner < ActiveRecord::Base
belongs_to :race
has_one :result, :dependent => :destroy
end
class Result < ActiveRecord::Base
belongs_to :runner
end
trying to use something like this
ordered_runners = race.runners.all(:include => :result, :order => 'results.position ASC')
position is their finishing position ie [1,2,3,4....]
but if a result is missing (nil) then the runner is not included. Is there a way to do this and return all runners?
cheers
Runners without Results are not included because :include only brings in the data minimizing the number of queries to avoid N+1 hits to the db. You want to do an outer :join to include all runners no matter if they have a result or not.
ordered_runners = race.runners.all(:joins => "left outer join results on runners.id = results.runner_id", :order => 'results.position ASC')
Check this code based on your migration column/table names and your database.
This should return the runners with a null result:
race.runners.all(:include => :result, :conditions => "results IS NULL", :order => 'results.position ASC')
Is there a shortcut for giving a limit and order when accessing a has_many relation in an ActiveRecord model?
For example, here's what I'd like to express:
#user.posts(:limit => 5, :order => "title")
As opposed to the longer version:
Post.find(:all, :limit => 5, :order => "title", :conditions => ['user_id = ?', #user.id])
I know you can specify it directly in the has_many relationship, but is there a way to do it on the fly, such as showing 10 posts on one page, but only 3 on another?
I have something similar in a blog model:
has_many :posts, :class_name => "BlogPost", :foreign_key => "owner_id",
:order => "items.published_at desc", :include => [:creator] do
def recent(limit=3)
find(:all, :limit => limit, :order => "items.published_at desc")
end
end
Usage:
Blog.posts.recent
or
Blog.posts.recent(5)
You can do it using a named scope on the post model:
class Post < ActiveRecord::Base
named_scope :limited, lambda {|*num| {:limit => num.empty? ? DEFAULT_LIMIT : num.first}}
end
This is essentially similar to utility_scopes, as reported by #Milan, except you do it piece meal, only where you need to do it.
You can use Ryan Daigle's utility_scopes. After installation (it's a gem) you will get some new useful scopes such as:
#user.posts.ordered('title ASC').limited(5)
You can even set default order and limit for any model.