Joins across multiple tables with ActiveRecord with named scopes - ruby-on-rails

I love making named scopes for rails. however, I ran into somewhat of a pickle.
Ive gotten pretty comfortable using named scopes for joins like so:
named_scope :foo, :joins => :bar, :conditions => "bar_attribute = 'something'"
Now pretend I have a table called baz which is contains a foreign key from the bar table. I need something like this:
named_scope :foo, :joins => (:bar => :baz), :conditions => "bar.id = baz.bar_id AND baz_attribute = 'something_else'"
How possible is this?
thanks

You are not that far off. This should work for you:
named_scope: foo, :joins => {:bar => :baz}, :conditions => "bazes.your_attr = 'something_else'"
This assumes that the plural of baz is bazes. You don't need to specify the condition that joins bar.id to bazes.bar_id, it will be inferred from :joins.

Maybe you can do this in combination with a has_many :through => ... relation.

Related

pass parameter from controller to models condition

I'm trying to bind a param to a join via a named scope., but I'm getting an error.
What is the correct way to do that?
class Idea < ActiveRecord::Base
#relations
has_many :votes, :inverse_of => :idea
has_one :has_voted, :class_name => 'Vote', :conditions => ['ip = :ip']
# named scopes
scope :with_vote, lambda {|ip| {
:include => [:has_voted],
# like this ??
:conditions => [:has_voted => {:conditions => {:userIp => ip}} ]
}}
end
Idea.with_vote(request.ip).all
I believe I need the condition definition in the model for it to appear in the ON clause of a JOIN, rather then in the WHERE one.
Edit I'm trying to get the following query
select Ideas.*, Votes.* from Ideas
left outer join Votes
on Votes.Idea_id = Idea.id AND Votes.ip = {request.ip}
I do not think you can use incomplete conditions in an association.
If I understand correctly, you need Idea has many votes and votes records the request.ip and idea id.
You want the scope to retrieve all ideas your current request ip voted for.
class Idea
has_many :votes
scope :with_vote_from_ip, lambda {|ip| {
:include => [:votes],
:conditions => ['votes.ip = ?', ip]
}}
end
but if you want all ideas including only votes from current up you need extra conditions on the outer join. I think this is not possible without sql fragment:
class Idea
has_many :votes
scope :with_vote_from_ip, lambda {|ip| {
:joins => 'left outer join Votes on Votes.Idea_id = Idea.id AND Votes.ip = #{ip}'
}}
end
now Idea.with_vote_from_ip(request.ip).all should work.

ActiveRecord find all parents that have associated children

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.

how do I join and include the association

How do I use both include and join in a named scope?
Post is polymorphic
class Post
has_many :approved_comments, :class_name => 'Comment'
end
class Comment
belongs_to :post
end
Comment.find(:all, :joins => :post, :conditions =>
["post.approved = ? ", true], :include => :post)
This does not work as joins does an inner join, and include does a left out join.
The database throws an error as both joins can't be there in same query.
Have you yet tried simply omitting the :joins part of the ActiveRecord call? In my test case of a has_many association, :include will use a join if your conditions refer to the included table name. For example,
Comment.all :include => :post
will run two queries total: one for comments, and one for their posts. The Rails team says that that is better performance. However, if Rails detects that your conditions need a join,
Comment.all :include => :post, :conditions => ['post.approved = ?', true]
will run one query, since you need it.
Isn't ActiveRecord so smart?
If you want to get all the approved Posts with their Comments you can just do something like this:
Post.find(:all, :include => "comments", :conditions => ['approved = ?', true])
This will give you all the approved posts with all the comments.
You should not probably use join as it will result in too huge data set (product columns will be included for each comment related to it).

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.

Rails Error: joins + include

I have the following models:
class Person < ActiveRecord::Base
has_many :images
has_one :preference
end
class Image < ActiveRecord::Base
belongs_to :person
end
class Preference < ActiveRecord::Base
belongs_to :person
end
I am trying to fetch all images that are public and at the same time eager load the people who own those images:
Image.find(:all, :conditions => ["images.person_id = ? AND preferences.image_privacy = ?", user.id, PRIVACY_PUBLIC],
:joins => [:person => :user_preference], :include => :person)
It appears Rails does not like the :include (I believe because :person is referenced in 2 models). This is the error I get (which disappears when I drop the :include option):
"ActiveRecord::StatementInvalid: Mysql::Error: Not unique table/alias: 'people'"
I can get around this by writing out the actual JOIN command as a string and passing it into the :include option, but this not Rails-y so I was hoping there's a cleaner way to do this.
Any help would be much appreciated.
Thanks!
It looks like you call it "preferences", and not user_preferences. So you join should be:
:joins => [:person => :preference])
By using JOIN you are actively including the People table, so you shoudn't need to add "include" again. This should work:
Image.find(:all, :conditions => ["images.person_id = ? AND preferences.image_privacy = ?", user.id, PRIVACY_PUBLIC],
:joins => [:person => :user_preference])
It might be the issue with Table Aliasing, rails doc has great details in Table Aliasing
also, post SQL here will be useful too.
You wrote :conditions => ["images.person_id", user.id] and you said that you want to load images and people who owns those images. But it looks like you are loading images that belongs to one person (not to group of people), because you specify only one user.id.
I would do it this way:
Person.find(user.id, :include => [:images, :preference], :conditions => ["preferences.image_privacy = ?", PRIVACY_PUBLIC])
It will load person and his/her images.
Probably I don't understand your problem correctly, because what I think you want to do doesn't seem logic to me.
Instead of using conditions you can try named_scope

Resources