Permalink
Browse files

* Refactored association code to be much simpler and rely on recursio…

…n. This allows the underlying class to do most of the work. This also allows calling any named scopes through any level of associations.
  • Loading branch information...
binarylogic committed Jul 31, 2009
1 parent 8a21abe commit 74d56319c9aa38163f3a70244b5c002c97d98299
View
@@ -1,3 +1,7 @@
+== 2.2.0 released 2009-07-30
+
+* Refactored association code to be much simpler and rely on recursion. This allows the underlying class to do most of the work. This also allows calling any named scopes through any level of associations.
+
== 2.1.13 released 2009-07-29
* Applied bug fix from http://github.com/skanev/searchlogic to make #order work with association ordering.
View
@@ -83,7 +83,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 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:
+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 leverage 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) } }
@@ -97,7 +97,7 @@ If you want to use the :include option, just specify it:
User.orders_line_items_price_greater_than(20).all(:include => {:orders => :line_items})
-Obviously, only do this if you want to actually use the included objects.
+Obviously, only do this if you want to actually use the included objects. Including objects into a query can be helpful with performance, especially when solving an N+1 query problem.
== Make searching and ordering data in your application trivial
@@ -176,7 +176,7 @@ Now just throw it in your form:
= f.check_box :four_year_olds
= f.submit
-What's great about this is that you can do just about anything you want. If Searchlogic doesn't provide a named scope for that crazy edge case that you need, just create your own named scope. The sky is the limit.
+This really allows Searchlogic to extend beyond what it provides internally. If Searchlogic doesn't provide a named scope for that crazy edge case that you need, just create your own named scope and use it. The sky is the limit.
== Use any or all
@@ -199,7 +199,7 @@ If you don't like will_paginate, use another solution, or roll your own. Paginat
== Conflicts with other gems
-You will notice searchlogic wants to create a method called "search". So do other libraries like thinking sphinx, etc. So searchlogic has a no conflict resolution. If the "search" method is already taken the method will be called "searchlogic" instead. So instead of
+You will notice searchlogic wants to create a method called "search". So do other libraries like thinking-sphinx, etc. So searchlogic has a no conflict resolution. If the "search" method is already taken the method will be called "searchlogic" instead. So instead of
User.search
View
@@ -1,6 +1,7 @@
require "searchlogic/core_ext/proc"
require "searchlogic/core_ext/object"
-require "searchlogic/active_record_consistency"
+require "searchlogic/active_record/consistency"
+require "searchlogic/active_record/named_scopes"
require "searchlogic/named_scopes/conditions"
require "searchlogic/named_scopes/ordering"
require "searchlogic/named_scopes/association_conditions"
@@ -10,10 +11,21 @@
Proc.send(:include, Searchlogic::CoreExt::Proc)
Object.send(:include, Searchlogic::CoreExt::Object)
+
+module ActiveRecord # :nodoc: all
+ class Base
+ class << self
+ include Searchlogic::ActiveRecord::Consistency
+ end
+ end
+end
+
+ActiveRecord::Base.extend(Searchlogic::ActiveRecord::NamedScopes)
+
ActiveRecord::Base.extend(Searchlogic::NamedScopes::Conditions)
-ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering)
ActiveRecord::Base.extend(Searchlogic::NamedScopes::AssociationConditions)
ActiveRecord::Base.extend(Searchlogic::NamedScopes::AssociationOrdering)
+ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering)
ActiveRecord::Base.extend(Searchlogic::NamedScopes::AliasScope)
ActiveRecord::Base.extend(Searchlogic::Search::Implementation)
@@ -0,0 +1,22 @@
+module Searchlogic
+ module ActiveRecord
+ # Active Record is pretty inconsistent with how their SQL is constructed. This
+ # method attempts to close the gap between the various inconsistencies.
+ module Consistency
+ def self.included(klass)
+ klass.class_eval do
+ alias_method_chain :merge_joins, :searchlogic
+ end
+ end
+
+ # In AR multiple joins are sometimes in a single join query, and other times they
+ # are not. The merge_joins method in AR should account for this, but it doesn't.
+ # This fixes that problem. This way there is one join per string, which allows
+ # the merge_joins method to delete duplicates.
+ def merge_joins_with_searchlogic(*args)
+ joins = merge_joins_without_searchlogic(*args)
+ joins.collect { |j| j.is_a?(String) ? j.split(" ") : j }.flatten.uniq
+ end
+ end
+ end
+end
@@ -0,0 +1,51 @@
+module Searchlogic
+ module ActiveRecord
+ # Adds methods that give extra information about a classes named scopes.
+ module NamedScopes
+ # Retrieves the options passed when creating the respective named scope. Ex:
+ #
+ # named_scope :whatever, :conditions => {:column => value}
+ #
+ # This method will return:
+ #
+ # :conditions => {:column => value}
+ #
+ # ActiveRecord hides this internally in a Proc, so we have to try and pull it out with this
+ # method.
+ def named_scope_options(name)
+ key = scopes.key?(name.to_sym) ? name.to_sym : primary_condition_name(name)
+
+ if key
+ eval("options", scopes[key].binding)
+ else
+ nil
+ end
+ end
+
+ # The arity for a named scope's proc is important, because we use the arity
+ # to determine if the condition should be ignored when calling the search method.
+ # If the condition is false and the arity is 0, then we skip it all together. Ex:
+ #
+ # User.named_scope :age_is_4, :conditions => {:age => 4}
+ # User.search(:age_is_4 => false) == User.all
+ # User.search(:age_is_4 => true) == User.all(:conditions => {:age => 4})
+ #
+ # We also use it when trying to "copy" the underlying named scope for association
+ # conditions. This way our aliased scope accepts the same number of parameters for
+ # the underlying scope.
+ def named_scope_arity(name)
+ options = named_scope_options(name)
+ options.respond_to?(:arity) ? options.arity : nil
+ end
+
+ # A convenience method for creating inner join sql to that your inner joins
+ # are consistent with how Active Record creates them. Basically a tool for
+ # you to use when writing your own named scopes. This way you know for sure
+ # that duplicate joins will be removed when chaining scopes together that
+ # use the same join.
+ def inner_joins(association_name)
+ ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
+ end
+ end
+ end
+end
@@ -1,28 +0,0 @@
-module Searchlogic
- # Active Record is pretty inconsistent with how their SQL is constructed. This
- # method attempts to close the gap between the various inconsistencies.
- module ActiveRecordConsistency
- def self.included(klass)
- klass.class_eval do
- alias_method_chain :merge_joins, :searchlogic
- end
- end
-
- # In AR multiple joins are sometimes in a single join query, and other times they
- # are not. The merge_joins method in AR should account for this, but it doesn't.
- # This fixes that problem. This way there is one join per string, which allows
- # the merge_joins method to delete duplicates.
- def merge_joins_with_searchlogic(*args)
- joins = merge_joins_without_searchlogic(*args)
- joins.collect { |j| j.is_a?(String) ? j.split(" ") : j }.flatten.uniq
- end
- end
-end
-
-module ActiveRecord # :nodoc: all
- class Base
- class << self
- include Searchlogic::ActiveRecordConsistency
- end
- end
-end
@@ -48,6 +48,7 @@ def alias_scopes # :nodoc:
end
def alias_scope?(name) # :nodoc:
+ return false if name.blank?
alias_scopes.key?(name.to_sym)
end
@@ -3,100 +3,52 @@ module NamedScopes
# Handles dynamically creating named scopes for associations.
module AssociationConditions
def condition?(name) # :nodoc:
- super || association_condition?(name) || association_alias_condition?(name)
+ super || association_condition?(name)
end
def primary_condition_name(name) # :nodoc:
if result = super
result
elsif association_condition?(name)
name.to_sym
- elsif details = association_alias_condition_details(name)
- "#{details[:association]}_#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
else
nil
end
end
- # Is the name of the method a valid name for an association condition?
- def association_condition?(name)
- !association_condition_details(name).nil?
- end
-
- # Is the named of the method a valid name for an association alias condition?
- # An alias being "gt" for "greater_than", etc.
- def association_alias_condition?(name)
- !association_alias_condition_details(name).nil?
- end
-
- # A convenience method for creating inner join sql to that your inner joins
- # are consistent with how Active Record creates them. Basically a tool for
- # you to use when writing your own named scopes. This way you know for sure
- # that duplicate joins will be removed when chaining scopes together that
- # use the same join.
- def inner_joins(association_name)
- ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
- end
-
private
+ def association_condition?(name)
+ !association_condition_details(name).nil?
+ end
+
def method_missing(name, *args, &block)
if details = association_condition_details(name)
- create_association_condition(details[:association], details[:column], details[:condition], args)
- send(name, *args)
- elsif details = association_alias_condition_details(name)
- create_association_alias_condition(details[:association], details[:column], details[:condition], args)
+ create_association_condition(details[:association], details[:condition], args)
send(name, *args)
else
super
end
end
def association_condition_details(name)
- assocs = non_polymorphic_associations
+ assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }
return nil if assocs.empty?
- regexes = [association_searchlogic_regex(assocs, Conditions::PRIMARY_CONDITIONS)]
- assocs.each do |assoc|
- scope_names = assoc.klass.scopes.keys + assoc.klass.alias_scopes.keys
- scope_names.uniq!
- scope_names.delete(:scoped)
- next if scope_names.empty?
- regexes << /^(#{assoc.name})_(#{scope_names.join("|")})$/
- end
- if !local_condition?(name) && regexes.any? { |regex| name.to_s =~ regex }
- {:association => $1, :column => $2, :condition => $3}
- end
- end
-
- def create_association_condition(association_name, column, condition, args)
- name_parts = [column, condition].compact
- condition_name = name_parts.join("_")
- named_scope("#{association_name}_#{condition_name}", association_condition_options(association_name, condition_name, args))
- end
-
- def association_alias_condition_details(name)
- assocs = non_polymorphic_associations
- return nil if assocs.empty?
-
- if !local_condition?(name) && name.to_s =~ association_searchlogic_regex(assocs, Conditions::ALIAS_CONDITIONS)
- {:association => $1, :column => $2, :condition => $3}
+ if name.to_s =~ /^(#{assocs.collect(&:name).join("|")})_(\w+)$/
+ association_name = $1
+ condition = $2
+ association = reflect_on_association(association_name.to_sym)
+ klass = association.klass
+ if klass.condition?(condition)
+ {:association => $1, :condition => $2}
+ else
+ nil
+ end
end
end
- def non_polymorphic_associations
- reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }
- end
-
- def association_searchlogic_regex(assocs, condition_names)
- /^(#{assocs.collect(&:name).join("|")})_(\w+)_(#{condition_names.join("|")})$/
- end
-
- def create_association_alias_condition(association, column, condition, args)
- primary_condition = primary_condition(condition)
- alias_name = "#{association}_#{column}_#{condition}"
- primary_name = "#{association}_#{column}_#{primary_condition}"
- send(primary_name, *args) # go back to method_missing and make sure we create the method
- (class << self; self; end).class_eval { alias_method alias_name, primary_name }
+ def create_association_condition(association, condition, args)
+ named_scope("#{association}_#{condition}", association_condition_options(association, condition, args))
end
def association_condition_options(association_name, association_condition, args)
@@ -2,14 +2,28 @@ module Searchlogic
module NamedScopes
# Handles dynamically creating named scopes for associations.
module AssociationOrdering
- def association_ordering_condition?(name)
- !association_ordering_condition_details(name).nil?
+ def condition?(name) # :nodoc:
+ super || association_ordering_condition?(name)
+ end
+
+ def primary_condition_name(name) # :nodoc
+ if result = super
+ result
+ elsif association_ordering_condition?(name)
+ name.to_sym
+ else
+ nil
+ end
end
private
+ def association_ordering_condition?(name)
+ !association_ordering_condition_details(name).nil?
+ end
+
def method_missing(name, *args, &block)
if details = association_ordering_condition_details(name)
- create_association_ordering_condition(details[:association], details[:order_as], details[:column], args)
+ create_association_ordering_condition(details[:association], details[:order_as], details[:condition], args)
send(name, *args)
else
super
@@ -18,13 +32,13 @@ def method_missing(name, *args, &block)
def association_ordering_condition_details(name)
associations = reflect_on_all_associations.collect { |assoc| assoc.name }
- if !local_condition?(name) && name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
- {:order_as => $1, :association => $2, :column => $3}
+ if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
+ {:order_as => $1, :association => $2, :condition => $3}
end
end
- def create_association_ordering_condition(association_name, order_as, column, args)
- named_scope("#{order_as}_by_#{association_name}_#{column}", association_condition_options(association_name, "#{order_as}_by_#{column}", args))
+ def create_association_ordering_condition(association, order_as, condition, args)
+ named_scope("#{order_as}_by_#{association}_#{condition}", association_condition_options(association, "#{order_as}_by_#{condition}", args))
end
end
end
Oops, something went wrong.

0 comments on commit 74d5631

Please sign in to comment.