Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Use inner joins for conditions instead of left outer

  • Loading branch information...
commit 764f8591a46fdbb7113ff753eff453039e2378e3 1 parent 8906208
@binarylogic authored
View
6 README.rdoc
@@ -85,7 +85,7 @@ You also get named scopes for any of your associations:
User.ascend_by_order_total
User.descend_by_orders_line_items_price
-Again these are just named scopes. You can chain them together, call methods off of them, etc. What's great about these named scopes is that they do NOT use the :include option, making them <em>much</em> faster. Instead they create a LEFT OUTER JOIN and pass it to the :joins option, which is great for performance. To prove my point here is a quick benchmark from an application I am working on:
+Again these are just named scopes. You can chain them together, call methods off of them, etc. What's great about these named scopes is that they do NOT use the :include option, making them <em>much</em> faster. Instead they create a INNER JOIN and pass it to the :joins option, which is great for performance. To prove my point here is a quick benchmark from an application I am working on:
Benchmark.bm do |x|
x.report { 10.times { Event.tickets_id_gt(10).all(:include => :tickets) } }
@@ -101,8 +101,6 @@ If you want to use the :include option, just specify it:
Obviously, only do this if you want to actually use the included objects.
-Lastly, because we are using ActiveRecord, named scopes, combining conditions, and ordering based on associated columns, the decision was made to use left outer joins instead of inner joins. This allows us to be consistent, include optional associations when ordering, and avoid duplicate joins when eager loading associations. If we use an inner join and combine any of these things we will get an "ambiguous name" sql error for the table being joined twice. Just like anything, be mindful of the SQL being produced in your application. If you are joining beyond 4 or 5 levels deep then you might consider looking at the query and optimizing it. Part of that optimization may require the use of inner joins instead of left outer joins depending on the query.
-
== Make searching and ordering data in your application trivial
The above is great, but what about tying all of this in with a search form in your application? What would be really nice is if we could use an object that represented a single search. Like this...
@@ -192,7 +190,7 @@ Before I use a library in my application I like to glance at the source and try
Searchlogic utilizes method_missing to create all of these named scopes. When it hits method_missing it creates a named scope to ensure it will never hit method missing for that named scope again. Sort of a caching mechanism. It works in the same fashion as ActiveRecord's "find_by_*" methods. This way only the named scopes you need are created and nothing more.
-That's about it, the named scope options are pretty bare bones and created just like you would manually. The only big difference being the use of LEFT OUTER JOINS on associated conditions instead of INNER JOINS.
+That's about it, the named scope options are pretty bare bones and created just like you would manually.
== Credit
View
42 lib/searchlogic/named_scopes/associations.rb
@@ -29,24 +29,6 @@ def association_alias_condition?(name)
!association_alias_condition_details(name).nil?
end
- # Leverages ActiveRecord's JoinDependency class to create a left outer join. Searchlogic uses left outer joins so that
- # records with no associations are included in the result when the association is optional. You can use this method
- # internally when creating your own named scopes that need joins. You need to do this because then ActiveRecord will
- # remove any duplicate joins for you when you chain named scopes that require the same join. If you are using a
- # LEFT OUTER JOIN and an INNER JOIN, ActiveRecord will add both to the query, causing SQL errors.
- #
- # Bottom line, this is convenience method that you can use when creating your own named scopes. Ex:
- #
- # named_scope :orders_line_items_price_expensive, :joins => left_out_joins(:orders => :line_items), :conditions => "line_items.price > 100"
- #
- # Now your joins are consistent with Searchlogic allowing you to avoid SQL errors with duplicate joins.
- def left_outer_joins(association_name)
- ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect do |assoc|
- sql = assoc.association_join.strip
- sql.split(/LEFT OUTER JOIN/).delete_if { |join| join.strip.blank? }.collect { |join| "LEFT OUTER JOIN #{join.strip}"}
- end.flatten
- end
-
private
def method_missing(name, *args, &block)
if details = association_condition_details(name)
@@ -110,7 +92,8 @@ def association_condition_options(association_name, association_condition, args)
# The underlying condition doesn't require any parameters, so let's just create a simple
# named scope that is based on a hash.
options = scope.proxy_options
- add_left_outer_joins(options, association)
+ options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
+ #add_left_outer_joins(options, association)
options
else
# The underlying condition requires parameters, let's match the parameters it requires
@@ -132,31 +115,12 @@ def association_condition_options(association_name, association_condition, args)
eval <<-"end_eval"
searchlogic_lambda(:#{scope_options.searchlogic_arg_type}) { |#{proc_args.join(",")}|
options = association.klass.named_scope_options(association_condition).call(#{proc_args.join(",")})
- add_left_outer_joins(options, association)
+ options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
options
}
end_eval
end
end
-
- # In a named scope you have 2 options for adding joins: :include and :joins.
- #
- # :include will execute multiple queries for each association and instantiate objects for each association.
- # This is not what we want when we are searching. The only other option left is :joins. We can pass the
- # name of the association directly, but AR creates an INNER JOIN. If we are ordering by an association's
- # attribute, and that association is optional, the records without an association will be omitted. Again,
- # not what we want.
- #
- # So the only option left is to use :joins with manually written SQL. We can still have AR generate this SQL
- # for us by leveraging it's join dependency classes. Instead of using the InnerJoinDependency we use the regular
- # JoinDependency which creates a LEFT OUTER JOIN, which is what we want.
- #
- # The code below was extracted out of AR's add_joins! method and then modified.
- def add_left_outer_joins(options, association)
- joins = left_outer_joins(association.name)
- options[:joins] ||= []
- options[:joins] = joins + options[:joins]
- end
end
end
end
View
29 spec/named_scopes/associations_spec.rb
@@ -2,16 +2,16 @@
describe "Associations" do
before(:each) do
- @users_join_sql = ["LEFT OUTER JOIN \"users\" ON users.company_id = companies.id"]
- @orders_join_sql = ["LEFT OUTER JOIN \"users\" ON users.company_id = companies.id", "LEFT OUTER JOIN \"orders\" ON orders.user_id = users.id"]
+ @users_join_sql = ["INNER JOIN \"users\" ON users.company_id = companies.id"]
+ @orders_join_sql = ["INNER JOIN \"users\" ON users.company_id = companies.id", "INNER JOIN \"orders\" ON orders.user_id = users.id"]
end
it "should create a named scope" do
- Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => @users_join_sql)
+ Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => :users)
end
it "should create a deep named scope" do
- Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => @orders_join_sql)
+ Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => {:users => :orders})
end
it "should not allowed named scopes on non existent association columns" do
@@ -23,13 +23,13 @@
end
it "should allow named scopes to be called multiple times and reflect the value passed" do
- Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => @users_join_sql)
- Company.users_username_like("thunt").proxy_options.should == User.username_like("thunt").proxy_options.merge(:joins => @users_join_sql)
+ Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => :users)
+ Company.users_username_like("thunt").proxy_options.should == User.username_like("thunt").proxy_options.merge(:joins => :users)
end
it "should allow deep named scopes to be called multiple times and reflect the value passed" do
- Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => @orders_join_sql)
- Company.users_orders_total_greater_than(20).proxy_options.should == Order.total_greater_than(20).proxy_options.merge(:joins => @orders_join_sql)
+ Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => {:users => :orders})
+ Company.users_orders_total_greater_than(20).proxy_options.should == Order.total_greater_than(20).proxy_options.merge(:joins => {:users => :orders})
end
it "should have an arity of 1 if the underlying scope has an arity of 1" do
@@ -48,30 +48,31 @@
end
it "should allow aliases" do
- Company.users_username_contains("bjohnson").proxy_options.should == User.username_contains("bjohnson").proxy_options.merge(:joins => @users_join_sql)
+ Company.users_username_contains("bjohnson").proxy_options.should == User.username_contains("bjohnson").proxy_options.merge(:joins => :users)
end
it "should allow deep aliases" do
- Company.users_orders_total_gt(10).proxy_options.should == Order.total_gt(10).proxy_options.merge(:joins => @orders_join_sql)
+ Company.users_orders_total_gt(10).proxy_options.should == Order.total_gt(10).proxy_options.merge(:joins => {:users => :orders})
end
it "should allow ascending" do
- Company.ascend_by_users_username.proxy_options.should == User.ascend_by_username.proxy_options.merge(:joins => @users_join_sql)
+ Company.ascend_by_users_username.proxy_options.should == User.ascend_by_username.proxy_options.merge(:joins => :users)
end
it "should allow descending" do
- Company.descend_by_users_username.proxy_options.should == User.descend_by_username.proxy_options.merge(:joins => @users_join_sql)
+ Company.descend_by_users_username.proxy_options.should == User.descend_by_username.proxy_options.merge(:joins => :users)
end
it "should allow deep ascending" do
- Company.ascend_by_users_orders_total.proxy_options.should == Order.ascend_by_total.proxy_options.merge(:joins => @orders_join_sql)
+ Company.ascend_by_users_orders_total.proxy_options.should == Order.ascend_by_total.proxy_options.merge(:joins => {:users => :orders})
end
it "should allow deep descending" do
- Company.descend_by_users_orders_total.proxy_options.should == Order.descend_by_total.proxy_options.merge(:joins => @orders_join_sql)
+ Company.descend_by_users_orders_total.proxy_options.should == Order.descend_by_total.proxy_options.merge(:joins => {:users => :orders})
end
it "should include optional associations" do
+ pending # this is a problem with using inner joins and left outer joins
Company.create
company = Company.create
user = company.users.create
Please sign in to comment.
Something went wrong with that request. Please try again.