ActiveRecord find all parents that have associated children - ruby-on-rails

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.

Related

how to use joins to get back a single query selected elements from two tables

I have (in Rails 3.2.13):
class User < ActiveRecord::Base
has_many :app_event_login_logouts
end
class AppEventLoginLogout < ActiveRecord::Base
belongs_to :user
end
and would like to get back something like:
AppEventLoginLogoug.select("id, type, users.email").joins(:user)
basically id, type from app_event_login_logouts and email from users but this doesn't seem to be working. What would be the correct syntax?
Try out following code:
ret = User.joins(:app_event_login_logouts).select('app_event_login_logouts.id, app_event_login_logouts.type, users.email')
ret.first.id # will return app_event_login_logouts.id
ret.first.email # will return users.email
...
AppEventLoginLogoug.find(:all,
{:include => [:users],
:select => ['id', 'type', 'users.email']})
I also checked the apidoc and found something like that:
result= AppEventLoginLogoug.find(:all,
:conditions => ['condition_here'],
:joins => [:users],
:select => 'whatever_to_select'
:order => 'your.order')

Why can't records with piggy-back attributes be saved?

I recently run into a problem where records were marked as readonly. Checking out the documentation I found this:
"Records loaded through joins with piggy-back attributes will be marked as read only since they cannot be saved. "
Why not? My model looks like the following:
class MailAccount
belongs_to :account, :class_name => "UserAccount"
named_scope :active, :joins => :account,
:conditions => "user_accounts.archived_at IS NULL"
end
I find no reason why models loaded retrieved with this named scope can not be saved. Any ideas?
It turned out I had to add :select => "mail_accounts.*" to the scope, or otherwise the query would store attributes from user_accounts in the MailAccount object, which prevented it from being saved.
So the proper code to use is:
class MailAccount
belongs_to :account, :class_name => "UserAccount"
named_scope :active, :joins => :account,
:conditions => "user_accounts.archived_at IS NULL",
:select => "mail_accounts.*"
end
When you use a :join, the ActiveRecord model for that associated object is not instantiated. You should use :include instead.

Correct ActiveRecord joins query assistance

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

Return only available items in Ruby on Rails

I've created a method that allows me to return all of the Books. I'd like to limit the books returned to those that are not not loaned. What do I need to add to available_books to ensure only unloaned books are returned. Can I leverage my preexisting loaned? method?
class Book < ActiveRecord::Base
has_many :book_loans
has_many :borrowers, :through => :book_loans, :source => :person
def loaned?
book_loans.exists?(:return_date => nil)
end
def self.available_books
#books = find(:all, :order => "title")
end
end
You can modify your find to make it look like this:
find(:all, :select => "books.*", :joins => :book_loans, :conditions => ['book_loans.return_date is null'], :order => "title")
First off you might want to consider using named scopes in place defining methods, so for example the available_books method you have written could be rewritten as
named_scope :available_books, :order => "title"
Which would allow you to write Book.available_books in the same way you are doing, but in addition you can chain multiple named scopes like Book.available_books.by_author("bob") (assuming you defined another named scope called by_author which took a name as a param.
For checking if it is loaned you could try something like:
named_scope :loaned, :joins => :book_loans, :conditions => { :book_loans => { :return_date => nil } }
Alternatively you should be able to use a string for the conditions in the same way that Vincent has done.

Shortcut for specifying an order and limit when accessing a has_many relation?

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.

Resources