Skip to content

Instantly share code, notes, and snippets.

@armstrjare
Created September 27, 2010 07:05
Show Gist options
  • Save armstrjare/598710 to your computer and use it in GitHub Desktop.
Save armstrjare/598710 to your computer and use it in GitHub Desktop.
# This file monkey patches ActiveRecord to fix the problem of:
#
# MoodelA <-> ModelB <-> ModelC
# ModelA.joins(:modelb) & ModelB.joins(:modelc)
# -> Would raise association not found because the association ("modelc") is searched for
# -> from the context of ModelA, rather than from the [expected] context of ModelB
# (expected because the joins associatin is specified where the association "modelc" exists within the base of the Relation (ModelB) )
# -> The reason this ocurred is because the context of the joins on associations are not persisted when merging two relations/scopes
# -> rather, they are simply copied by name in to the merging relation/scope.
#
# -> This monkey patch adds functionality of persisting this context by wrapping the association join with a proxy that keeps the
# -> intended context of the association (keeps the ActiveRecord class at the base of the relation/scope where the join was defined).
#
# -> This allows for a lookup for the association reflection on the correct ActiveRecord class when the AREL is built.
module ActiveRecord::QueryMethods
def build_joins(relation, joins)
joined_associations = []
association_joins = []
joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
joins.each do |join|
# armstrjare: added case for the join association proxy
association_joins << join if [Hash, Array, Symbol, ActiveRecord::SpawnMethods::AssociationJoinFromContext].include?(join.class) && !array_of_strings?(join)
# end added
end
stashed_association_joins = joins.grep(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)
non_association_joins = (joins - association_joins - stashed_association_joins)
custom_joins = custom_join_sql(*non_association_joins)
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins)
join_dependency.graft(*stashed_association_joins)
@implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
to_join = []
join_dependency.join_associations.each do |association|
if (association_relation = association.relation).is_a?(Array)
to_join << [association_relation.first, association.join_class, association.association_join.first]
to_join << [association_relation.last, association.join_class, association.association_join.last]
else
to_join << [association_relation, association.join_class, association.association_join]
end
end
to_join.each do |tj|
unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] }
joined_associations << tj
relation = relation.join(tj[0], tj[1]).on(*tj[2])
end
end
relation.join(custom_joins)
end
end
module ActiveRecord
module SpawnMethods
# Added
AssociationJoinFromContext = Struct.new(:context, :association)
def merge(r)
merged_relation = clone
return merged_relation unless r
((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - [:joins, :where]).each do |method|
value = r.send(:"#{method}_values")
unless value.empty?
if method == :includes
merged_relation = merged_relation.includes(value)
else
merged_relation.send(:"#{method}_values=", value)
end
end
end
# armstrjare: added to provide a proxy to the join association
new_joins = r.joins_values.map do |j|
case j
when Symbol, String
# Association join - keep the context of the association
AssociationJoinFromContext.new(r.klass, j)
else
j
end
end
merged_relation = merged_relation.joins(new_joins)
# end added
merged_wheres = @where_values
r.where_values.each do |w|
if w.respond_to?(:operator) && w.operator == :==
merged_wheres = merged_wheres.reject { |p|
p.respond_to?(:operator) && p.operator == :== && p.operand1.name == w.operand1.name
}
end
merged_wheres += [w]
end
merged_relation.where_values = merged_wheres
Relation::SINGLE_VALUE_METHODS.reject {|m| m == :lock}.each do |method|
unless (value = r.send(:"#{method}_value")).nil?
merged_relation.send(:"#{method}_value=", value)
end
end
merged_relation.lock_value = r.lock_value unless merged_relation.lock_value
# Apply scope extension modules
merged_relation.send :apply_modules, r.extensions
merged_relation
end
end
end
module ActiveRecord
module Associations
module ClassMethods
class JoinDependency
private
def build(associations, parent = nil, join_class = Arel::InnerJoin)
parent ||= @joins.last
case associations
# armstrjare: added case for the join association proxy
when ActiveRecord::SpawnMethods::AssociationJoinFromContext
reflection = associations.context.reflections[associations.association.to_s.to_sym] or
raise ConfigurationError, "[AJ] Association named '#{ associations.association }' was not found in #{associations.context.name}; perhaps you misspelled it?"
@reflections << reflection
@joins << build_join_association(reflection, JoinBase.new(associations.context, [associations.association])).with_join_class(join_class)
# end added
when Symbol, String
reflection = parent.reflections[associations.to_s.intern] or
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
@reflections << reflection
@joins << build_join_association(reflection, parent).with_join_class(join_class)
when Array
associations.each do |association|
build(association, parent, join_class)
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
build(name, parent, join_class)
build(associations[name], nil, join_class)
end
else
raise ConfigurationError, associations.inspect
end
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment