Get columns names with ActiveRecord

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.

Answers


AR provides a #column_names method that returns an array of 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 =)


Need Your Help

better way to shrink copy/paste codes

c++ enums

Currently I have this trunk of code: a vector of structs v[myStruct]. The size of it is 3. I named these structs as myStruct_A, myStruct_B, and myStruct_C. Each piece of code is almost same, but wi...

How to remove audio file from SDCard by getting it's position

android android-listview audio-recording listviewitem

I have a custom list-view with check-boxes which get's all audio file name from SD Card folder and when i click any of this it play's audio. Everything is working fine. Now i have a image-view when i