binarylogic-searchlogic 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +191 -0
- data/Rakefile +48 -0
- data/VERSION.yml +4 -0
- data/init.rb +1 -0
- data/lib/searchlogic/core_ext/object.rb +39 -0
- data/lib/searchlogic/core_ext/proc.rb +11 -0
- data/lib/searchlogic/named_scopes/associations.rb +144 -0
- data/lib/searchlogic/named_scopes/conditions.rb +215 -0
- data/lib/searchlogic/named_scopes/ordering.rb +53 -0
- data/lib/searchlogic/rails_helpers.rb +63 -0
- data/lib/searchlogic/search.rb +128 -0
- data/lib/searchlogic.rb +18 -0
- data/rails/init.rb +1 -0
- data/spec/core_ext/object_spec.rb +7 -0
- data/spec/core_ext/proc_spec.rb +9 -0
- data/spec/named_scopes/associations_spec.rb +124 -0
- data/spec/named_scopes/conditions_spec.rb +253 -0
- data/spec/named_scopes/ordering_spec.rb +23 -0
- data/spec/search_spec.rb +251 -0
- data/spec/spec_helper.rb +77 -0
- metadata +81 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Ben Johnson of Binary Logic
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
= Searchlogic
|
2
|
+
|
3
|
+
<b>Searchlogic has been <em>completely</em> rewritten for v2. It is much simpler and has taken an entirely new approach. To give you an idea, v1 had ~2300 lines of code, v2 has ~350 lines of code.</b>
|
4
|
+
|
5
|
+
<b>Attention v1 users. v2 breaks backwards compatibility. Please make sure your gem declaration for searchlogic looks like this so that you app does not break when the gem is updated: config.gem "searchlogic", :version => "~> 1.6.6"</b>
|
6
|
+
|
7
|
+
Searchlogic provides common named scopes and object based searching.
|
8
|
+
|
9
|
+
== Helpful links
|
10
|
+
|
11
|
+
* <b>Documentation:</b> http://rdoc.info/projects/binarylogic/searchlogic
|
12
|
+
* <b>Repository:</b> http://github.com/binarylogic/searchlogic/tree/master
|
13
|
+
* <b>Bugs / feature suggestions:</b> http://binarylogic.lighthouseapp.com/projects/16601-searchlogic
|
14
|
+
* <b>Google group:</b> http://groups.google.com/group/searchlogic
|
15
|
+
|
16
|
+
<b>Before contacting me directly, please read:</b>
|
17
|
+
|
18
|
+
If you find a bug or a problem please post it on lighthouse. If you need help with something, please use google groups. I check both regularly and get emails when anything happens, so that is the best place to get help. This also benefits other people in the future with the same questions / problems. Thank you.
|
19
|
+
|
20
|
+
== Install & use
|
21
|
+
|
22
|
+
In your rails project:
|
23
|
+
|
24
|
+
# config/environment.rb
|
25
|
+
config.gem "binarylogic-searchlogic",
|
26
|
+
:lib => 'searchlogic',
|
27
|
+
:source => 'http://gems.github.com',
|
28
|
+
:version => '0.6.6'
|
29
|
+
|
30
|
+
Then install the gem:
|
31
|
+
|
32
|
+
rake gems:install
|
33
|
+
|
34
|
+
That's it, you are ready to go. See below for usage examples.
|
35
|
+
|
36
|
+
== Search using conditions on columns
|
37
|
+
|
38
|
+
Instead of explaining what Searchlogic can do, let me show you. Let's start at the top:
|
39
|
+
|
40
|
+
# We have the following model
|
41
|
+
User(id: integer, created_at: datetime, username: string, age: integer)
|
42
|
+
|
43
|
+
# Searchlogic gives you a bunch of named scopes for free:
|
44
|
+
User.username_equals("bjohnson")
|
45
|
+
User.username_does_not_equal("bjohnson")
|
46
|
+
User.username_begins_with("bjohnson")
|
47
|
+
User.username_like("bjohnson")
|
48
|
+
User.username_ends_with("bjohnson")
|
49
|
+
User.age_greater_than(20)
|
50
|
+
User.age_greater_than_or_equal_to(20)
|
51
|
+
User.age_less_than(20)
|
52
|
+
User.age_less_than_or_equal_to(20)
|
53
|
+
User.username_null
|
54
|
+
User.username_blank
|
55
|
+
|
56
|
+
# You can also order by columns
|
57
|
+
User.ascend_by_username
|
58
|
+
User.descend_by_username
|
59
|
+
User.order("ascend_by_username")
|
60
|
+
|
61
|
+
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:
|
62
|
+
|
63
|
+
scope = User.username_like("bjohnson").age_greater_than(20).ascend_by_username
|
64
|
+
scope.all
|
65
|
+
scope.first
|
66
|
+
scope.count
|
67
|
+
# etc...
|
68
|
+
|
69
|
+
That's all pretty standard, but here's where Searchlogic starts to get interesting...
|
70
|
+
|
71
|
+
== Search using conditions on association columns
|
72
|
+
|
73
|
+
You also get named scopes for any of your associations:
|
74
|
+
|
75
|
+
# We have the following relationships
|
76
|
+
User.has_many :orders
|
77
|
+
Order.has_many :line_items
|
78
|
+
LineItem
|
79
|
+
|
80
|
+
# Set conditions on association columns
|
81
|
+
User.orders_total_greater_than(20)
|
82
|
+
User.orders_line_items_price_greater_than(20)
|
83
|
+
|
84
|
+
# Order by association columns
|
85
|
+
User.ascend_by_order_total
|
86
|
+
User.descend_by_orders_line_items_price
|
87
|
+
|
88
|
+
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:
|
89
|
+
|
90
|
+
Benchmark.bm do |x|
|
91
|
+
x.report { 10.times { Event.tickets_id_gt(10).all(:include => :tickets) } }
|
92
|
+
x.report { 10.times { Event.tickets_id_gt(10).all } }
|
93
|
+
end
|
94
|
+
user system total real
|
95
|
+
10.120000 0.170000 10.290000 ( 12.625521)
|
96
|
+
2.630000 0.050000 2.680000 ( 3.313754)
|
97
|
+
|
98
|
+
If you want to use the :include option, just specify it:
|
99
|
+
|
100
|
+
User.orders_line_items_price_greater_than(20).all(:include => {:orders => :line_items})
|
101
|
+
|
102
|
+
Obviously, only do this if you want to actually use the included objects.
|
103
|
+
|
104
|
+
== Make searching and ordering data in your application trivial
|
105
|
+
|
106
|
+
The above is great, but what about tying all of this in with a search form in your application? Just do this...
|
107
|
+
|
108
|
+
User.search(:username_like => "bjohnson", :age_less_than => 20)
|
109
|
+
|
110
|
+
The above is equivalent to:
|
111
|
+
|
112
|
+
User.username_like("bjohnson").age_less_than(20)
|
113
|
+
|
114
|
+
All that the search method does is chain named scopes together for you. What's so great about that? It keeps your controllers extremely simple:
|
115
|
+
|
116
|
+
class UsersController < ApplicationController
|
117
|
+
def index
|
118
|
+
@search = User.search(params[:search])
|
119
|
+
@users = @search.all
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
It doesn't get any simpler than that. Adding a search condition is as simple as adding a condition to your form. Remember all of those named scopes above? Just create fields with the same names:
|
124
|
+
|
125
|
+
- form_for @search do |f|
|
126
|
+
= f.text_field :username_like
|
127
|
+
= f.select :age_greater_than, (0..100)
|
128
|
+
= f.text_field :orders_total_greater_than
|
129
|
+
= f.submit
|
130
|
+
|
131
|
+
When a Searchlogic::Search object is passed to form_for it will add a hidden field for the "order" condition, to preserve the order of the data. If you want to order your search with a link, just specify the name of the column. Ex:
|
132
|
+
|
133
|
+
= order @search, :by => :age
|
134
|
+
|
135
|
+
This will create a link that alternates between calling "ascend_by_age" and "descend_by_age". If you wanted to order your data by more than just a column, create your own named scopes: "ascend_by_*" and "descend_by_*". The "order" helper is a very straight forward helper, checkout the docs for some of the options.
|
136
|
+
|
137
|
+
== Use your existing named scopes
|
138
|
+
|
139
|
+
This is one of the big differences between Searchlogic v1 and v2. What about your existing named scopes? Let's say you have this:
|
140
|
+
|
141
|
+
User.named_scope :four_year_olds, :conditions => {:age => 4}
|
142
|
+
|
143
|
+
Again, these are all just named scopes, use it in the same way:
|
144
|
+
|
145
|
+
User.search(:four_year_olds => true, :username_like => "bjohnson")
|
146
|
+
|
147
|
+
Notice we pass true as the value. If a named scope does not accept any parameters (arity == 0) you can simply pass it true or false. If you pass false, the named scope will be ignored. If your named scope accepts a parameter, the value will be passed right to the named scope regardless of the value.
|
148
|
+
|
149
|
+
Now just throw it in your form:
|
150
|
+
|
151
|
+
- form_for @search do |f|
|
152
|
+
= f.text_field :username_like
|
153
|
+
= f.check_box :four_year_olds
|
154
|
+
= f.submit
|
155
|
+
|
156
|
+
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.
|
157
|
+
|
158
|
+
== Use any or all
|
159
|
+
|
160
|
+
Every condition you've seen in this readme also has 2 related conditions that you can use. Example:
|
161
|
+
|
162
|
+
User.username_like_any("bjohnson", "thunt") # will return any users that have either of those strings in their username
|
163
|
+
User.username_like_all("bjohnson", "thunt") # will return any users that have all of those string in their username
|
164
|
+
User.username_like_any(["bjohnson", "thunt"]) # also accept an array
|
165
|
+
|
166
|
+
This is great for checkbox filters, etc. Where you can pass an array right from your form to this condition.
|
167
|
+
|
168
|
+
== Pagination (leverage will_paginate)
|
169
|
+
|
170
|
+
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:
|
171
|
+
|
172
|
+
User.username_like("bjohnson").age_less_than(20).paginate(:page => params[:page])
|
173
|
+
User.search(:username_like => "bjohnson", :age_less_than => 20).paginate(:page => params[:page])
|
174
|
+
|
175
|
+
If you don't like will_paginate, use another solution, or roll your own. Pagination really has nothing to do with searching, and the main goal for Searchlogic v2 was to keep it lean and simple. No reason to recreate the wheel and bloat the library.
|
176
|
+
|
177
|
+
== Under the hood
|
178
|
+
|
179
|
+
Before I use a library in my application I like to glance at the source and try to at least understand the basics of how it works. If you are like me, a nice little explanation from the author is always helpful:
|
180
|
+
|
181
|
+
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.
|
182
|
+
|
183
|
+
That's about it, the named scope options are pretty bare bones and created just like you would manually.
|
184
|
+
|
185
|
+
== Credit
|
186
|
+
|
187
|
+
Thanks a lot to {Tyler Hunt}[http://github.com/tylerhunt] for helping plan, design, and start the project. He was a big help.
|
188
|
+
|
189
|
+
== Copyright
|
190
|
+
|
191
|
+
Copyright (c) 2009 {Ben Johnson of Binary Logic}[http://www.binarylogic.com], released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "searchlogic"
|
8
|
+
gem.summary = "Searchlogic provides common named scopes and object based searching."
|
9
|
+
gem.email = "bjohnson@binarylogic.com"
|
10
|
+
gem.homepage = "http://github.com/binarylogic/search"
|
11
|
+
gem.authors = ["Ben Johnson of Binary Logic"]
|
12
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
13
|
+
end
|
14
|
+
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'spec/rake/spectask'
|
20
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
21
|
+
spec.libs << 'lib' << 'spec'
|
22
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
23
|
+
end
|
24
|
+
|
25
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
26
|
+
spec.libs << 'lib' << 'spec'
|
27
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
28
|
+
spec.rcov = true
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task :default => :spec
|
33
|
+
|
34
|
+
require 'rake/rdoctask'
|
35
|
+
Rake::RDocTask.new do |rdoc|
|
36
|
+
if File.exist?('VERSION.yml')
|
37
|
+
config = YAML.load(File.read('VERSION.yml'))
|
38
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
39
|
+
else
|
40
|
+
version = ""
|
41
|
+
end
|
42
|
+
|
43
|
+
rdoc.rdoc_dir = 'rdoc'
|
44
|
+
rdoc.title = "search #{version}"
|
45
|
+
rdoc.rdoc_files.include('README*')
|
46
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
47
|
+
end
|
48
|
+
|
data/VERSION.yml
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "searchlogic"
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module CoreExt
|
3
|
+
# Contains extensions for the Object class that Searchlogic uses.
|
4
|
+
module Object
|
5
|
+
# Searchlogic needs to know the expected type of the condition value so that it can properly cast
|
6
|
+
# the value in the Searchlogic::Search object. For example:
|
7
|
+
#
|
8
|
+
# search = User.search(:id_gt => "1")
|
9
|
+
#
|
10
|
+
# You would expect this:
|
11
|
+
#
|
12
|
+
# search.id_gt => 1
|
13
|
+
#
|
14
|
+
# Not this:
|
15
|
+
#
|
16
|
+
# search.id_gt => "1"
|
17
|
+
#
|
18
|
+
# Parameter values from forms are ALWAYS strings, so we have to cast them. Just like ActiveRecord
|
19
|
+
# does when you instantiate a new User object.
|
20
|
+
#
|
21
|
+
# The problem is that ruby has no variable types, so Searchlogic needs to know what type you are expecting
|
22
|
+
# for your named scope. So instead of this:
|
23
|
+
#
|
24
|
+
# named_scope :id_gt, lambda { |value| {:conditions => ["id > ?", value]} }
|
25
|
+
#
|
26
|
+
# You need to do this:
|
27
|
+
#
|
28
|
+
# named_scope :id_gt, searchlogic_lambda(:integer) { |value| {:conditions => ["id > ?", value]} }
|
29
|
+
#
|
30
|
+
# If you are wanting a string, you don't have to do anything, because Searchlogic assumes you are want a string.
|
31
|
+
# If you want something else, you need to specify it as I did in the above example.
|
32
|
+
def searchlogic_lambda(type = :string, &block)
|
33
|
+
proc = lambda(&block)
|
34
|
+
proc.searchlogic_arg_type = type
|
35
|
+
proc
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module NamedScopes
|
3
|
+
# Handles dynamically creating named scopes for associations.
|
4
|
+
module Associations
|
5
|
+
def condition?(name) # :nodoc:
|
6
|
+
super || association_condition?(name) || association_alias_condition?(name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def primary_condition_name(name) # :nodoc:
|
10
|
+
if result = super
|
11
|
+
result
|
12
|
+
elsif association_condition?(name)
|
13
|
+
name.to_sym
|
14
|
+
elsif details = association_alias_condition_details(name)
|
15
|
+
"#{details[:association]}_#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
|
16
|
+
else
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Is the name of the method a valid name for an association condition?
|
22
|
+
def association_condition?(name)
|
23
|
+
!association_condition_details(name).nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Is the ane of the method a valie name for an association alias condition?
|
27
|
+
# An alias being "gt" for "greater_than", etc.
|
28
|
+
def association_alias_condition?(name)
|
29
|
+
!association_alias_condition_details(name).nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def method_missing(name, *args, &block)
|
34
|
+
if details = association_condition_details(name)
|
35
|
+
create_association_condition(details[:association], details[:column], details[:condition], args)
|
36
|
+
send(name, *args)
|
37
|
+
elsif details = association_alias_condition_details(name)
|
38
|
+
create_association_alias_condition(details[:association], details[:column], details[:condition], args)
|
39
|
+
send(name, *args)
|
40
|
+
elsif details = association_ordering_condition_details(name)
|
41
|
+
create_association_ordering_condition(details[:association], details[:order_as], details[:column], args)
|
42
|
+
send(name, *args)
|
43
|
+
else
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def association_condition_details(name)
|
49
|
+
associations = reflect_on_all_associations.collect { |assoc| assoc.name }
|
50
|
+
if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::PRIMARY_CONDITIONS.join("|")})$/
|
51
|
+
{:association => $1, :column => $2, :condition => $3}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_association_condition(association_name, column, condition, args)
|
56
|
+
named_scope("#{association_name}_#{column}_#{condition}", association_condition_options(association_name, "#{column}_#{condition}", args))
|
57
|
+
end
|
58
|
+
|
59
|
+
def association_alias_condition_details(name)
|
60
|
+
associations = reflect_on_all_associations.collect { |assoc| assoc.name }
|
61
|
+
if name.to_s =~ /^(#{associations.join("|")})_(\w+)_(#{Conditions::ALIAS_CONDITIONS.join("|")})$/
|
62
|
+
{:association => $1, :column => $2, :condition => $3}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_association_alias_condition(association, column, condition, args)
|
67
|
+
primary_condition = primary_condition(condition)
|
68
|
+
alias_name = "#{association}_#{column}_#{condition}"
|
69
|
+
primary_name = "#{association}_#{column}_#{primary_condition}"
|
70
|
+
send(primary_name, *args) # go back to method_missing and make sure we create the method
|
71
|
+
(class << self; self; end).class_eval { alias_method alias_name, primary_name }
|
72
|
+
end
|
73
|
+
|
74
|
+
def association_ordering_condition_details(name)
|
75
|
+
associations = reflect_on_all_associations.collect { |assoc| assoc.name }
|
76
|
+
if name.to_s =~ /^(ascend|descend)_by_(#{associations.join("|")})_(\w+)$/
|
77
|
+
{:order_as => $1, :association => $2, :column => $3}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def create_association_ordering_condition(association_name, order_as, column, args)
|
82
|
+
named_scope("#{order_as}_by_#{association_name}_#{column}", association_condition_options(association_name, "#{order_as}_by_#{column}", args))
|
83
|
+
end
|
84
|
+
|
85
|
+
def association_condition_options(association_name, association_condition, args)
|
86
|
+
association = reflect_on_association(association_name.to_sym)
|
87
|
+
scope = association.klass.send(association_condition, *args)
|
88
|
+
scope_options = association.klass.named_scope_options(association_condition)
|
89
|
+
arity = association.klass.named_scope_arity(association_condition)
|
90
|
+
|
91
|
+
if !arity || arity == 0
|
92
|
+
# The underlying condition doesn't require any parameters, so let's just create a simple
|
93
|
+
# named scope that is based on a hash.
|
94
|
+
options = scope.proxy_options
|
95
|
+
add_left_outer_joins(options, association)
|
96
|
+
options
|
97
|
+
else
|
98
|
+
# The underlying condition requires parameters, let's match the parameters it requires
|
99
|
+
# and pass those onto the named scope. We can't use proxy_options because that returns the
|
100
|
+
# result after a value has been passed.
|
101
|
+
proc_args = []
|
102
|
+
if arity > 0
|
103
|
+
arity.times { |i| proc_args << "arg#{i}"}
|
104
|
+
else
|
105
|
+
positive_arity = arity * -1
|
106
|
+
positive_arity.times do |i|
|
107
|
+
if i == (positive_arity - 1)
|
108
|
+
proc_args << "*arg#{i}"
|
109
|
+
else
|
110
|
+
proc_args << "arg#{i}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
eval <<-"end_eval"
|
115
|
+
searchlogic_lambda(:#{scope_options.searchlogic_arg_type}) { |#{proc_args.join(",")}|
|
116
|
+
options = association.klass.named_scope_options(association_condition).call(#{proc_args.join(",")})
|
117
|
+
add_left_outer_joins(options, association)
|
118
|
+
options
|
119
|
+
}
|
120
|
+
end_eval
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# In a named scope you have 2 options for adding joins: :include and :joins.
|
125
|
+
#
|
126
|
+
# :include will execute multiple queries for each association and instantiate objects for each association.
|
127
|
+
# This is not what we want when we are searching. The only other option left is :joins. We can pass the
|
128
|
+
# name of the association directly, but AR creates an INNER JOIN. If we are ordering by an association's
|
129
|
+
# attribute, and that association is optional, the records without an association will be omitted. Again,
|
130
|
+
# not what we want.
|
131
|
+
#
|
132
|
+
# So the only option left is to use :joins with manually written SQL. We can still have AR generate this SQL
|
133
|
+
# for us by leveraging it's join dependency classes. Instead of using the InnerJoinDependency we use the regular
|
134
|
+
# JoinDependency which creates a LEFT OUTER JOIN, which is what we want.
|
135
|
+
#
|
136
|
+
# The code below was extracted out of AR's add_joins! method and then modified.
|
137
|
+
def add_left_outer_joins(options, association)
|
138
|
+
join = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association.name, nil).join_associations.collect { |assoc| assoc.association_join }.join.strip
|
139
|
+
options[:joins] ||= []
|
140
|
+
options[:joins].unshift(join)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|