activerecord uses JoinDependency to automagically generate inner join statements for any type of association (belongs_to, has_many, and has_and_belongs_to_many). For example, for HABTM associations, two join statements are required. This method encapsulates that functionality and yields an intermediate object for chaining. It also allows you to use an outer join instead of the default inner via the join_type arg.
# File lib/arel-helpers/join_association.rb, line 19 def join_association(table, association, join_type = Arel::Nodes::InnerJoin, options = {}, &block) if ActiveRecord::VERSION::STRING >= '5.2.0' join_association_5_2(table, association, join_type, options, &block) elsif ActiveRecord::VERSION::STRING >= '5.0.0' join_association_5_0(table, association, join_type, options, &block) elsif ActiveRecord::VERSION::STRING >= '4.2.0' join_association_4_2(table, association, join_type, options, &block) elsif ActiveRecord::VERSION::STRING >= '4.1.0' join_association_4_1(table, association, join_type, options, &block) else join_association_3_1(table, association, join_type, options, &block) end end
# File lib/arel-helpers/join_association.rb, line 192 def find_alias(name, aliases) aliases.find { |a| a.table_name == name } end
# File lib/arel-helpers/join_association.rb, line 35 def join_association_3_1(table, association, join_type, options = {}) aliases = options.fetch(:aliases, {}) associations = association.is_a?(Array) ? association : [association] join_dependency = ActiveRecord::Associations::JoinDependency.new(table, associations, []) manager = Arel::SelectManager.new(table) join_dependency.join_associations.each do |assoc| assoc.join_type = join_type assoc.join_to(manager) end manager.join_sources.map do |assoc| if found_alias = find_alias(assoc.left.name, aliases) assoc.left.table_alias = found_alias.name end if block_given? # yield |assoc_name, join_conditions| right = yield assoc.left.name.to_sym, assoc.right assoc.class.new(assoc.left, right) else assoc end end end
# File lib/arel-helpers/join_association.rb, line 61 def join_association_4_1(table, association, join_type, options = {}) aliases = options.fetch(:aliases, {}) associations = association.is_a?(Array) ? association : [association] join_dependency = ActiveRecord::Associations::JoinDependency.new(table, associations, []) join_dependency.join_constraints([]).map do |constraint| right = if block_given? yield constraint.left.name.to_sym, constraint.right else constraint.right end if found_alias = find_alias(constraint.left.name, aliases) constraint.left.table_alias = found_alias.name end join_type.new(constraint.left, right) end end
ActiveRecord 4.2 moves bind variables out of the join classes and into the relation. For this reason, a method like ::join_association isn't able to add to the list of bind variables dynamically. To get around the problem, this method must return a string.
# File lib/arel-helpers/join_association.rb, line 86 def join_association_4_2(table, association, join_type, options = {}) aliases = options.fetch(:aliases, []) associations = association.is_a?(Array) ? association : [association] join_dependency = ActiveRecord::Associations::JoinDependency.new(table, associations, []) constraints = join_dependency.join_constraints([]) binds = constraints.flat_map do |info| info.binds.map { |bv| table.connection.quote(*bv.reverse) } end joins = constraints.flat_map do |constraint| constraint.joins.map do |join| right = if block_given? yield join.left.name.to_sym, join.right else join.right end if found_alias = find_alias(join.left.name, aliases) join.left.table_alias = found_alias.name end join_type.new(join.left, right) end end join_strings = joins.map do |join| to_sql(join, table, binds) end join_strings.join(' ') end
# File lib/arel-helpers/join_association.rb, line 120 def join_association_5_0(table, association, join_type, options = {}) aliases = options.fetch(:aliases, []) associations = association.is_a?(Array) ? association : [association] join_dependency = ActiveRecord::Associations::JoinDependency.new(table, associations, []) constraints = join_dependency.join_constraints([], join_type) binds = constraints.flat_map do |info| prepared_binds = info.binds.map(&:value_for_database) prepared_binds.map { |value| table.connection.quote(value) } end joins = constraints.flat_map do |constraint| constraint.joins.map do |join| right = if block_given? yield join.left.name.to_sym, join.right else join.right end if found_alias = find_alias(join.left.name, aliases) join.left.table_alias = found_alias.name end join_type.new(join.left, right) end end join_strings = joins.map do |join| to_sql(join, table, binds) end join_strings.join(' ') end
# File lib/arel-helpers/join_association.rb, line 155 def join_association_5_2(table, association, join_type, options = {}) aliases = options.fetch(:aliases, []) associations = association.is_a?(Array) ? association : [association] alias_tracker = ActiveRecord::Associations::AliasTracker.create( table.connection, table.name, {} ) join_dependency = ActiveRecord::Associations::JoinDependency.new( table, table.arel_table, associations, alias_tracker ) constraints = join_dependency.join_constraints([], join_type) constraints.map do |join| right = if block_given? yield join.left.name.to_sym, join.right else join.right end if found_alias = find_alias(join.left.name, aliases) join.left.table_alias = found_alias.name end join_type.new(join.left, right) end end
# File lib/arel-helpers/join_association.rb, line 186 def to_sql(node, table, binds) visitor = table.connection.visitor collect = visitor.accept(node, Arel::Collectors::Bind.new) collect.substitute_binds(binds).join end