Created
November 16, 2012 22:27
-
-
Save mildmojo/4091480 to your computer and use it in GitHub Desktop.
Adding #left_joins to ActiveRecord::Base
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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