Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retrieve all association's attributes of an AR model?

What do you think is the most optimal way to retrieve all attributes for all the associations an AR model has?

i.e: let's say we have the model Target.

class Target < ActiveRecord::Base
  has_many :countries
  has_many :cities
  has_many :towns
  has_many :colleges
  has_many :tags

  accepts_nested_attributes_for :countries, :cities, ...
end

I'd like to retrieve all the association's attributes by calling a method on a Target instance:

target.associations_attributes
>> { :countries => { "1" => { :name => "United States", :code => "US", :id => 1 }, 
                     "2" => { :name => "Canada", :code => "CA", :id => 2 } },
     :cities => { "1" => { :name => "New York", :region_id => 1, :id => 1 } },
     :regions => { ... },
     :colleges => { ... }, ....
   }

Currently I make this work by iterating on each association, and then on each model of the association, But it's kind of expensive, How do you think I can optimize this?

Just a note: I realized you can't call target.countries_attributes on has_many associations with nested_attributes, one_to_one associations allow to call target.country_attributes

like image 287
jpemberthy Avatar asked Feb 19 '10 19:02

jpemberthy


People also ask

What is the difference between Has_one and Belongs_to?

They essentially do the same thing, the only difference is what side of the relationship you are on. If a User has a Profile , then in the User class you'd have has_one :profile and in the Profile class you'd have belongs_to :user . To determine who "has" the other object, look at where the foreign key is.

What is Has_and_belongs_to_many?

As far as I can remember, has_and_belongs_to_many gives you a simple lookup table which references your two models. For example, Stories can belong to many categories. Categories can have many stories.

What is ActiveRecord in Ruby on Rails?

What is ActiveRecord? ActiveRecord is an ORM. It's a layer of Ruby code that runs between your database and your logic code. When you need to make changes to the database, you'll write Ruby code, and then run "migrations" which makes the actual changes to the database.


3 Answers

I'm not clear on what you mean with iterating on all associations. Are you already using reflections?

Still curious if there's a neater way, but this is what I could come up with, which more or less results in the hash you're showing in your example:

class Target < ActiveRecord::Base
  has_many :tags

  def associations_attributes
    # Get a list of symbols of the association names in this class
    association_names = self.class.reflect_on_all_associations.collect { |r| r.name }
    # Fetch myself again, but include all associations
    me = self.class.find self.id, :include => association_names
    # Collect an array of pairs, which we can use to build the hash we want
    pairs = association_names.collect do |association_name|
      # Get the association object(s)
      object_or_array = me.send(association_name)
      # Build the single pair for this association
      if object_or_array.is_a? Array
        # If this is a has_many or the like, use the same array-of-pairs trick
        # to build a hash of "id => attributes"
        association_pairs = object_or_array.collect { |o| [o.id, o.attributes] }
        [association_name, Hash[*association_pairs.flatten(1)]]
      else
        # has_one, belongs_to, etc.
        [association_name, object_or_array.attributes]
      end
    end
    # Build the final hash
    Hash[*pairs.flatten(1)]
  end
end

And here's an irb session through script/console to show how it works. First, some environment:

>> t = Target.create! :name => 'foobar'
=> #<Target id: 1, name: "foobar">
>> t.tags.create! :name => 'blueish'
=> #<Tag id: 1, name: "blueish", target_id: 1>
>> t.tags.create! :name => 'friendly'
=> #<Tag id: 2, name: "friendly", target_id: 1>
>> t.tags
=> [#<Tag id: 1, name: "blueish", target_id: 1>, #<Tag id: 2, name: "friendly", target_id: 1>]

And here's the output from the new method:

>> t.associations_attributes
=> {:tags=>{1=>{"id"=>1, "name"=>"blueish", "target_id"=>1}, 2=>{"id"=>2, "name"=>"friendly", "target_id"=>1}}}
like image 56
Stéphan Kochen Avatar answered Oct 13 '22 20:10

Stéphan Kochen


try this with exception handling:

class Target < ActiveRecord::Base

  def associations_attributes
    tmp = {}
    self.class.reflections.symbolize_keys.keys.each do |key|
      begin
        data = self.send(key) || {}
        if data.is_a?(ActiveRecord::Base)
          tmp[key] = data.attributes.symbolize_keys!
        else
          mapped_data = data.map { |item| item.attributes.symbolize_keys! }
          tmp[key] = mapped_data.each_with_index.to_h.invert
        end
      rescue Exception => e
        tmp[key] = e.message
      end
    end
    tmp
  end

end
like image 25
sa77 Avatar answered Oct 13 '22 19:10

sa77


This is updated version of Stéphan Kochen's code for Rails 4.2

def associations_attributes       
    association_names = self.class.reflect_on_all_associations.collect { |r| r.name }   
    me = self.class.includes(association_names).find self.id    

    pairs = association_names.collect do |association_name|    
        object_or_array = me.send(association_name)    

        if object_or_array.is_a? ActiveRecord::Associations::CollectionProxy
            association_pairs = object_or_array.collect { |o| [o.id, o.attributes] }
            [association_name, Hash[*association_pairs.flatten(1)]]
        else
            [association_name, object_or_array.attributes]    
        end
    end    

    Hash[*pairs.flatten(1)]
end
like image 39
yaru Avatar answered Oct 13 '22 20:10

yaru