Permalink
Browse files

Added feature for combining conditions with OR and as well as some se…

…rious documentation cleanup
  • Loading branch information...
1 parent 5288f08 commit 24bc3715a2baf3a21ae95d0d7d7d8f2868503458 @binarylogic committed Aug 22, 2009
View
@@ -1,3 +1,7 @@
+== 2.2.4
+
+* Thanks to laserlemon for support of ranges and arrays in the equals condition.
+
== 2.2.3 released 2009-07-31
* Fixed bug when an associations named scope joins is a string or an array of strings, the joins we add in automatically should also be a string, not a symbol.
View
@@ -1,6 +1,6 @@
= Searchlogic
-Searchlogic provides common named scopes and object based searching for ActiveRecord.
+Searchlogic provides tools that make using ActiveRecord named scopes easier and less repetitive. It helps keep your code DRY, clean, and simple.
== Helpful links
@@ -40,35 +40,48 @@ Instead of explaining what Searchlogic can do, let me show you. Let's start at t
# Searchlogic gives you a bunch of named scopes for free:
User.username_equals("bjohnson")
+ User.username_equals(["bjohnson", "thunt"])
+ User.username_equals("a".."b")
User.username_does_not_equal("bjohnson")
User.username_begins_with("bjohnson")
+ User.username_not_begin_with("bjohnson")
User.username_like("bjohnson")
+ User.username_not_like("bjohnson")
User.username_ends_with("bjohnson")
+ User.username_not_end_with("bjohnson")
User.age_greater_than(20)
User.age_greater_than_or_equal_to(20)
User.age_less_than(20)
User.age_less_than_or_equal_to(20)
User.username_null
+ User.username_not_null
User.username_blank
-
- # You can also order by columns
- User.ascend_by_username
- User.descend_by_username
- User.order("ascend_by_username")
Any named scope Searchlogic creates is dynamic and created via method_missing. Meaning it will only create what you need. Also, keep in mind, these are just named scopes, you can chain them, call methods off of them, etc:
- scope = User.username_like("bjohnson").age_greater_than(20).ascend_by_username
+ scope = User.username_like("bjohnson").age_greater_than(20).id_less_than(55)
scope.all
scope.first
scope.count
# etc...
-That's all pretty standard, but here's where Searchlogic starts to get interesting...
+For a complete list of conditions please see the constants in Searchlogic::NamedScopes::Conditions.
+
+== Use condition aliases
+
+Typing out 'greater_than_or_equal_to' is not fun. Instead Searchlogic provides various aliases for the conditions. For a complete list please see Searchlogic::NamedScopes::Conditions. But they are pretty straightforward:
+
+ User.username_is(10)
+ User.username_eq(10)
+ User.id_lt(10)
+ User.id_lte(10)
+ # etc...
+
+== Search using scopes in associated classes
-== Search using conditions on associated columns
+This is my favorite part of Searchlogic. You can dynamically call scopes on associated classes and Searchlogic will take care of creating the necessary joins for you. This is REALY nice for keeping your code DRY. The best way to explain this is to show you:
-You also get named scopes for any of your associations:
+Let's take some basic scopes that Searchlogic provides:
# We have the following relationships
User.has_many :orders
@@ -83,7 +96,17 @@ 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 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:
+This is recursive, you can travel through your associations simply by typing it in the name of the method. Again these are just named scopes. You can chain them together, call methods off of them, etc.
+
+Also, these conditions aren't limited to the scopes Searchlogic provides. You can use your own scopes. Like this:
+
+ LineItem.named_scope :expensive, :conditions => "line_items.price > 500"
+
+ User.orders_line_items_expensive(true)
+
+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?
+
+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:
Benchmark.bm do |x|
x.report { 10.times { Event.tickets_id_gt(10).all(:include => :tickets) } }
@@ -99,6 +122,45 @@ If you want to use the :include option, just specify it:
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.
+== Order your search
+
+Just like the various conditions, Searchlogic gives you some very basic scopes for ordering your data:
+
+ User.ascend_by_id
+ User.descend_by_id
+ User.ascend_by_orders_line_items_price
+ # etc...
+
+== Use any or all
+
+Every condition you've seen in this readme also has 2 related conditions that you can use. Example:
+
+ User.username_like_any("bjohnson", "thunt") # will return any users that have either of the strings in their username
+ User.username_like_all("bjohnson", "thunt") # will return any users that have all of the strings in their username
+ User.username_like_any(["bjohnson", "thunt"]) # also accepts an array
+
+This is great for checkbox filters, etc. Where you can pass an array right from your form to this condition.
+
+== Combine scopes with 'OR'
+
+In the same fashion that Searchlogic provides a tool for accessing scopes in associated classes, it also provides a tool for combining scopes with 'OR'. As we all know, when scopes are combined they are joined with 'AND', but sometimes you need to combine scopes with 'OR'. Searchlogic solves this problem:
+
+ User.username_or_first_name_like("ben")
+ => "username LIKE '%ben%' OR first_name like'%ben%'"
+
+ User.id_or_age_lt_or_username_or_first_name_begins_with(10)
+ => "id < 10 OR age < 10 OR username LIKE 'ben%' OR first_name like'ben%'"
+
+Notice you don't have to specify the explicit condition (like, gt, lt, begins with, etc.). You just need to eventually specify it. If you specify a column it will just use the next condition specified. So instead of:
+
+ User.username_like_or_first_name_like("ben")
+
+You can do:
+
+ User.username_or_first_name_like("ben")
+
+Again, these just map to named scopes. Use Searchlogic's dynamic scopes, use scopes on associations, use your own custom scopes. As long as it maps to a named scope it will join the conditions with 'OR'. There are no limitations.
+
== 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...
@@ -178,16 +240,6 @@ Now just throw it in your form:
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
-
-Every condition you've seen in this readme also has 2 related conditions that you can use. Example:
-
- User.username_like_any("bjohnson", "thunt") # will return any users that have either of the strings in their username
- User.username_like_all("bjohnson", "thunt") # will return any users that have all of the strings in their username
- User.username_like_any(["bjohnson", "thunt"]) # also accepts an array
-
-This is great for checkbox filters, etc. Where you can pass an array right from your form to this condition.
-
== Pagination (leverage will_paginate)
Instead of recreating the wheel with pagination, Searchlogic works great with will_paginate. All that Searchlogic is doing is creating named scopes, and will_paginate works great with named scopes:
View
@@ -5,8 +5,8 @@ begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "searchlogic"
- gem.summary = "Searchlogic provides common named scopes and object based searching for ActiveRecord."
- gem.description = "Searchlogic provides common named scopes and object based searching for ActiveRecord."
+ gem.summary = "Searchlogic provides tools that make using ActiveRecord named scopes easier and less repetitive."
+ gem.description = "Searchlogic provides tools that make using ActiveRecord named scopes easier and less repetitive."
gem.email = "bjohnson@binarylogic.com"
gem.homepage = "http://github.com/binarylogic/searchlogic"
gem.authors = ["Ben Johnson of Binary Logic"]
@@ -30,5 +30,6 @@ Spec::Rake::SpecTask.new(:rcov) do |spec|
spec.rcov = true
end
+task :spec => :check_dependencies
task :default => :spec
View
@@ -7,36 +7,31 @@
require "searchlogic/named_scopes/association_conditions"
require "searchlogic/named_scopes/association_ordering"
require "searchlogic/named_scopes/alias_scope"
+require "searchlogic/named_scopes/or_conditions"
require "searchlogic/search"
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
+ 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::AssociationConditions)
ActiveRecord::Base.extend(Searchlogic::NamedScopes::AssociationOrdering)
ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering)
ActiveRecord::Base.extend(Searchlogic::NamedScopes::AliasScope)
+ActiveRecord::Base.extend(Searchlogic::NamedScopes::OrConditions)
ActiveRecord::Base.extend(Searchlogic::Search::Implementation)
# Try to use the search method, if it's available. Thinking sphinx and other plugins
# like to use that method as well.
if !ActiveRecord::Base.respond_to?(:search)
- ActiveRecord::Base.class_eval do
- class << self
- alias_method :search, :searchlogic
- end
- end
+ ActiveRecord::Base.class_eval { class << self; alias_method :search, :searchlogic; end }
end
if defined?(ActionController)
@@ -8,7 +8,7 @@ def self.included(klass)
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
@@ -43,9 +43,18 @@ def named_scope_arity(name)
# 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.
+ #
+ # Also, don't worry about breaking up the joins or retriving multiple joins.
+ # ActiveRecord will remove dupilicate joins and Searchlogic assists ActiveRecord in
+ # breaking up your joins so that they are unique.
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.
+ def left_outer_joins(association_name)
+ ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
+ end
end
end
end
@@ -1,34 +1,36 @@
module Searchlogic
module NamedScopes
# Adds the ability to create alias scopes that allow you to alias a named
- # scope or create a named scope procedure, while at the same time letting
- # Searchlogic know that this is a safe method.
+ # scope or create a named scope procedure. See the alias_scope method for a more
+ # detailed explanation.
module AliasScope
- # The searchlogic Search class takes a hash and chains the values together as named scopes.
- # For security reasons the only hash keys that are allowed must be mapped to named scopes.
- # You can not pass the name of a class method and expect that to be called. In some instances
- # you might create a class method that essentially aliases a named scope or represents a
- # named scope procedure. Ex:
- #
- # User.named_scope :teenager, :conditions => ["age >= ? AND age <= ?", 13, 19]
- #
- # This is obviously a very basic example, but there is logic that is duplicated here. For
- # more complicated named scopes this might make more sense, but to make my point you could
- # do something like this instead
+ # In some instances you might create a class method that essentially aliases a named scope
+ # or represents a named scope procedure. Ex:
#
# class User
# def teenager
# age_gte(13).age_lte(19)
# end
# end
#
- # As I stated above, you could not use this method with the Searchlogic::Search class because
- # there is no way to tell that this is actually a named scope. Instead, Searchlogic lets you
- # do something like this:
+ # This is obviously a very basic example, but notice how we are utilizing already existing named
+ # scopes so that we do not have to repeat ourself. This method makes a lot more sense when you are
+ # dealing with complicated named scope.
+ #
+ # There is a problem though. What if you want to use this in your controller's via the 'search' method:
+ #
+ # User.search(:teenager => true)
+ #
+ # You would expect that to work, but how does Searchlogic::Search tell the difference between your
+ # 'teenager' method and the 'destroy_all' method. It can't, there is no way to tell unless we actually
+ # call the method, which we obviously can not do.
+ #
+ # The being said, we need a way to tell searchlogic that this is method is safe. Here's how you do that:
#
# User.alias_scope :teenager, lambda { age_gte(13).age_lte(19) }
#
- # It fits in better, at the same time Searchlogic will know this is an acceptable named scope.
+ # This feels better, it feels like our other scopes, and it provides a way to tell Searchlogic that this
+ # is a safe method.
def alias_scope(name, options = nil)
alias_scopes[name.to_sym] = options
(class << self; self end).instance_eval do
@@ -1,21 +1,11 @@
module Searchlogic
module NamedScopes
- # Handles dynamically creating named scopes for associations.
+ # Handles dynamically creating named scopes for associations. See the README for a detailed explanation.
module AssociationConditions
def condition?(name) # :nodoc:
super || association_condition?(name)
end
- def primary_condition_name(name) # :nodoc:
- if result = super
- result
- elsif association_condition?(name)
- name.to_sym
- else
- nil
- end
- end
-
private
def association_condition?(name)
!association_condition_details(name).nil?
@@ -64,23 +54,7 @@ def association_condition_options(association_name, association_condition, args)
prepare_named_scope_options(options, association)
options
else
- # The underlying condition requires parameters, let's match the parameters it requires
- # and pass those onto the named scope. We can't use proxy_options because that returns the
- # result after a value has been passed.
- proc_args = []
- if arity > 0
- arity.times { |i| proc_args << "arg#{i}"}
- else
- positive_arity = arity * -1
- positive_arity.times do |i|
- if i == (positive_arity - 1)
- proc_args << "*arg#{i}"
- else
- proc_args << "arg#{i}"
- end
- end
- end
-
+ proc_args = arity_args(arity)
arg_type = (scope_options.respond_to?(:searchlogic_arg_type) && scope_options.searchlogic_arg_type) || :string
eval <<-"end_eval"
@@ -94,8 +68,27 @@ def association_condition_options(association_name, association_condition, args)
end
end
+ # Used to match the new scopes parameters to the underlying scope. This way we can disguise the
+ # new scope as best as possible instead of taking the easy way out and using *args.
+ def arity_args(arity)
+ args = []
+ if arity > 0
+ arity.times { |i| args << "arg#{i}" }
+ else
+ positive_arity = arity * -1
+ positive_arity.times do |i|
+ if i == (positive_arity - 1)
+ args << "*arg#{i}"
+ else
+ args << "arg#{i}"
+ end
+ end
+ end
+ args
+ end
+
def prepare_named_scope_options(options, association)
- options.delete(:readonly)
+ options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
options[:joins] = [inner_joins(association.name), options[:joins]].flatten
Oops, something went wrong.

0 comments on commit 24bc371

Please sign in to comment.