I'd like to combine two scope condition with an inclusive or.
I tried with: scope :myscope, lambda { |u| where(cond1: u.id) or where(cond2: u.id)}, but it doesn't work. What can I do?
ActiveRecord provides some methods that bypasses the need to write SQL, but in this case you'll need to go put a very little effort and write a small piece of SQL.
scope :myscope, -> { |u| where("cond1 = ? OR cond2 = ?", u.id, u.id) }
You can also be more concise and use
scope :myscope, -> { |u| where("cond1 = :id OR cond2 = :id", id: u.id) }
There is nothing wrong in writing some SQL. Don't fall into the trap "if I can't write it in Ruby, it's ugly or not the Rails way".
There is a query method #or which you can use like this:
where(cond1: u.id).or(cond2: u.id)
Rails 5 will implement "OR" as #jphager2's answer stated, but meanwhile you have to get your hands a little dirty :
scope :myscope, lambda {where ["cond1 = ? OR cond2 = ?", u.id, u.id]}
You can do by this:-
scope :myscope, lambda { |u| where('cond1 = ? OR cond2 = ?', u.id, u.id)}
Related
I have a two scopes in my user model:
scope :hard_deactivated, where(:hard_deactivated => true)
scope :soft_deactivated, where(:soft_deactivated => true)
So far so good
OR
I want to create a scope :deactivated, which will include all users where hard_deactivated is true OR soft deactivated is true. Obviously I could just do this:
scope :deactivated, where("hard_deactivated = ? or soft_deactivated = ?", true, true)
but this does not feel very dry.
NOT
Also I would like to create an inverse scope :not_hard_deactivated. I could do this:
scope :not_hard_deactivated, where(:hard_deactivated => false)
but again, this feels bad, especially if my scope becomes more complex. There should be some way or warpping the SQL generated by the previous scope in a not clause.
Use an arel table:
hard_deactivated_true = arel_table[:hard_deactivated].eq(true)
soft_deactivated_true = arel_table[:soft_deactivated].eq(true)
scope :deactivated, where(hard_deactivated_true.and(soft_deactivated_true))
scope :not_hard_deactivated, where(hard_deactivated_true.not)
See: Is it possible to invert a named scope in Rails3?
For the "NOT" part, you can do something like this:
extend ScopeUtils
positive_and_negative_scopes :deactivated do |value|
where(:hard_deactivated => value)
end
And implement this method in a separate module:
module ScopeUtils
def positive_and_negative_scopes(name)
[true, false].each do |filter_value|
prefix = ("not_" if filter_value == false)
scope :"#{prefix}#{name}", yield(filter_value)
end
end
end
Regarding the "OR" case, you might be something similar, depending on what your recurring pattern is. In the simple example above it's not worth it, as doesn't help readability.
scopes_with_adjectives_and_negatives :deactivated, [:soft, :hard]
module ScopeUtils
def scopes_with_adjectives_and_negatives(name, kinds)
kinds.each do |kind|
positive_and_negative_scopes name do |filter_value|
where("#{kind}_#{name}" => filter_value)
end
end
scope :"#{name}", where(kinds.map{|kind| "#{kind}_#{name} = ?"}.join(" OR "), true, true)
scope :"not_#{name}", where(kinds.map{|kind| "#{kind}_#{name} = ?"}.join(" AND "), false, false)
end
end
You should use sql snippet in where method (like in your second example), or more 'sugar' gems like squeel
I have the following scope:
scope :user_reviews, lambda { |user| where(:user_id => user) }
I apply this in the controller:
def show
#review = #reviewable.reviews.user_reviews(current_user).first || Review.new
end
The first is to limit to search the current user's one and only review. Now I try to write a new scope user_review which I tried many ways to chain the user_reviews scope with first, but just couldn't get it what. Something like this:
scope :user_reviews, lambda { |user| where(:user_id => user) }
scope :user_review, lambda { |user| user_reviews(user).first }
I know the above user_review is wrong, but just trying to show you guys what I am trying to do.
How should I write this properly?
Thanks.
#Victor, just stick with your original idea. Use scope :user_reviews, lambda { |user| where(:user_id => user) } and call user_reviews.first. Nothing wrong with that.
Definitely do not define a scope that returns a single object. A scope should be chainable.
I also used this:
def self.user_review(user)
self.user_reviews(user).first
end
I feel this use case is very common.
We can solve this by following:
scope :user_reviews, lambda { | user_id | where(:user_id => user_id) }
and then in the model have user_review as class method
def self.user_review(user_id)
user_reviews(user_id).first
end
Now you can call user_review and it'll return the one record.
scope :user_reviews, ->(user) { where(user_id: :user) }
I have a few places in a model that does stuff like
def ServerInfo.starttime(param)
find(:all, :conditions => "name ='#{param}_started'", :select => "date").first.date.to_datetime
end
Now, for reasons not relevant to the question, it can happen that this particular row is not in the database at all and the code above fails with NoMethodError (undefined method `date' for nil:NilClass):. My current fix is
res = find(:all, :conditions => "name ='#{param}_started'", :select => "date")
check_time = res.first.nil? ? 0 : res.first.date.to_datetime
This works find, but I feel it's not right to sprinkle that code all over the place. Is there some more ruby-ish / rail-ish way to prevent dereferencing nil?
In order to avoid the NoMethodError for nil, you should define a begin rescue block,
def ServerInfo.starttime(param)
begin
find(:all, :conditions => "foo").first.date.to_datetime
rescue
0
end
end
I also like the Rails try method:
find(:all, :conditions => "foo").first.try(:date).try(:to_datetime) || 0
maybe this is cleaner:
check_time = res.first.date.to_datetime if res.first
btw, don't use:
:conditions => "name ='#{param}_started'" # SQL injection vulnerability.
use this one instead:
:conditions => ["name = ?", "#{param}_started"] # This is safer. Pure clean Ruby
it's safer
You may also define a scope. For instance in a Rails3 app you should try:
In your ServerInfo.rb model:
scope :starttime, lambda{|param|
if self.has_attribute?(param+'_started')
where("name = ?", param+'_started' ).select('date')
else
false
end
}
// Remember to never put your params directly in your sql query, that is bad practice since you risk some sql injection //
Then in a controller:
res = ServerInfo.starttime('a_param')
check_time = res.first.date.to_datetime if res
I didn't try that code, then you may need to adapt it to your need (or to your Rails2 app)
I have an index action in rails that can handle quite a few params eg:
params[:first_name] # can be nil or first_name
params[:age] # can be nil or age
params[:country] # can be nil or country
When finding users I would like to AND all the conditions that are not nil. This gives me 8 permutations of the find conditions.
How can I can I keep my code DRY and flexible and not end up with a bunch of if statements just to build the conditions for the find. Keep in mind that if no conditions are specified I just want to return User.all
How about something like:
conditions = params.only(:first_name, :age, :country)
conditions = conditions.delete_if {|key, value| value.blank?}
if conditions.empty?
User.all
else
User.all(:conditions => conditions)
end
I would normally use named scopes for something like this:
class User < ActiveRecord::Base
named_scope :name_like, lambda {|name| {:conditions => ["first_name LIKE ?", "#{name}%"]}}
named_scope :age, lambda {|age| {:conditions => {:age => age}}}
named_scope :in_country, lambda {|country| {:conditions => {:country => country}}}
end
class UsersController < ActionController
def index
root = User
root = root.name_like(params[:first_name]) unless params[:first_name].blank?
root = root.age(params[:age]) unless params[:age].blank?
root = root.country(params[:country]) unless params[:age].blank?
#users = root.paginate(params[:page], :order => "first_name")
end
end
That's what I normally do.
This seems to work quite nicely:
conditions = params.slice(:first_name, :age, :country)
hash = conditions.empty? ? {} : {:conditions => conditions}
#users = User.all hash
Using James Healy answer, I modify the code to be used in Rails 3.2 (in case anyone out there need this).
conditions = params.slice(:first_name, :age, :country)
conditions = conditions.delete_if {|key, value| value.blank?}
#users = User.where(conditions)
You could try Ambition, or a number of other ActiveRecord extensions.
This works for me too
conditions = params[:search] ? params[:search].keep_if{|key, value| !value.blank?} : {}
User.all(:conditions => conditions)
If you happen to be on an ancient project (Rails 2.x) and very messy, you could do something like the following for adding new fields to the original query.
Original code:
User.find(:all,
:conditions => ['first_name LIKE ? AND age=? AND country=?',
"#{name}%", age, country]
Adding a new dynamic condition on zip_code field:
zip_code = params[:zip_code] # Can be blank
zip_query = "AND zip_code = ?" unless zip_code.blank?
User.find(:all,
:conditions => ['first_name LIKE ? AND age=? AND country=? #{zip_query}',
"#{name}%", age, country, zip_code].reject(&:blank?)
Adding a reject(&:blank?) to the conditions arrays will filter the nil value.
Note: The other answers are much better if you are coding from zero, or refactoring.
I thought the following two were equivalent:
named_scope :admin, lambda { |company_id| {:conditions => ['company_id = ?', company_id]} }
named_scope :admin, lambda do |company_id|
{:conditions => ['company_id = ?', company_id]}
end
but Ruby is complaining:
ArgumentError: tried to create Proc object without a block
Any ideas?
it's a parser problem. try this
named_scope :admin, (lambda do |company_id|
{:conditions => ['company_id = ?', company_id]}
end)
I think the problem may be related to the difference in precedence between {...} and do...end
There's some SO discussion here
I think assigning a lambda to a variable (which would be a Proc) could be done with a do
... end:
my_proc = lambda do
puts "did it"
end
my_proc.call #=> did it
If you're on ruby 1.9 or later 1, you can use the lambda literal (arrow syntax), which has high enough precedence to prevent the method call from "stealing" the block from the lambda.
named_scope :admin, ->(company_id) do
{:conditions => ['company_id = ?', company_id]}
end
1 The first stable Ruby 1.9.1 release was 2009-01-30.
It's something related to precedence as I can tell
1.upto 3 do # No parentheses, block delimited with do/end
|x| puts x
end
1.upto 3 {|x| puts x } # Syntax Error: trying to pass a block to 3!