Chaining Named Scopes not working as intended - ruby-on-rails

I have 2 simple named scopes defined as such:
class Numbers < ActiveRecord::Base
named_scope :even, :conditions => {:title => ['2','4','6']}
named_scope :odd, :conditions => {:title => ['1','3','5']}
end
if I call Numbers.even I get back 2,4,6 which is correct
if I call Numbers.odd I get back 1,3,5 which is correct
When I chain them together like this: Numbers.even.odd I get back 1,3,5 because that is the last scope I reference. So if I say Numbers.odd.even I would actually get 2,4,6.
I would expect to get 1,2,3,4,5,6 when I chain them together. One other approach I tried was this:
named_scope :even, :conditions => ["title IN (?)", ['2', '4','6']]
named_scope :odd, :conditions => ["title IN (?)", ['1', '3','5']]
But I get no results when I chain them together because the query it creates looks like this:
SELECT * FROM `numbers`
WHERE ((title IN ('1','3','5')) AND (title IN ('2','4',6')))
The 'AND' clause should be changed to OR but I have no idea how to force that. Could this be an issue with ActiveRecord??

It's an issue with how ActiveRecord handles scopes. When you apply multiple scopes, the results are joined together with AND. There is no option for using OR.
What you need to do instead is combine two result sets:
Numbers.even.all + Numbers.odd.all

Related

Rails 3 to_json include with condition/where clause

I am using the following to output a list of JSON records:
#team.people.to_json(
:include => [:user, :statistics => {:include => :attribute}]).html_safe
However, I would like to only include statistics that have a certain type_id set on them. Essentially a left outer join with the users and the statistics, where the a type_id on the statistic equals some number.
I can think of at least a couple options:
In the Person model, override to_json (or, perhaps better yet, serializable_hash) and do your conditional there.
Instead of {:include => :attribute} do {:methods => :foo} and do your conditional in foo.
Here's an example of where I overrode serializable_hash, if it helps:
def serializable_hash(options={})
options = {
:methods => [
'client',
'services',
'products',
'has_payments',
]}.update(options)
super(options)
end
I could imagine something above options = where you set the methods array to one thing if type_id is the number you're looking for, or to something else otherwise.

rails habtm: return associated records but with exclusive match

Maintaining an existing Rails 2.3.x app that has a custom role-based authorization system.
The code has something like this:
class Role << AR:Base
# has an int attribute called "level" with higher values indicating more powerful role
habtm: members
end
class Member << AR:Base
habtm: roles
end
Roles table has something like
(id, name, level)
1, admin, 1000
2, VIP, 500
3, regular, 100
4, some_other_role, 50
I have the following members with stated roles
member1 (roles: admin, VIP, regular)
member2 (roles: VIP, regular)
member3 (roles: regular)
What I need at times is pull up members based on their highest assigned role:
Role.admins_exclusively # should return member1
Role.vips_exclusively # should return just member2
Role.regulars_exclusively # should be just member3
Can't wrap my head around how to do this in Rails, without resorting to writing raw SQL queries.
Any suggestions?
Update: Mar 29th, 2012
This was my solution to basically define a bunch of methods like this (well using some dynamic programming along with define_method()) for each role.
class Member < AR:Base
define_method :vips_exclusively do
scoped :joins => :roles,
:group => 'members.id',
:having => ["max(roles.level) = ?", Role.find_by_name('vip').level]
end
end
However, I discovered that there is an issue with older rails 2.3.x. Calling size() or count() on Member.vips_exclusively for example would produce incorrect totals. Calling length() would produce correct result, but it is recommended to use size() wherever possible.
After looking at Rails code, it looks like options like :group and :having do not get passed along to count() when set in scoped(). Replacing calls to scoped() with named_scopes (update: DOES NOT) solve the counting problem.
So I incorporated Chris's proposal along with some edits for correctness/brevity. Thank you!
Another update.
Actually the issue of :group and :having not being passed is also in named_scoped implementation.
And sure enough here's a stale ticket with no fix ever making it to Rails source tree (at least not in 2.3.x branch).
https://rails.lighthouseapp.com/projects/8994/tickets/1349-named-scope-with-group-by-bug
That's great...
I don't think you'll need to write SQL queries directly, but I think you'll need a named_scope with some SQL group and having clauses to do what you're looking for:
In app/models/member.rb
named_scope :maximum_level, lambda { |level| {
:having => [ 'MAX(roles.level) = ?', level ],
:group => 'members.id', # edited to need quotes
:joins => :roles # dont need the whole join statement }
}
In app/models/role.rb
def exclusive_members
Member.maximum_level(self.level).all
end
def self.members_by_role_name(role_name)
role = self.find(:conditions => ['name = ?', role_name]).first
role.exclusive_members
end
Example
>> r = Role.find(1)
=> <Role id:1, name:"admin", level:1000>
>> r.exclusive_members
=> [ list of members with a highest role of "admin"]
>> r.exclusive_members.map { |m| m.name }
=> [ "member1" ]
>> Role.members_by_role_name("admin")
=> # the same list as you'd get by calling r.exclusive_members

Rails 2.3.x - how does this ActiveRecord scope work?

There's a named_scope in a project I'm working on that looks like the following:
# default product scope only lists available and non-deleted products
::Product.named_scope :active, lambda { |*args|
Product.not_deleted.available(args.first).scope(:find)
}
The initial named_scope makes sense. The confusing part here is how .scope(:find) works. This is clearly calling another named scope (not_deleted), and applying .scope(:find) afterwards. What/how does .scope(:find) work here?
A quick answer
Product.not_deleted.available(args.first)
is a named-scope itself, formed by combining both named scopes.
scope(:find) gets the conditions for a named-scope (or combination of scopes), which you can in turn use to create a new named-scope.
So by example:
named_scope :active, :conditions => 'active = true'
named_scope :not_deleted, :conditions => 'deleted = false'
then you write
named_scope :active_and_not_deleted, :conditions => 'active = true and deleted = false'
or, you could write
named_scope :active_and_not_deleted, lambda { self.active.not_deleted.scope(:find) }
which is identical. I hope that makes it clear.
Note that this has become simpler (cleaner) in rails 3, you would just write
scope :active_and_not_deleted, active.not_deleted
Scope is a method on ActiveRecord::Base that returns the current scope for the method passed in (what would actually be used to build the query if you were to run it at this moment).
# Retrieve the scope for the given method and optional key.
def scope(method, key = nil) #:nodoc:
if current_scoped_methods && (scope = current_scoped_methods[method])
key ? scope[key] : scope
end
end
So in your example, the lambda returns the scope for a Product.find call after merging all the other named scopes.
I have a named_scope:
named_scope :active, {:conditions => {:active => true}}
In my console output, Object.active.scope(:find) returns:
{:conditions => {:active => true}}

Complex filtering based on a many-to-many relationship

In my application, Annotations are considered "accepted" if either:
They have been explicitly marked "accepted" (i.e., their state == 'accepted')
They were last updated by a user who has the "editor" role
My question is how to find all accepted explanations with a single DB query. Basically I'm looking for the database-driven version of
Annotation.all.select do |a|
a.last_updated_by.roles.map(&:name).include?('editor') or a.state == 'accepted'
end
My first attempt was
Annotation.all(:joins => {:last_updated_by => :roles}, :conditions => ['roles.name = ? or annotations.state = ?', 'editor', 'accepted'])
But this returns a bunch of duplicate records (adding a .uniq makes it work though)
Changing :joins to :include works, but this makes the query way too slow
Are the results of your first attempt just wrong or do they only need an ".uniq"?
Have you tried
:include => {:last_updated_by => [:roles]}
instead of the join?
or making two queries
#ids = Editor.all(:conditions => ["role = 'editor'"], :select => ["id"]).map{|e|e.id}
Annotation.all(:conditions => ["last_updated_by in (?) or state = ?", #ids.join(","), "accepted"]
is that any faster?

Conditions with Bind Variables and Optional Parameters

Let's say I have a form where users can search for people whose name starts with a particular name string, for example, "Mi" would find "Mike" and "Miguel". I would probably create a find statement like so:
find(:all, :conditions => ['name LIKE ?', "#{name}%"])
Let's say the form also has two optional fields, hair_color and eye_color that can be used to further filter the results. Ignoring the name portion of the query, a find statement for people that can take in an arbitrary number of optional parameters might look like this:
find(:all, :conditions => { params[:person] })
Which for my two optional parameters would behave as the equivalent of this:
find(:all, :conditions => { :hair_color => hair_color, :eye_color => eye_color })
What I can't figure out is how to merge these two kinds of queries where the required field, "name" is applied to the "like" condition above, and the optional hair_color and eye_color parameters (and perhaps others) can be added to further filter the results.
I certainly can build up a query string to do this, but I feel there must be a "rails way" that is more elegant. How can I merge mandatory bind parameters with optional parameters?
This is the perfect use of a named scope.
create a named scope in the model:
named_scope :with_name_like, lambda {|name|
{:conditions => ['name LIKE ?', "#{name}%"]}
}
At this point you can call
Model.with_name_like("Mi").find(:all, :conditions => params[:person])
And Rails will merge the queries for you.
Edit: Code for Waseem:
If the name is optional you could either omit the named scope from your method chain with an if condition:
unless name.blank?
Model.with_name_like("Mi").find(:all, :conditions => params[:person])
else
Model.find(:all, :conditions => params[:person])
end
Or you could redefine the named scope to do the same thing.
named_scope :with_name_like, lambda {|name|
if name.blank?
{}
else
{:conditions => ['name LIKE ?', "#{name}%"]}
end
}
Update
Here is the Rails 3 version of the last code snippet:
scope :with_name_like, lambda {|name|
if not name.blank?
where('name LIKE ?', "#{name}%")
end
}
To comply also with Waseem request, but allowing nil instead blank? (which is udeful in case you want to use "#things = Thing.named_like(params[:name])" directly)
named_scope :named_like, lambda do |*args|
if (name=args.first)
{:conditions => ["name like ?",name]}
else
{}
end
end
# or oneliner version:
named_scope :named_like, lambda{|*args| (name=args.first ? {:conditions => ["name like ?",name]} : {}) } }
I hope it helps

Resources