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
Related
Using Rails 5. How do I get an ActiveRecord instead of an ActiveRecord relation? I ahve the below model
class State < ActiveRecord::Base
belongs_to :country
...
def self.cached_find_us_state_by_name(name)
Rails.cache.fetch("#{name.upcase} US") do
find_us_state_by_name(name)
end
end
# Look up a US state by its full name
def self.find_us_state_by_name(name)
search_criteria = ["upper(states.name) = ? "]
search_criteria.push( "states.country_id = countries.id " )
search_criteria.push( "countries.iso = 'US' " )
results = State.joins(:country).where( search_criteria.join(' and '),
name.upcase)
end
but when I lookup an item using the methods, waht I get back is an ActiveReocrd relation ...
2.4.0 :004 > State.cached_find_us_state_by_name('Ohio')
=> #<ActiveRecord::Relation [#<State id: 3538, name: "Ohio", country_id: 233, iso: "OH">]>
This is cauisng problems later on, specifically ...
ActiveRecord::AssociationTypeMismatch: State(#70288305660760) expected, got State::ActiveRecord_Relation(#70288290686360)
from /Users/davea/.rvm/gems/ruby-2.4.0#global/gems/activerecord-5.0.1/lib/active_record/associations/association.rb:221:in `raise_on_type_mismatch!'
from /Users/davea/.rvm/gems/ruby-2.4.0#global/gems/activerecord-5.0.1/lib/active_record/associations/belongs_to_association.rb:12:in `replace'
Edit: Per the suggestion given, I changed my methods to
def self.cached_find_us_state_by_name(name)
Rails.cache.fetch("#{name.upcase} US") do
find_us_state_by_name(name)
end
end
# Look up a US state by its full name
def self.find_us_state_by_name(name)
search_criteria = ["upper(states.name) = ? "]
search_criteria.push( "states.country_id = countries.id " )
search_criteria.push( "countries.iso = 'US' " )
results = State.joins(:country).where( search_criteria.join(' and '),
name.upcase).take
end
but alas, I still get an ActiveRecord::Relation
2.4.0 :011 > State.cached_find_us_state_by_name('Ohio')
=> #<ActiveRecord::Relation [#<State id: 3538, name: "Ohio", country_id: 233, iso: "OH">]>
You can use find_by instead of where - the source code for this is simple:
def find_by(*args)
where(*args).take
end
Another way is to call .first or [0] but if your where has a lot of records this will be slow since it's loading them all into memory before selecting the first. If use limit(1) then these methods will be acceptably fast.
There are a variety of ways to get the ActiveRecord object from an ActiveRecord::Relation.The result of the call to
results = State.joins(:country).where( search_criteria.join(' and '),
name.upcase)
returns an ActiveRecord::Relation. This is always the case when you use the where clause in ActiveRecord. In order to get a specific ActiveRecord object, you could:
use the find_by method which returns the first instance in the database that it finds. Note that this does not return all of the records matching the criteria. * For example, User.find_by(first_name: "Mark") will return an ActiveRecord object containing the first record with a first name of "Mark"
Use the first method on the ActiveRecord::Relation collection. For example, User.where(first_name: "Mark").first does the same thing as the previous example.
Lastly, you could write something like this:
User.where(first_name: "Mark") do |u|
puts u.last_name
end
This allows you to iterate through the ActiveRecord::Relation collection one record at a time. So for your example, it would look something like this:
results.each do |res|
# do stuff
end
Most of ActiveRecord methods returns ActiveRecord::Relations in order to provide a fluent API ( syntactically pleasing method chaining if you will ). I think this is a design decision. There are a few method that breaks this, such as first (which really is a call to find_nth!(0) ), last to_a, to_sql and some others.
So, the way I found "around" this is to use .first where I really intended to get one and only one result back, as Mark and maxple are suggesting. I also think its probably one of the only way to do so.
This : https://github.com/rails/rails/blob/603ebae2687a0b50fcf003cb830c57f82fb795b9/activerecord/lib/active_record/querying.rb
And this (to which Querying delegates most of it's methods): https://github.com/rails/rails/blob/603ebae2687a0b50fcf003cb830c57f82fb795b9/activerecord/lib/active_record/querying.rb
are probably worth skimming through.
I'm using Rails and I have a hash object. I want to search the hash for a specific value. I don't know the keys associated with that value.
How do I check if a specific value is present in a hash? Also, how do I find the key associated with that specific value?
Hash includes Enumerable, so you can use the many methods on that module to traverse the hash. It also has this handy method:
hash.has_value?(value_you_seek)
To find the key associated with that value:
hash.key(value_you_seek)
This API documentation for Ruby (1.9.2) should be helpful.
The simplest way to check multiple values are present in a hash is:
h = { a: :b, c: :d }
h.values_at(:a, :c).all? #=> true
h.values_at(:a, :x).all? #=> false
In case you need to check also on blank values in Rails with ActiveSupport:
h.values_at(:a, :c).all?(&:present?)
or
h.values_at(:a, :c).none?(&:blank?)
The same in Ruby without ActiveSupport could be done by passing a block:
h.values_at(:a, :c).all? { |i| i && !i.empty? }
Hash.has_value? and Hash.key.
Imagine you have the following Array of hashes
available_sports = [{name:'baseball', label:'MLB Baseball'},{name:'tackle_football', label:'NFL Football'}]
Doing something like this will do the trick
available_sports.any? {|h| h['name'] == 'basketball'}
=> false
available_sports.any? {|h| h['name'] == 'tackle_football'}
=> true
While Hash#has_key? works but, as Matz wrote here, it has been deprecated in favour of Hash#key?.
Hash's key? method tells you whether a given key is present or not.
hash.key?(:some_key)
The class Hash has the select method which will return a new hash of entries for which the block is true;
h = { "a" => 100, "b" => 200, "c" => 300 }
h.select {|k,v| v == 200} #=> {"b" => 200}
This way you'll search by value, and get your key!
If you do hash.values, you now have an array.
On arrays you can utilize the Enumerable search method include?
hash.values.include?(value_you_seek)
An even shorter version that you could use would be hash.values
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'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"
I am doing an ActiveRecord find on a model as such
#foo = MyModel.find(:all, :select => 'year')
As you can see, I only need the year column from this, so my ideal output would be
["2008", "2009", "2010"]
Instead, though, I get an a hash of the models, with each one containing the year, as such:
[#<MyModel year: "2008">, #<MyModel year: "2009">, #<MyModel year: "2010">]
I can loop through it as such to convert it to my ideal output:
#years = []
for bar in #foo
#years.push(bar.year)
end
but is there a way to retrieve this result to begin with? (i.e. without going through the extra processing?). If there is not, what is a more concise way to do this processing?
Thank you.
try:
#foo = MyModel.find(:all, :select => 'year').map(&:year)
You also can call .uniq() on the result to remove duplicates.
#foo = MyModel.find(:all, :select => 'year').map(&:year).uniq
This is the short form for
#foo = MyModel.find(:all, :select => 'year').map{ |model| model.year }
This loops over the MyModel Array and crates a new array of the return values with in the block (model.year).
or even shorter code, but first calling everything from db:
#foo = MyModel.all.map(&:year)
If you don't want to instantiate models, you'd do it this way:
MyModel.connection.select_values("SELECT year FROM my_models")
This will return an Array of String objects. Then you can transform using Ruby tools as you see fit.
You can use map to change every value in an array. Example:
#foo = MyModel.find(:all, :select => 'year').map { |model_object| model_object.year }