Get columns names with ActiveRecord - ruby-on-rails

Is there a way to get the actual columns name with ActiveRecord?
When I call find_by_sql or select_all with a join, if there are columns with the same name, the first one get overridden:
select locations.*, s3_images.* from locations left join s3_images on s3_images.imageable_id = locations.id and s3_images.imageable_type = 'Location' limit 1
In the example above, I get the following:
#<Location id: 22, name: ...
>
Where id is that of the last s3_image. select_rows is the only thing that worked as expected:
Model.connection.select_rows("SELECT id,name FROM users") => [["1","amy"],["2","bob"],["3","cam"]]
I need to get the field names for the rows above.
This post gets close to what I want but looks outdated (fetch_fields doesn't seem to exist anymore How do you get the rows and the columns in the result of a query with ActiveRecord? )
The ActiveRecord join method creates multiple objects. I'm trying to achieve the same result "includes" would return but with a left join.
I am attempting to return a whole lot of results (and sometimes whole tables) this is why includes does not suit my needs.

Active Record provides a #column_names method that returns an array of column names.
Usage example: User.column_names

two options
Model.column_names
or
Model.columns.map(&:name)
Example
Model named Rabbit with columns name, age, on_facebook
Rabbit.column_names
Rabbit.columns.map(&:name)
returns
["id", "name", "age", "on_facebook", "created_at", "updated_at"]

This is just way active record's inspect method works: it only lists the column's from the model's table. The attributes are still there though
record.blah
will return the blah attribute, even if it is from another table. You can also use
record.attributes
to get a hash with all the attributes.
However, if you have multiple columns with the same name (e.g. both tables have an id column) then active record just mashes things together, ignoring the table name.You'll have to alias the column names to make them unique.

Okay I have been wanting to do something that's more efficient for a while.
Please note that for very few results, include works just fine. The code below works better when you have a lot of columns you'd like to join.
In order to make it easier to understand the code, I worked out an easy version first and expanded on it.
First method:
# takes a main array of ActiveRecord::Base objects
# converts it into a hash with the key being that object's id method call
# loop through the second array (arr)
# and call lamb (a lambda { |hash, itm| ) for each item in it. Gets called on the main
# hash and each itm in the second array
# i.e: You have Users who have multiple Pets
# You can call merge(User.all, Pet.all, lambda { |hash, pet| hash[pet.owner_id].pets << pet }
def merge(mainarray, arr, lamb)
hash = {}
mainarray.each do |i|
hash[i.id] = i.dup
end
arr.each do |i|
lamb.call(i, hash)
end
return hash.values
end
I then noticed that we can have "through" tables (nxm relationships)
merge_through! addresses this issue:
# this works for tables that have the equivalent of
# :through =>
# an example would be a location with keywords
# through locations_keywords
#
# the middletable should should return as id an array of the left and right ids
# the left table is the main table
# the lambda fn should store in the lefthash the value from the righthash
#
# if an array is passed instead of a lefthash or a righthash, they'll be conveniently converted
def merge_through!(lefthash, righthash, middletable, lamb)
if (lefthash.class == Array)
lhash = {}
lefthash.each do |i|
lhash[i.id] = i.dup
end
lefthash = lhash
end
if (righthash.class == Array)
rhash = {}
righthash.each do |i|
rhash[i.id] = i.dup
end
righthash = rhash
end
middletable.each do |i|
lamb.call(lefthash, righthash, i.id[0], i.id[1])
end
return lefthash
end
This is how I call it:
lambmerge = lambda do |lhash, rhash, lid, rid|
lhash[lid].keywords << rhash[rid]
end
Location.merge_through!(Location.all, Keyword.all, LocationsKeyword.all, lambmerge)
Now for the complete method (which makes use of merge_through)
# merges multiple arrays (or hashes) with the main array (or hash)
# each arr in the arrs is a hash, each must have
# a :value and a :proc
# the procs will be called on values and main hash
#
# :middletable will merge through the middle table if provided
# :value will contain the right table when :middletable is provided
#
def merge_multi!(mainarray, arrs)
hash = {}
if (mainarray.class == Hash)
hash = mainarray
elsif (mainarray.class == Array)
mainarray.each do |i|
hash[i.id] = i.dup
end
end
arrs.each do |h|
arr = h[:value]
proc = h[:proc]
if (h[:middletable])
middletable = h[:middletable]
merge_through!(hash, arr, middletable, proc)
else
arr.each do |i|
proc.call(i, hash)
end
end
end
return hash.values
end
Here's how I use my code:
def merge_multi_test()
merge_multi!(Location.all,
[
# each one location has many s3_images (one to many)
{ :value => S3Image.all,
:proc => lambda do |img, hash|
if (img.imageable_type == 'Location')
hash[img.imageable_id].s3_images << img
end
end
},
# each location has many LocationsKeywords. Keywords is the right table and LocationsKeyword is the middletable.
# (many to many)
{ :value => Keyword.all,
:middletable => LocationsKeyword.all,
:proc => lambda do |lhash, rhash, lid, rid|
lhash[lid].keywords << rhash[rid]
end
}
])
end
You can modify the code if you wish to lazy load attributes that are one to many (such as a City is to a Location) Basically, the code above won't work because you'll have to loop through the main hash and set the city from the second hash (There is no "city_id, location_id" table). You could reverse the City and Location to get all the locations in the city hash then extract back. I don't need that code yet so I skipped it =)

Related

determining if at least one pair of fields are filled out

So in my rails app I have a column of text boxes in pairs. There are 4 pairs of textboxes.
Just one of these pairs of text box needs to be filled out (with both textboxes in that pair filled out) for it to be valid.
So I'm trying to loop through them all and add them to a multi dimensional array and to check that at least one row(?) in the array has both values.
def no__values?
all_values = Array.new
txt_a_values = Array.new
txt_b_values = Array.new
self.item.line_items.each do |item|
txt_a_values << item.single.nil? # this is a value from the text box a
txt_b_values << item.aggregate.nil? # this is a value from the text box b
end
all_values << txt_a_values #create multi dimensional array
all_values << txt_b_values
all_values.each do |v|
??? # there needs to be one pair in the array which has both values
end
end
So it should create an array like this
[true][true] # both textboxes are nil
[false][false] # both textboxes have values
[true][true] # both textboxes are nil
[true][true] # both textboxes are nil
the above scenario is valid since there is one pair which BOTH have values
I really don't like the way I'm progressing with this, so I',m looking for some help.
Couldn't you do something like this:
def no__values?
self.item.line_items.each do |item|
return false if !item.single.nil? && !item.aggregate.nil?
end
true
end
That will return false when both values of a pair isn't null, and returns true if each pair had at least one null value.
Edit:
Based on the method name I didn't create the array and instead returns false/true directly.
I think you are looking for the following snippets to solve your problems. I have done it with little simplification. Please let me know if you are looking for this.
def no__values?
all_values = []
self.item.line_items.each do |item|
all_values << [item.single.nil?, item.aggregate.nil?]
end
all_values.each do |v|
puts v # [true, false] or [true, true], [false, false]....
end
end

Conditionally appending models to collection

I'm trying to conditionally build up a list of models. I.e.
#items = []
if some_condition
#items << MyModel.where(...)
end
if another_condition
#items << MyModel.where(...)
end
...
This isn't working. It will actually build up an array of the correct objects but when I access fields and relationships of items in the array, they are 'not found'. I tried a few other things like #items = MyModel.none and #items = {} and .merge but none work. I cant seem to figure this out.
What is the best way to conditionally build up a collection like this?
Update I would like to be able to maintain the Relation so that I can continue to query it with .where, .first and the rest of the Relation methods.
<< will append an item to the array, so the query won't be run and instead will be appended as a ActiveRecord::Relation and if you use all you will end up with an array of arrays. You should use concat to append entire collections (+= will also work, but will instantiate unnecesary temp arrays impacting performance if your queries return a lot of records):
#items = []
if some_condition
#items.concat(MyModel.where(...))
end
if another_condition
#items.concat(MyModel.where(...))
end
Your concatenation approach will result in multiple database queries and will not be chain-able which is often needed for pagination, scoping, grouping and ordering.
I would instead collect your conditions and combine at the end. It appears your conditions are in fact OR type queries, as opposed to AND type queries which are more easily chained.
So do the following:
#queries = []
if some_condition
# Relation condition
#queries << MyModel.where(...)
end
if another_condition
# another Relation condition
#queries << MyModel.where(...)
end
if and_another_condition
# Hash equality and IN conditions
#queries << { attr1: 'foo', attr2: [1,2,3] }
end
if yet_another_condition
# string condition with argument
#queries << ['attr LIKE ? ', arg]
end
#items = MyModel.any_of(*queries).order(...).page(...).per(...)
The magic is in a nifty custom AR extension method any_of? for combining OR type queries using Arel. It can take Relations, String conditions, Hash conditions, or Arrays for splatting into where() clauses.
# put in config/initializers/ar_any_of.rb or in lib/xxxx
class ActiveRecord::Base
def self.any_of(*queries)
where(
queries.map { |query|
query = where(query) if [String, Hash].any? { |type| query.kind_of? type }
query = where(*query) if query.kind_of? Array
query.arel.constraints.reduce(:and)
}.reduce(:or)
)
end
end
Which can be used with a variety of conditions as follows to produce a single SQL:
Country.any_of(
Country.where(alpha2: 'AU'),
{ alpha2: ['NZ', 'UK'] },
['alpha2 LIKE ?', 'U%']).to_sql
# => "SELECT \"countries\".* FROM \"countries\" WHERE (((\"countries\".\"alpha2\" = 'AU' OR \"countries\".\"alpha2\" IN ('NZ', 'AD')) OR (alpha2 LIKE 'U%')))"
I think the answer is quite simpler.
You can initialize your collection as an anonymous scope
#items = MyModel.scoped
which is an ActiveRecord::Relation.
**note that scoped is deprecated on RoR 4 BUT all does the same thing http://blog.remarkablelabs.com/2012/12/what-s-new-in-active-record-rails-4-countdown-to-2013 so our example will be
#items = MyModel.all
After that, chaining extra conditions (by condition) should be really easy:
#items = #items.where(owner: me) if me.present?
#items = #items.group(:attribute_1) if show_groups

Ruby list values are identical after assigning different values

Sorry for the confusing title, not sure how to describe this issue.
Inside a Ruby on Rails controller I'm creating a list named #commits, where each item in #commits should contain a hash table whose elements are the values of various properties for each commit. These property values are stored in a Redis database.
Below, I iterate through a list of properties whose values should be grabbed from Redis, and then grab those values for each of 8 different commits. Then I place the values from redis into a different hash table for each commit, using the commit property name as the key for the hash.
# Initialize #commits as a list of eight empty hash tables
#commits = Array.new(8, {})
# Iterate over the attributes that need hashed for each item in #commits
[:username, :comment, :rev, :repo].each do |attrib|
# 8 items in #commits
8.times do |i|
# Get a value from redis and store it in #commits[i]'s hash table
#commits[i][attrib] = $redis.lindex(attrib, i)
# Print the value stored in the hash
# Outputs 7, 6, .., 0 for #commits[i][:rev]
puts #commits[i][attrib].to_s
end
end
# Print the value of every item that was stored in the hash tables above,
# but only for the :rev key
# Outputs 0 eight times
8.times do |i|
puts #commits[i][:rev]
end
However, per the comments above, #commits[0..7] all seem to have the same values in their hashes, despite them being seemingly stored correctly a few lines above. Using the hash key :rev as an example, the first puts outputs 7..0, which is correct, but the second puts outputs the number 0 eight times.
Anyone know why?
It would help if you show how #commits is initialized, but it looks like you've created a structure with multiple references to the same object.
Incorrect, same object recycled for all keys:
#commits = Hash.new([ ])
Correct, new object created for each key:
#commits = Hash.new { |h, k| h[k] = [ ] }
You could be using an Array with the same mistake:
#commits = Array.new(8, [ ])
This will lead to the following behaviour:
a = Array.new(4, [ ])
a[0]
# => []
a[0] << 'x'
# => ["x"]
a
# => [["x"], ["x"], ["x"], ["x"]]
It can be fixed by passing in a block:
a = Array.new(4) { [ ] }
a[0]
# => []
a[0] << 'x'
# => ["x"]
a
# => [["x"], [], [], []]
It is highly unusual to see an array pre-initialized with values, though. Normally these are just lazy-initialized, or a Hash is used in place of an Array.

How to assign an array of Hashes in a loop?

I'm attempting to convert MySQL timestamps in an ActiveRecord object to another timestamp format. My method takes an array of ActiveRecord records and returns an array of hashes with the timestamped fields with the formatted timestamp:
def convert_mysql_timestamps(records)
ary = []
hash = {}
records.each_with_index do |record, i|
record.attributes.each do |field, value|
if time_columns.include?(field) and value then
hash[field] = value.strftime("%Y-%m-%dT%H:%M:%S%z")
else
hash[field] = value
end
end
ary[i] = {}
ary[i] = hash
end
ary
end
However, when in the ary[i] = hash assignment, all ary elements get set to hash.
Is there a better way to convert a record's timestamp fields? (I don't need to save the records back to the database.) Also, how can I get the array to capture each individual hash representation of the record?
Input:
[#<Vehicle id: 15001, approved_at: "2011-03-28 10:16:31", entry_date: "2011-03-28 10:16:31">, #<Vehicle id: 15002, approved_at: "2011-03-28 10:16:31", entry_date: "2011-03-28 10:16:31">]
Desired output:
[{"id"=>15001, "approved_at"=>"2011-03-28T10:16:31-0700", "entry_date"=>"2011-03-28T10:16:31-0700"}, {"id"=>15002, "approved_at"=>"2011-03-28T10:16:31-0700", "entry_date"=>"2011-03-28T10:16:31-0700"}]
The problem is that you're creating one Hash:
def convert_mysql_timestamps(records)
ary = []
hash = {}
#...
and then trying to re-use for each record. You probably want a fresh Hash for each each_with_index iteration:
def convert_mysql_timestamps(records)
ary = []
records.each_with_index do |record, i|
hash = { }
record.attributes.each do |field, value|
#...
end
ary[i] = hash
end
end
You can use map for this - it's always a good option when you want to take an array in one format and produce a same-sized array in another. Here's how:
def convert_mysql_timestamps(records)
records.map do |record|
Hash[records.attributes.map{|k, v| [k, convert_mysql_timestamp(v)] }]
end
end
def convert_mysql_timestamp(field, value)
return value unless time_columns.include?(field) && value
value.strftime("%Y-%m-%dT%H:%M:%S%z")
end
It works like so:
Hash[array_of_pairs] turns an array of key-value pairs - like [["foo", 2], ["bar", 3], ...] - into a hash like {"foo" => 2, "bar" => 3, ...}.
map calls its block for each item in the collection, and collects each return value of the block into a new array, which it returns. The attributes.map inside the Hash[...] creates the array of key-value pairs, and the outer records.map collects up all the hashes into the returned array.
I'd suggest reading up on the methods in Enumerable because there are so many neat things like map in there. You will find that you almost never have to use indices in your loops, although if you're coming from another language with for loops everywhere it's a hard habit to break!
I am not sure what your time_columns are, but assuming they are Time class, you can simplify the part like value.is_a?(Time).
def convert_mysql_timestamps(records)
records.collect do |record|
# assuming records are from a Rails model, I am using #attributes
# to loop through all fields in a record
# then inject values in this hash -> ({}),
# which is in the block, named attributes
record.attributes.inject({}) do |attributes, (column_name, value)|
# if it is Time, convert it to iso8601 (slightly different from your format,
# but if this is also acceptable, your code can be simpler)
attributes[column_name] = (value.is_a?(Time) ? value.iso8601 : value)
attributes
end
end
end

Verifying if an object is in an array of objects in Rails

I'm doing this:
#snippets = Snippet.find :all, :conditions => { :user_id => session[:user_id] }
#snippets.each do |snippet|
snippet.tags.each do |tag|
#tags.push tag
end
end
But if a snippets has the same tag two time, it'll push the object twice.
I want to do something like if #tags.in_object(tag)[...]
Would it be possible? Thanks!
I think there are 2 ways to go about it to get a faster result.
1) Add a condition to your find statement ( in MySQL DISTINCT ). This will return only unique result. DBs in general do much better jobs than regular code at getting results.
2) Instead if testing each time with include, why don't you do uniq after you populate your array.
here is example code
ar = []
data = []
#get some radom sample data
100.times do
data << ((rand*10).to_i)
end
# populate your result array
# 3 ways to do it.
# 1) you can modify your original array with
data.uniq!
# 2) you can populate another array with your unique data
# this doesn't modify your original array
ar.flatten << data.uniq
# 3) you can run a loop if you want to do some sort of additional processing
data.each do |i|
i = i.to_s + "some text" # do whatever you need here
ar << i
end
Depending on the situation you may use either.
But running include on each item in the loop is not the fastest thing IMHO
Good luck
Another way would be to simply concat the #tags and snippet.tags arrays and then strip it of duplicates.
#snippets.each do |snippet|
#tags.concat(snippet.tags)
end
#tags.uniq!
I'm assuming #tags is an Array instance.
Array#include? tests if an object is already included in an array. This uses the == operator, which in ActiveRecord tests for the same instance or another instance of the same type having the same id.
Alternatively, you may be able to use a Set instead of an Array. This will guarantee that no duplicates get added, but is unordered.
You can probably add a group to the query:
Snippet.find :all, :conditions => { :user_id => session[:user_id] }, :group => "tag.name"
Group will depend on how your tag data works, of course.
Or use uniq:
#tags << snippet.tags.uniq

Resources