Rails 4 Accessing Join Table Attributes
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
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