I love new Rail 3!
The new query syntax is so awesome:
users = User.where(:name => 'Bob', :last_name => 'Brown')
But when we need to do something like
SELECT * FROM Users WHERE Age >= const AND Money > const2
We have to use
users = User.where('Age >= ? and money > ?', const, const2)
Which is not very cool. The following query is not safe because of SQL injection:
users = User.where('Age >= #{const} and money > #{const2}')
I like the C#/LINQ version
var users = DB.Where(u => u.Age >= const && u.Money > const2);
Is there a way to do something like that in Rails?
The new querying with rails isn't vulnerable to SQL injection. Any quotes in the argument are escaped.
Rails 3 AR has gained the delayed execution that LINQ has had for a while. This lets you chain any of the query methods. The only time you have to put 2 or more parts into a where is when you want an OR.
That aside, there are many different ways to do your query.
Users.where('age >= ?', age).where('money > ?', money)
Users.where('age >= ? and money > ?', age, money)
class User < ActiveRecord::Base
scope :aged, lambda { |age| where('age >= ?', age) }
scope :enough_money, lambda { |money| where('money > ?', money) }
scope :can_admit, lambda { |age, money| aged(age).enough_money(money) }
end
Users.aged(18).enough_money(200)
Users.can_admit(18, 200)
You might be interested in MetaWhere with which you can write:
users = User.where(:age >= const, :money > const2)
In Rails 3 you can chain these selections together. I'm not up on the specific syntax, but this is a good start: http://railscasts.com/episodes/202-active-record-queries-in-rails-3
The basic concept is that you can chain together scopes or where clauses, etc:
meta-code here:
users = User.where(:age_of_consent).where(:has_credit)
scope :age_of_consent where("age >= ?", 18)
scope :has_credit where("credit > ?", 10)
You can pass a hash of named parameters to your query which is an improvement over the anonymous positional parameters.
users = User.where('Age >= ? and money > ?', const, const2)
becomes (is is more similar to the LINQ syntax)
users = User.where('Age >= :const and money > :const2', {:const => const, :const2 => const2})
Related
I have User and Gift models. A user can send gifts to another users. I have a relational table telling me which users received a gift. On the other hand, a user belongs to a School, which can be free or paid.
I want the count of users that have received a gift in the last week for a specific type of school (this is, free or paid).
I can do:
Gift.joins(:schools).where("created_at >= ? AND schools.free_school = ?", Time.now.beggining_of_week, true).collect(&:gift_recipients).flatten.uniq.count.
Or, I want to know how many users sent gifts the last week. This works:
Gift.joins(:schools).where("created_at >= ? AND schools.free_school = ?", Time.now.beggining_of_week, true).collect(&:user_id).uniq.count.
If I want to know how many users have sent or received a gift in the last week I can do:
(Gift.joins(:schools).where("created_at >= ? AND schools.free_school = ?", Time.now.beggining_of_week, true).collect(&:gift_recipients).flatten + Gift.joins(:schools).where("created_at >= ? AND schools.free_school = ?", Time.now.beggining_of_week, true).collect(&:user_id)).uniq.count
All this works fine but if the database is big enough this is really slow. Do you have any suggestions to make it more efficient, maybe using raw SQL where needed?
"gifts"
user_id (integer)
school_id (integer)
created_at (datetime)
updated_at (datetime)
"gift_recipients" is a table like
gift_id | recipient_id,
You do not want to do this using collect(), which is loading all of the results into memory and filtering them within an Array of ActiveRecords. This is slow and dangerous, as it could potential leak/use all of the memory available, depending on the size of the data vs. your server.
Once you post your schema I can help you query/aggregate this in SQL, which is the right way to do it.
For example, instead of:
Gift.joins(:schools).where("created_at >= ? AND schools.free_school = ?", Time.now.beggining_of_week, true).collect(&:user_id).uniq.count
You should use:
Gift.joins(:schools).where("created_at >= ? AND schools.free_school = ?", Time.now.beggining_of_week, true).count('distinct user_id')
...which will count the distinct user_ids in SQL and return the result instead of returning all of the objects and counting them in memory.
I saw this old post and I wanted to make a couple of comments:
As Winfield said
Gift.joins(:school).where("created_at >= ? AND schools.free_school = ?", Time.now.beggining_of_week, true).count('distinct user_id')
is a good way of doing this. I would do
Gift.joins(:school).count('distinct user_id', :conditions => ["gifts.created_at >= ? AND free_school = ?", Time.now.beginning_of_week, true])
but just because this is nicer to my eyes, a personal thing, you can check that both produces exactly the same SQL query. Note that is necessary to write
gifts.created_at
to avoid ambiguity because both tables has a column with this name, in the case of the column name
free_school
there is no ambiguity as this is not a column name in gifts tables. For the first query i was doing
Gift.joins(:school).where("created_at >= ? AND schools.free_school = ?", Time.now.beginning_of_week, true).collect(&:user_id).uniq.count
which is awkward. This works better
Gift.joins(:school).count("distinct user_id", :conditions => ["gifts.created_at >= ? AND free_school = ?", Time.now.beginning_of_week, true])
which avoid the problem of bringing gifts to memory and filtering them with ruby.
Up to this there's nothing new. The key point here is that my problem was calculating the number of users who sent or received a gift during the last week. For this I came up with the following
senders_ids = Gift.joins(:school).find(:all, :select => 'distinct user_id', :conditions => ['gifts.created_at >= ? AND free_school = ?', Time.now.beginning_of_week, type]).map {|g| g.user_id}
receivers_ids = Gift.joins(:school).find(:all, :select => 'distinct rec.recipient_id', :conditions => ['gifts.created_at >= ? AND free_school = ?', Time.now.beginning_of_week, type], :joins => "INNER JOIN gifts_recipients as rec on rec.gift_id = gifts.id").map {|g| g.recipient_id}
(senders_ids + receivers_ids).uniq.count
I'm pretty sure that exists a better way of doing this, I mean, returning exactly this number in a single SQL query, but at least the results are arrays of objects containing only the id (recipient_id for the receivers case), not bringing all objects into memory. Well this is just hoping to be useful for someone new to sql queries through rails like me :).
I have the following scope:
scope :this_month, :conditions => ["created_at >= ?", Date.today.beginning_of_month]
Which makes the SQL output of a.response_sets.this_month.to_sql:
SELECT "response_sets".* FROM "response_sets" WHERE created_at >= '2012-05-01'
But since today is actually June 1, that date seems wrong. So, I tried bypassing the scope and just doing a condition directly, like so:
a.response_sets.where(["created_at >= ?", Date.today.beginning_of_month]).to_sql
Which then, outputs:
SELECT "response_sets".* FROM "response_sets" WHERE created_at >= '2012-06-01'
Which is correct. So why is there a difference between doing Date.today.beginning_of_month in a scope and doing it directly in where?
When working with dates in scopes you should use a lambda so the scope gets evaluated every time it is called:
scope :this_month, -> { where("created_at >= ?", Date.today.beginning_of_month) }
This query won't return any records, when hidden_episodes_ids is empty.
:conditions => ["episodes.show_id in (?) AND air_date >= ? AND air_date <= ? AND episodes.id NOT IN (?)", #show_ids, #start_day, #end_day, hidden_episodes_ids]
If it's empty, the SQL will look like NOT IN (null)
So my solution is:
if hidden_episodes_ids.any?
*mode code*:conditions => ["episodes.show_id in (?) AND air_date >= ? AND air_date <= ? AND episodes.id NOT IN (?)", #show_ids, #start_day, #end_day, hidden_episodes_ids]
else
*mode code*:conditions => ["episodes.show_id in (?) AND air_date >= ? AND air_date <= ?", #show_ids, #start_day, #end_day]
end
But it is rather ugly (My real query is actually 5 lines, with joins and selects etc..)
Is there a way to use a single query and avoid the NOT IN (null)?
PS: These are old queries migrated into Rails 3, hence the :conditions
You should just use the where method instead as that'll help clean all of this up. You just chain it together:
scope = Thing.where(:episodes => { :show_id => #show_ids })
scope = scope.where('air_date BETWEEN ? AND ?', #start_day, #end_day)
if (hidden_episode_ids.any?)
scope = scope.where('episodes.id NOT IN (?)', hidden_episode_ids)
end
Being able to conditionally modify the scope avoids a lot of duplication.
I'm using the Rails3 beta, will_paginate gem, and the geokit gem & plugin.
As the geokit-rails plugin, doesn't seem to support Rails3 scopes (including the :origin symbol is the issue), I need to use the .find syntax.
In lieu of scopes, I need to combine two sets of criteria in array format:
I have a default condition:
conditions = ["invoices.cancelled = ? AND invoices.paid = ?", false, false]
I may need to add one of the following conditions to the default condition, depending on a UI selection:
#aged 0
lambda {["created_at IS NULL OR created_at < ?", Date.today + 30.days]}
#aged 30
lambda {["created_at >= ? AND created_at < ?", Date.today + 30.days, Date.today + 60.days]}
#aged > 90
lamdba {["created_at >= ?", Date.today + 90.days]}
The resulting query resembles:
#invoices = Invoice.find(
:all,
:conditions => conditions,
:origin => ll #current_user's lat/lng pair
).paginate(:per_page => #per_page, :page => params[:page])
Questions:
Is there an easy way to combine these two arrays of conditions (if I've worded that correctly)
While it isn't contributing to the problem, is there a DRYer way to create these aging buckets?
Is there a way to use Rails3 scopes with the geokit-rails plugin that will work?
Thanks for your time.
Try this:
ca = [["invoices.cancelled = ? AND invoices.paid = ?", false, false]]
ca << ["created_at IS NULL OR created_at < ?",
Date.today + 30.days] if aged == 0
ca << ["created_at >= ? AND created_at < ?",
Date.today + 30.days, Date.today + 60.days] if aged == 30
ca << ["created_at >= ?", Date.today + 90.days] if aged > 30
condition = [ca.map{|c| c[0] }.join(" AND "), *ca.map{|c| c[1..-1] }.flatten]
Edit Approach 2
Monkey patch the Array class. Create a file called monkey_patch.rb in config/initializers directory.
class Array
def where(*args)
sql = args[0]
unless (sql.is_a?(String) and sql.present?)
return self
end
self[0] = self[0].present? ? " #{self[0]} AND #{sql} " : sql
self.concat(args[1..-1])
end
end
Now you can do this:
cond = []
cond.where("id = ?", params[id]) if params[id].present?
cond.where("state IN (?)", states) unless states.empty?
User.all(:conditions => cond)
I think a better way is to use Anonymous scopes.
Check it out here:
http://railscasts.com/episodes/112-anonymous-scopes
I have a model who holds 2 properties: valid_from and valid_to.
I need to select all instances that are currently valid, i.e. valid_from <= today and valid_to >= today.
i have the following find :
Mymodel.find(:all, :conditions => ["valid_from <= ? and valid_to >= ?", Date.today, Date.today])
I already thought about storing Date.today in a variable and calling that variable, but i still need to call it twice.
my_date = Date.today
Mymodel.find(:all, :conditions => ["valid_from <= ? and valid_to >= ?", my_date, my_date])
Is there a way to improve and do only one call to the variable to match all the "?" in the :conditions ?
thanks,
P.
I would use named_scope. In model add:
named_scope :valid,
:conditions =>
["valid_from <= ? and valid_to >= ?", Date.today, Date.today]
And then in your controller you can call:
#mymodels = Mymodel.valid
I think that focusing on reducing two calls to Date.today to only one call is wasting of time. It won't make your application faster or using less memory.
I'm not aware of a way to do what you're asking, but even if you could I don't think it would buy you much. I would create a named scope within your model class.
In this example, you can pass the date to the named scope, or it will default to today's date if no date is specified:
named_scope :by_valid_date, lambda { |*args|
{ :conditions => ["valid_from <= ? and valid_to >= ?",
(args.first || Date.today), (args.first || Date.today)]} }