I'm manually building an SQL query where I'm using an Array in the params hash for an SQL IN statement, like: ("WHERE my_field IN('blue','green','red')"). So I need to take the contents of the array and output them into a string where each element is single quoted and comma separated (and with no ending comma).
So if the array was: my_array = ['blue','green','red']
I'd need a string that looked like: "'blue','green','red'"
I'm pretty new to Ruby/Rails but came up with something that worked:
if !params[:colors].nil?
#categories_array = params[:colors][:categories]
#categories_string =""
for x in #categories_array
#categories_string += "'" + x + "',"
end
#categories_string.chop! #remove the last comma
end
So, I'm good but curious as to what a proper and more consise way of doing this would look like?
Use map and join:
#categories_string = #categories_array.map {|element|
"'#{element}'"
}.join(',')
This functionality is built into ActiveRecord:
Model.where(:my_field => ['blue','green','red'])
Are you going to pass this string on to a ActiveRecord find method?
If so, ActiveRecord will handle this for you automatically:
categories_array = ['foo', 'bar', 'baz']
Model.find(:all, :conditions => ["category in (?)", categories_array])
# => SELECT * FROM models WHERE (category in ('foo', 'bar', 'baz'))
Hope this helps.
If you are using the parameter hash you don't have to do any thing special:
Model.all(:conditions => {:category => #categories_array})
# => SELECT * FROM models WHERE (category in ('foo', 'bar', 'baz'))
Rails (actually ActiveSupport, part of the Rails framework) offers a very nice Array#to_sentence method.
If you are using Rails or ActiveSupport, you can call
['dog', 'cat', 'bird', 'monkey'].to_sentence # "dog, cat, bird, and monkey"
Related
I know about prepared statements, but if I'm using raw SQL, does ActiveRecord have a way to manually escape values?
Something like this would be nice:
self.escape("O'Malley") # O\'Malley
You can do:
Dude.sanitize("O'Malley")
or
Dude.connection.quote("O'Malley")
both with the same result: => "'O''Malley'"
A quick dive into the ActiveRecord source reveals its method "sanitize_sql_array" for sanitizing the [string, bind_variable[, bind_variable]] type of sql statement
You could call it directly:
sql = ActiveRecord::Base.send(:sanitize_sql_array, ["insert into foo (bar, baz) values (?, ?), (?, ?)", 'a', 'b', 'c', 'd'])
res = ActiveRecord::Base.connection.execute(sql)
You can easily use the mysql2 gem to do this:
irb(main):002:0> require 'rubygems'
=> true
irb(main):003:0> require 'mysql2'
=> true
irb(main):004:0> Mysql2::Client.escape("O'Malley") # => "O\\'Malley"
=> "O\\'Malley"
Or if using the earlier mysql (not mysql2) gem:
irb(main):002:0> require 'rubygems'
=> true
irb(main):003:0> require 'mysql'
=> true
irb(main):004:0> Mysql.escape_string("O'Malley")
=> "O\\'Malley"
This will allow you to escape anything you want then insert to the db. You can also do this on most models in your rails application using the sanitize method. For instance say you have a model called Person. You could do.
Person.sanitize("O'Malley")
That should do the trick.
If you don't want the extra single quotes wrapping your string that occur when you use the solution posted by #konus, you can do this:
Dude.connection.quote_string("O'Malley")
This returns "O\'Malley" instead of "'O\'Malley'"
Even with Model.find_by_sql you can still use the form where question marks stand in as escaped values.
Simply pass an array where the first element is the query and succeeding elements are the values to be substituted in.
Example from the Rails API documentation:
Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
In case somebody is looking for a more concrete example of #jemminger's solution, here it is for bulk insert:
users_places = []
users_values = []
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
params[:users].each do |user|
users_places "(?,?,?,?)"
users_values << user[:name] << user[:punch_line] << timestamp << timestamp
end
bulk_insert_users_sql_arr = ["INSERT INTO users (name, punch_line, created_at, updated_at) VALUES #{users_places.join(", ")}"] + users_values
begin
sql = ActiveRecord::Base.send(:sanitize_sql_array, bulk_insert_users_sql_arr)
ActiveRecord::Base.connection.execute(sql)
rescue
"something went wrong with the bulk insert sql query"
end
Here is the reference to sanitize_sql_array method in ActiveRecord::Base, it generates the proper query string by escaping the single quotes in the strings. For example the punch_line "Don't let them get you down" will become "Don\'t let them get you down".
I am sending data via get and I need to put it into a int array to be used in a find.
here is my code :
#found = Array.new
params['candidate'].each do |c|
#found << c.to_i
end
My url looks like this
http://localhost:3000/export/candidate?candidate[]=3&candidate[]=4&commit=Export
If it makes any difference I am using it for this find
#candidate = Candidate.find(:all, :conditions => ["candidates.id IN ?", #found])
But currently it doesn't put it in a real array because I get this error
Mysql::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '4)' at line 1: SELECT * FROM `candidates` WHERE (candidates.id IN 4,2)
The brackets are missing around the array
Thanks and good morning!
Alex
Just put parentheses around your ?
#candidate = Candidate.find(:all, :conditions => ["candidates.id IN (?)", #found])
Also, your first snippet can be collapsed down to:
#found = params['candidate'].map(&:to_i)
The entire conversion you are making is unnecessary. You can pass the string array as a input to the query (as long as the string values represent numbers).
You can get what you need in one line:
Candidate.find_all_by_id(params[`candidate`])
Which is same as:
Candidate.find(:all, :conditions => {:id => params[`candidate`]})
Which is same as:
Candidate.find(:all, :conditions => ["id IN (?)",params[`candidate`]])
Your original attempt did not work because you did not put brackets after the IN clause.
I'm trying to pass in both the field and the value in a find call:
#employee = Employee.find(:all,
:conditions => [ '? = ?', params[:key], params[:value].to_i)
The output is
SELECT * FROM `employees` WHERE ('is_manager' = 1)
Which returns no results, however when I try this directly in mysqsl using the same call without the '' around is_manager, it works fine. How do I convert my params[:key] value to a symbol so that the resulting SQL call looks like:
SELECT * FROM `employees` WHERE (is_manager = 1)
Thanks,
D
If you want to convert a string to symbol(which is what params[:key] produces, all you need to do is
params[:key].to_s.to_sym
2 points:
A word of caution : symbols are
not garbage collected.
Make sure your key is not a
number, if you convert to_s first
then to_sym, your code will work but
you may get a wierd symbol like
this:
:"5"
You could use variable substitution for column name instead of using bind values:
# make sure the key passed is a valid column
if Employee.columns_hash[params[:key]]
Employee.all :conditions => [ "#{params[:key]} = ?", params[:value]]
end
You can further secure the solution by ensuring column name passed belongs to a pre selected set.:
if ["first_name", "last_name"].include? [params[:key]]
Employee.all :conditions => [ "#{params[:key]} = ?", params[:value]]
end
"string".to_sym
Say I have model 'Car' and controller 'cars', and a method 'display'.
I have multiple attributes like:
in_production, year, make
I can easily do something like this to find cars that match all the parameters passed:
def display
#cars = Car.find(:all, :conditions => { :in_production => #{params[:in_production]}, :year => #{params[:year]}, :make => #{params[:make]} })`
end
So what I'm doing is coding hard links in the menu, so if I wanted to find all Nissan cars from 2009 that were in production, I would pass those values as parameters in my link.
On another page I want to show every car from 2009 that is in_production, only two params instead of three. What's the best way to dynamically alter the conditions so it will work with one, two, or three params, whilst using the same action?
Any ideas?
First of all, using
:conditions => "in_production = '#{params[:in_production]}' AND year = '#{params[:year]}' AND make = '#{params[:make]}'"
is vulnerable to SQL injection. You need to escape the user provided parameters before using them in database conditions.
Something like this should let you add conditions more dynamically depending on whether or not the parameters exist. I did not test it, so I may edit it shortly...
def display
conditions = []
conditions << [ "in_production = ?", params[:in_production] ] if params[:in_production].present?
conditions << [ "year = ?", params[:year] ] if params[:year].present?
conditions << [ "make = ?", params[:make] ] if params[:make].present?
#cars = Car.all(:conditions => conditions )
end
Certainly escape the params and ensure that you only query against fields you want to be exposed. Beyond that, you could use what is built into Rails:
Car.find_all_by_in_production_and_year_and_make(in_production, year, make)
Hand-rolling the conditions may allow for additional logic to be applied (search by year only if the year is between x and y, etc). Using the rails finders (which in turn use method_missing) keeps the API clean and flexible without having to stare at direct SQL conditions.
You could construct a Car#search method that takes the entire params hash as input, where the params are sanitized and stripped of non-exposed fields, and construct the Car#find_all_by* method call using the param names themselves. Adding new conditions to search by is then as simple as passing them in the params.
You might check out searchlogic. It uses some method missing magic to construct named_scopes that would do what you want.
http://github.com/binarylogic/searchlogic
I use SmartTuple for stuff like this. Simple, powerful, designed specifically for the task.
#cars = Car.all(:conditions => (SmartTuple.new(" AND ") +
({:in_production => params[:in_production]} if params[:in_production].present?) +
({:year => params[:year]} if params[:year].present?) +
({:make => params[:make]} if params[:make].present?)
).compile)
or
#cars = Car.all(:conditions => [SmartTuple.new(" AND "),
({:in_production => params[:in_production]} if params[:in_production].present?),
({:year => params[:year]} if params[:year].present?),
({:make => params[:make]} if params[:make].present?),
].sum.compile)
or
keys = [:in_production, :year, :make]
#cars = Car.all(:conditions => (SmartTuple.new(" AND ").add_each(keys) do |k|
{k => params[k]} if params[k].present?
end).compile)
Pick the one you like the most. :)
I'm doing custom find_by_sql queries which are dynamically created by user input. What is the best way to find the field names returned by find_by_sql
I've tried using columns, column_names and keys methods but none work.
Is the ActiveRecord result a hash or an array?
e.g.
#fm_reports = FmReport.find_by_sql(crosstab_query)
field_names = #fm_reports.keys (or .columns or .column_names)
Cheers, Keith
Update ::
It seems that unless you do "select * from ...." in a find_by_sql it does not return the attribute names
>> FmReport.find_by_sql("select object_class, alarm_mr from fm_reports limit 1")
=> [#<FmReport >]
>> FmReport.find_by_sql("select * from fm_reports limit 1")
=> [#<FmReport id: 7, ALARM_DN: "PLMN-PLMN/BSC-31569/TCSM-72", ALARM_OBJECT: "MELB_BSC1", ALARM_MR: "VIC_METRO", PARENT_DN: "PLMN-PLMN/BSC-31569", PARENT_CLASS: "BSC", OBJECT_CLASS: "TCSM", ALARM_NUMBER: "2955", ALARM_TIME: "21/12/2009 11:02:19", CANCEL_TIME: "21/12/2009 11:03:27", SEVERITY: "2", created_at: nil, updated_at: nil>]
#fm_reports = FmReport.find_by_sql(crosstab_query)
field_values = #fm_reports.attributes
field_values.each {|key, value| puts key}
The above line will return a hashmap of field-names and their values. They can be iterated on if req.
http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002353
attribute_names
Maybe you're looking for #attributes?
Also, find_by_sql returns an Array so that's why there's no method called attributes. How about doing first.attributes on the result of your find?
Another question is why are you using find_by_sql at all?
Why not just use ActiveRecord's built in stuff for this?
SomeModel.find( :all, :limit => 1 ).first.attributes
I know this is old, but it might help anyone with the same question.
I usually do it like this:
#fm_reports = FmReport.find_by_sql(crosstab_query)
field_names = #fm_reports.first.attributes.keys