Rails 4 Accessing Join Table Attributes

后端 未结 2 478
一个人的身影
一个人的身影 2020-12-07 17:12

I have a has_many through join table setup for a recipe app where Ingredient and Meal connect through MealIngredient. Wit

相关标签:
2条回答
  • 2020-12-07 17:21

    In this case, you should loop through the meal_ingredients association. You should eager load the ingredients association to reduce db queries.

    @meal.meal_ingredients.includes(:ingredient).each do |meal_ingredient|
      puts meal_ingredient.amount
      puts meal_ingredient.ingredient.name
    end
    

    UPDATE

    This update came after Rich Peck's answer but I think there's a simpler way to achieve what he did.

    @meal.ingredients.select('ingredients.*, meal_ingredients.amount').each do |ingredient|
      puts ingredient.amount
      puts ingredient.name
    end
    
    0 讨论(0)
  • 2020-12-07 17:28

    Updated on 08/01/2020 - RPECK

    Struggled with this for months until we found an appropriate solution:

    --

    ActiveRecord Association Extensions

    The problem you have is that Rails will just use the foreign_keys in your join table to load the associative data you need. Unless you actually load the join model directly, it won't give you the ability to access the join attributes

    Some foraging lead us to ActiveRecord Association Extensions - a way to access the intermediary data in between different ActiveRecord Associations (using a collection called proxy_association). This will allow you to access the extra attributes from the join model, appending them to your "original" model:

    #app/models/ingredient.rb
    class Ingredient < ActiveRecord::Base
       attr_accessor :amount #-> need a setter/getter
    end
    
    #app/models/meal.rb
    class Meal < ActiveRecord::Base
       has_many :meal_ingredients
       has_many :ingredients, { -> extending: IngredientAmount }, through: :meal_ingredients
    end
    
    #app/models/concerns/ingerdient_amount.rb
    module IngredientAmount
    
        # => Load
        # => Triggered whenever module is invoked (allows us to populate response data)
        # => #<Method: ActiveRecord::Relation#load(&block) c:/Dev/Apps/pwinty-integration/.bundle/ruby/2.7.0/gems/activerecord-6.0.2.1/lib/active_record/relation.rb:614>
        def load(&block)
    
           # => This is called from the following reference - if you want to dig deeper, you can use method(:exec_queries).source_location
           # => c:/Dev/Apps/pwinty-integration/.bundle/ruby/2.7.0/gems/activerecord-6.0.2.1/lib/active_record/association_relation.rb:42
          unless loaded?
            exec_queries do |record|
              record.assign_attributes({amount: items[record.id]}) if items[record.id].present?
            end
          end
    
        end
    
        # Load
        # Deprecated with AR 6.0
        #def load
        #   amounts.each do |amount|
        #       proxy_association.target << amount
        #   end
        #end
    
        #Private
        private
    
        #Amounts
        # Not needed after AR 6.0
        #def amounts
        #   return_array = []
        #   through_collection.each_with_index do |through,i|
        #       associate = through.send(reflection_name)
        #       associate.assign_attributes({amount: items[i]}) if items[i].present?
        #       return_array.concat Array.new(1).fill( associate )
        #   end
        #   return_array
        #end
    
        #######################
        #      Variables      #
        #######################
    
        #Association
        def reflection_name
            proxy_association.source_reflection.name
        end
    
        #Foreign Key
        def through_source_key
            proxy_association.reflection.source_reflection.foreign_key
        end
    
        #Primary Key
        def through_primary_key
             proxy_association.reflection.through_reflection.active_record_primary_key
        end
    
        #Through Name
        def through_name
            proxy_association.reflection.through_reflection.name
        end
    
        #Through
        def through_collection
            proxy_association.owner.send through_name
        end
    
        #Captions
        def items
            #through_collection.map(&:amount)
            through_collection.pluck(through_source_key, :amount).map{ |id, amount| { id => amount } }.inject(:merge) #-> { id: record, id: record }
        end
    
        #Target
        # This no longer works with AR 6.0+
        #def target_collection
        #   #load_target
        #   proxy_association.target
        #end
    
    end
    

    This should append the amount attribute to your ingredient objects now, allowing you to perform:

    @meal = Meal.find 1
    @meal.ingredients.each do |ingredient|
       ingredient.amount
    end
    
    0 讨论(0)
提交回复
热议问题