Skip to content

Instantly share code, notes, and snippets.

@mildmojo
Created November 16, 2012 22:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mildmojo/4091480 to your computer and use it in GitHub Desktop.
Save mildmojo/4091480 to your computer and use it in GitHub Desktop.
Adding #left_joins to ActiveRecord::Base
# Adds left joins to ActiveRecord.
#
# = Description
#
# This patch adds a #left_joins method to ActiveRecord models and relations. It
# works like #joins, but performs a LEFT JOIN instead of an INNER JOIN.
#
# = A warning about +count+
#
# When using with #count, ActiveRecord 3.2.8 is hard-coded to act as if
# you'd called +count(distinct: true)+ when it detects an
# Arel::Nodes::OuterJoin in the query's abstract syntax tree. This means it
# counts the number of unique rows from the starting table, NOT the number of
# rows returned by the query. For example, if the DB contains a single profile
# joined to three causes, +Profile.left_joins(:causes).count+ will return the
# number of distinct Profiles in the results (1), not the total number of rows
# returned by the outer join (3). You can work around this by converting the
# resulting scope's #joins_values into strings with #to_sql. It's not a good
# idea to do that here because it prevents ActiveRecord from de-duping joins.
#
# (See: `bundle show activerecord`/lib/active_record/relation/calculations.rb,
# private method +perform_calculations+)
#
# Examples:
# Profile.left_joins(:causes).to_sql
# => SELECT "profiles".*
# FROM "profiles"
# LEFT OUTER JOIN "causes_profiles"
# ON "causes_profiles"."profile_id" = "profiles"."id"
# LEFT OUTER JOIN "causes"
# ON "causes"."id" = "causes_profiles"."cause_id"
#
# # Don't do this:
# Profile.left_joins(:causes).count
# => SELECT COUNT(DISTINCT "profiles"."id") FROM "profiles" ...
#
# # Do this:
# profile_joins_causes = Profile.left_joins(:causes).join_values.
# collect(&:to_sql)
# Profile.joins(profile_joins_causes).count
# => SELECT COUNT(*) FROM "profiles" ...
#
module ActiveRecord
module Associations
class LeftJoinDependency < JoinDependency
# Change default join type here. No way to feed it in from outside.
def build associations, parent = nil, join_type = Arel::OuterJoin
super
end
end
end
module QueryMethods
def left_joins *relation_names
relation_names.flatten!
return self if relation_names.compact.blank?
unless relation_names.all? { |n| n.is_a?(Symbol) }
raise ArgumentError, "Must provide relation names as symbols"
end
relation = clone
# Figure out how to join the model's +@klass+ to each of +relation_names+.
join_dependency =
Associations::LeftJoinDependency.new(@klass, relation_names, [])
# Get a bare Arel::SelectManager that says "SELECT FROM model_table".
manager = arel_table.from arel_table
# From the new +join_dependency+ graph, join each association onto the
# empty SelectManager, adding Arel::OuterJoins to its #join_sources.
join_dependency.join_associations.each do |association|
association.join_to(manager)
end
# Pull the Arel::OuterJoin objects out of the SelectManager and add them
# to the current relation's +joins_values+ list. They'll be swept up,
# de-duped, and converted to SQL when the query is finally executed.
relation.joins_values += manager.join_sources
relation
end
end
module Querying
# +left_joins+ is included into ActiveRecord::Relation, so delegate to that
# from ActiveRecord::Base.
delegate :left_joins, to: :scoped
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment