Skip to content
Browse files

New favorite feature: support for traversing through polymorphic asso…

…ciations
  • Loading branch information...
1 parent 40a721a commit 2ee4174642411e22d3c3395507474ced26d2f8b6 @binarylogic committed
View
13 README.rdoc
@@ -113,6 +113,19 @@ Also, these conditions aren't limited to the scopes Searchlogic provides. You ca
As I stated above, Searchlogic will take care of creating the necessary joins for you. This is REALLY nice when trying to keep your code DRY, because if you wanted to use a scope like this in your User model you would have to copy over the conditions. Now you have 2 named scopes that are essentially doing the same thing. Why do that when you can dynamically access that scope using this feature?
+=== Polymorphic associations
+
+Polymorphic associations are tough because ActiveRecord doesn't support them with the :joins or :include options. Searchlogic checks for a specific syntax and takes care of this for you. Ex:
+
+ Audit.belongs_to :auditable, :polymorphic => true
+ User.has_many :audits, :as => :auditable
+
+ Audit.auditable_user_type_username_equals("ben")
+
+The above will take care of creating the inner join on the polymorphic association so that it only looks for type 'User'. On the surface it works the same as a non polymorphic association. The syntax difference being that you need to call the association and then specify the type:
+
+ [polymorphic association name]_[association type]_type
+
=== Uses :joins not :include
Another thing to note is that the joins created by Searchlogic 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:
View
22 lib/searchlogic/active_record/named_scopes.rb
@@ -51,7 +51,27 @@ def inner_joins(association_name)
::ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
end
- # See inner_joins, except this creates LEFT OUTER joins.
+ # A convenience methods to create a join on a polymorphic associations target.
+ # Ex:
+ #
+ # Audit.belong_to :auditable, :polymorphic => true
+ # User.has_many :audits, :as => :auditable
+ #
+ # Audit.inner_polymorphic_join(:user, :as => :auditable) # =>
+ # "INNER JOINER users ON users.id = audits.auditable_id AND audits.auditable_type = 'User'"
+ #
+ # This is used internally by searchlogic to handle accessing conditions on polymorphic associations.
+ def inner_polymorphic_join(target, options = {})
+ options[:on] ||= table_name
+ options[:on_table_name] ||= connection.quote_table_name(options[:on])
+ options[:target_table] ||= connection.quote_table_name(target.to_s.pluralize)
+ options[:as] ||= "owner"
+ postgres = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
+ "INNER JOIN #{options[:target_table]} ON #{options[:target_table]}.id = #{options[:on_table_name]}.#{options[:as]}_id AND " +
+ "#{options[:on_table_name]}.#{options[:as]}_type = #{postgres ? "E" : ""}'#{target.to_s.camelize}'"
+ end
+
+ # See inner_joins. Does the same thing except creates LEFT OUTER joins.
def left_outer_joins(association_name)
::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
end
View
60 lib/searchlogic/named_scopes/association_conditions.rb
@@ -13,7 +13,7 @@ def association_condition?(name)
def method_missing(name, *args, &block)
if !local_condition?(name) && details = association_condition_details(name)
- create_association_condition(details[:association], details[:condition], args)
+ create_association_condition(details[:association], details[:condition], args, details[:poly_class])
send(name, *args)
else
super
@@ -21,38 +21,52 @@ def method_missing(name, *args, &block)
end
def association_condition_details(name, last_condition = nil)
- assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
- return nil if assocs.empty?
-
+ non_poly_assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
+ poly_assocs = reflect_on_all_associations.reject { |assoc| !assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
+ return nil if non_poly_assocs.empty? && poly_assocs.empty?
+
name_with_condition = [name, last_condition].compact.join('_')
- if name_with_condition.to_s =~ /^(#{assocs.collect(&:name).join("|")})_(\w+)$/
+
+ association_name = nil
+ poly_type = nil
+ condition = nil
+
+ if name_with_condition.to_s =~ /^(#{non_poly_assocs.collect(&:name).join("|")})_(\w+)$/
association_name = $1
condition = $2
+ elsif name_with_condition.to_s =~ /^(#{poly_assocs.collect(&:name).join("|")})_(\w+)_type_(\w+)$/
+ association_name = $1
+ poly_type = $2
+ condition = $3
+ end
+
+ if association_name && condition
association = reflect_on_association(association_name.to_sym)
- klass = association.klass
+ klass = poly_type ? poly_type.camelcase.constantize : association.klass
if klass.condition?(condition)
- {:association => $1, :condition => $2}
+ {:association => association, :poly_class => poly_type && klass, :condition => condition}
else
nil
end
end
end
- def create_association_condition(association, condition, args)
- named_scope("#{association}_#{condition}", association_condition_options(association, condition, args))
+ def create_association_condition(association, condition_name, args, poly_class = nil)
+ name = [association.name, poly_class && "#{poly_class.name.underscore}_type", condition_name].compact.join("_")
+ named_scope(name, association_condition_options(association, condition_name, args, poly_class))
end
- def association_condition_options(association_name, association_condition, args)
- association = reflect_on_association(association_name.to_sym)
- scope = association.klass.send(association_condition, *args)
- scope_options = association.klass.named_scope_options(association_condition)
- arity = association.klass.named_scope_arity(association_condition)
+ def association_condition_options(association, association_condition, args, poly_class = nil)
+ klass = poly_class ? poly_class : association.klass
+ scope = klass.send(association_condition, *args)
+ scope_options = klass.named_scope_options(association_condition)
+ arity = klass.named_scope_arity(association_condition)
if !arity || arity == 0
# 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.scope(:find)
- prepare_named_scope_options(options, association)
+ prepare_named_scope_options(options, association, poly_class)
options
else
proc_args = arity_args(arity)
@@ -60,9 +74,9 @@ def association_condition_options(association_name, association_condition, args)
eval <<-"end_eval"
searchlogic_lambda(:#{arg_type}) { |#{proc_args.join(",")}|
- scope = association.klass.send(association_condition, #{proc_args.join(",")})
+ scope = klass.send(association_condition, #{proc_args.join(",")})
options = scope ? scope.scope(:find) : {}
- prepare_named_scope_options(options, association)
+ prepare_named_scope_options(options, association, poly_class)
options
}
end_eval
@@ -88,13 +102,19 @@ def arity_args(arity)
args
end
- def prepare_named_scope_options(options, association)
+ def prepare_named_scope_options(options, association, poly_class = nil)
options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
- options[:conditions] = association.klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)
+ klass = poly_class || association.klass
+ # sanitize the conditions locally so we get the right table name, otherwise the conditions will be evaluated on the original model
+ options[:conditions] = klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)
+
+ poly_join = poly_class && inner_polymorphic_join(poly_class.name.underscore, :as => association.name)
if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
- options[:joins] = [inner_joins(association.name), options[:joins]].flatten
+ options[:joins] = [poly_class ? poly_join : inner_joins(association.name), options[:joins]].flatten
+ elsif poly_class
+ options[:joins] = options[:joins].blank? ? poly_join : [poly_join, inner_joins(options[:joins])]
else
options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
end
View
9 lib/searchlogic/named_scopes/association_ordering.rb
@@ -29,14 +29,15 @@ def method_missing(name, *args, &block)
end
def association_ordering_condition_details(name)
- associations = reflect_on_all_associations.collect { |assoc| assoc.name }
- if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
- {:order_as => $1, :association => $2, :condition => $3}
+ associations = reflect_on_all_associations
+ association_names = associations.collect { |assoc| assoc.name }
+ if name.to_s =~ /^(ascend|descend)_by_(#{association_names.join("|")})_(\w+)$/
+ {:order_as => $1, :association => associations.find { |a| a.name == $2.to_sym }, :condition => $3}
end
end
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))
+ named_scope("#{order_as}_by_#{association.name}_#{condition}", association_condition_options(association, "#{order_as}_by_#{condition}", args))
end
end
end
View
6 lib/searchlogic/named_scopes/or_conditions.rb
@@ -101,10 +101,10 @@ def interpolate_or_conditions(parts)
end
def full_association_path(part, last_condition, given_assoc)
- path = [given_assoc.to_sym]
- part.sub!(/^#{given_assoc}_/, "")
+ path = [given_assoc.name]
+ part.sub!(/^#{given_assoc.name}_/, "")
klass = self
- while klass = klass.send(:reflect_on_association, given_assoc.to_sym)
+ while klass = klass.send(:reflect_on_association, given_assoc.name)
klass = klass.klass
if details = klass.send(:association_condition_details, part, last_condition)
path << details[:association]
View
7 spec/named_scopes/association_conditions_spec.rb
@@ -142,4 +142,11 @@
Company.named_scope(:users_count_10, :conditions => {:users_count => 10})
User.company_users_count_10.proxy_options.should == {:conditions => "\"companies\".\"users_count\" = 10", :joins => :company}
end
+
+ it "should polymorph" do
+ Audit.auditable_user_type_name_like("ben").proxy_options.should == {
+ :conditions => ["users.name LIKE ?", "%ben%"],
+ :joins => "INNER JOIN \"users\" ON \"users\".id = \"audits\".auditable_id AND \"audits\".auditable_type = 'User'"
+ }
+ end
end
View
9 spec/spec_helper.rb
@@ -11,6 +11,11 @@
ActiveRecord::Schema.verbose = false
ActiveRecord::Schema.define(:version => 1) do
+ create_table :audits do |t|
+ t.string :auditable_type
+ t.integer :auditable_id
+ end
+
create_table :companies do |t|
t.datetime :created_at
t.datetime :updated_at
@@ -66,6 +71,10 @@
Spec::Runner.configure do |config|
config.before(:each) do
+ class Audit < ActiveRecord::Base
+ belongs_to :auditable, :polymorphic => true
+ end
+
class Company < ActiveRecord::Base
has_many :users, :dependent => :destroy
end

0 comments on commit 2ee4174

Please sign in to comment.
Something went wrong with that request. Please try again.