flyerhzm-bullet 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Richard Huang (flyerhzm@gmail.com)
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.textile ADDED
@@ -0,0 +1,243 @@
1
+ h1. Bullet
2
+
3
+ This plugin is aimed to give you some performance suggestion about ActiveRecord usage, what should use but not use, such as eager loading, counter cache and so on, what should not use but use, such as unused eager loading.
4
+
5
+ Now it provides you the suggestion of eager loading and unused eager loading.
6
+
7
+ The others are todo, next may be couter cache.
8
+
9
+ ****************************************************************************
10
+
11
+ h2. Install
12
+
13
+ install as gem:
14
+ <pre><code>
15
+ gem sources -a http://gems.github.com
16
+ gem install flyerhzm-bullet
17
+ </code></pre>
18
+
19
+ install as plugin:
20
+ <pre><code>script/plugin install git://github.com/flyerhzm/bullet.git</code></pre>
21
+
22
+ ****************************************************************************
23
+
24
+ h2. Usage
25
+
26
+ * "Eager Loading, protect N+1 query and protect from unused eager loading":#EagerLoading
27
+
28
+ ****************************************************************************
29
+
30
+ h2. Step by step example
31
+
32
+ * "Eager Loading, protect N+1 query and protect from unused eager loading":#EagerLoadingExample
33
+
34
+
35
+ ****************************************************************************
36
+
37
+ h3. Eager Loading, protect N+1 query and protect from unused eager loading, usage
38
+ <a id="EagerLoading"/></a>
39
+
40
+ *important*: It is strongly recommended to disable cache in browser.
41
+
42
+ * add configuration to environment
43
+ <pre><code>
44
+ Bullet.enable = true
45
+ Bullet::Association.logger = true
46
+ Bullet::Association.alert = true
47
+ </code></pre>
48
+ ** Bullet.enable (required), if enable is true (default is false), Bullet plugin is enabled. Otherwise, Bullet plugin is disabled.
49
+ ** Bullet::Association.logger (optional), if logger is true (default is true), the N+1 query hints will be appended to <code>log/bullet.log</code> with N+1 query method call stack. Otherwise, no hint to log/bullet.log.
50
+ ** Bullet::Association.alert (optional), if alert is true (default value), alert box will popup if there is N+1 query when browsing web page. Otherwise, no alert box.
51
+
52
+ * browse the webpage, if there are N+1 queries or unused eager loading, alert box and bullet log will generate according to configurations. Alert box will only popup when the request's Content-Type is text/html, and <code>log/bullet.log</code> will produce whatever the request is.
53
+
54
+ * example of <code>log/bullet.log</code>
55
+ <pre><code>
56
+ 2009-08-25 20:40:17[INFO] N+1 Query: PATH_INFO: /posts; model: Post => associations: [comments]·
57
+ Add to your finder: :include => [:comments]
58
+ 2009-08-25 20:40:17[INFO] N+1 Query: method call stack:·
59
+ /Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb'
60
+ /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
61
+ /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb'
62
+ /Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
63
+ </code></pre>
64
+ It represents that in request '/posts', there is a N+1 query from Post to comments. It means you may have a logic code in controller <code>@posts = Post.find(:all)</code> should be changed to <code>@posts = Post.find(:all, :include => :comments)</code>
65
+ <pre><code>
66
+ 2009-08-25 20:53:56[INFO] Unused preload associations: PATH_INFO: /posts; model: Post => associations: [comments]·
67
+ Remove from your finder: :include => [:comments]
68
+ </code></pre>
69
+ It represents that in request '/posts', there is a used eager loading from Post to comments. It means you may have a logic code in controller <code>@posts = Post.find(:all, :include => :comments)</code> should be changed to <code>@posts = Post.find(:all)</code>
70
+
71
+ * To see what causes N+1 queries, check the <code>spec/bullet_association_spec.rb</code>
72
+
73
+ * Rake tasks
74
+ <code>rake bullet:log:clear</code>, clear the <code>log/bullet.log</code>
75
+
76
+ ****************************************************************************
77
+
78
+ h3. Eager Loading, protect N+1 query and protect from unused eager loading, step by step example
79
+ <a id="EagerLoadingExample"/></a>
80
+
81
+ 1. setup test environment
82
+
83
+ <pre><code>
84
+ $ rails test
85
+ $ cd test
86
+ $ script/generate scaffold post name:string
87
+ $ script/generate scaffold comment name:string post_id:integer
88
+ $ rake db:migrate
89
+ </code></pre>
90
+
91
+ 2. change <code>app/model/post.rb</code> and <code>app/model/comment.rb</code>
92
+
93
+ <pre><code>
94
+ class Post < ActiveRecord::Base
95
+ has_many :comments
96
+ end
97
+
98
+ class Comment < ActiveRecord::Base
99
+ belongs_to :post
100
+ end
101
+ </code></pre>
102
+
103
+ 3. go to script/console and execute
104
+
105
+ <pre><code>
106
+ post1 = Post.create(:name => 'first')
107
+ post2 = Post.create(:name => 'second')
108
+ post1.comments.create(:name => 'first')
109
+ post1.comments.create(:name => 'second')
110
+ post2.comments.create(:name => 'third')
111
+ post2.comments.create(:name => 'fourth')
112
+ </code></pre>
113
+
114
+ 4. change the <code>app/views/posts/index.html.erb</code> to produce a N+1 query
115
+
116
+ <pre><code>
117
+ <% @posts.each do |post| %>
118
+ <tr>
119
+ <td><%=h post.name %></td>
120
+ <td><%= post.comments.collect(&:name) %></td>
121
+ <td><%= link_to 'Show', post %></td>
122
+ <td><%= link_to 'Edit', edit_post_path(post) %></td>
123
+ <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
124
+ </tr>
125
+ <% end %>
126
+ </code></pre>
127
+
128
+ 5. add bullet plugin
129
+
130
+ <pre><code>
131
+ $ script/plugin install git://github.com/flyerhzm/bullet.git
132
+ </code></pre>
133
+
134
+ 6. enable the bullet plugin in development, add a line to <code>config/environments/development.rb</code>
135
+
136
+ <pre><code>
137
+ Bullet.enable = true
138
+ </code></pre>
139
+
140
+ 7. start server
141
+
142
+ <pre><code>
143
+ $ script/server
144
+ </code></pre>
145
+
146
+ 8. input http://localhost:3000/posts in browser, then you will see a popup alert box says
147
+
148
+ <pre><code>
149
+ The request has unused preload associations as follows:
150
+ None
151
+ The request has N+1 queries as follows:
152
+ model: Post => associations: [comment]
153
+ </code></pre>
154
+
155
+ which means there is a N+1 query from post object to comments associations.
156
+
157
+ In the meanwhile, there's a log appended into <code>log/bullet.log</code> file
158
+
159
+ <pre><code>
160
+ 2009-08-20 09:12:19[INFO] N+1 Query: PATH_INFO: /posts; model: Post => assocations: [comments]
161
+ Add your finder: :include => [:comments]
162
+ 2009-08-20 09:12:19[INFO] N+1 Query: method call stack:
163
+ /Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb'
164
+ /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
165
+ /Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb'
166
+ /Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'
167
+ </code></pre>
168
+
169
+ The generated SQLs are
170
+
171
+ <pre><code>
172
+ Post Load (1.0ms) SELECT * FROM "posts"
173
+ Comment Load (0.4ms) SELECT * FROM "comments" WHERE ("comments".post_id = 1)
174
+ Comment Load (0.3ms) SELECT * FROM "comments" WHERE ("comments".post_id = 2)
175
+ </code></pre>
176
+
177
+
178
+ 9. fix the N+1 query, change <code>app/controllers/posts_controller.rb</code> file
179
+
180
+ <pre><code>
181
+ def index
182
+ @posts = Post.find(:all, :include => :comments)
183
+
184
+ respond_to do |format|
185
+ format.html # index.html.erb
186
+ format.xml { render :xml => @posts }
187
+ end
188
+ end
189
+ </code></pre>
190
+
191
+ 10. refresh http://localhost:3000/posts page, no alert box and no log appended.
192
+
193
+ The generated SQLs are
194
+
195
+ <pre><code>
196
+ Post Load (0.5ms) SELECT * FROM "posts"
197
+ Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE ("comments".post_id IN (1,2))
198
+ </code></pre>
199
+
200
+ a N+1 query fixed. Cool!
201
+
202
+ 11. now simulate unused eager loading. Change <code>app/controllers/posts_controller.rb</code> and <code>app/views/posts/index.html.erb</code>
203
+
204
+ <pre><code>
205
+ def index
206
+ @posts = Post.find(:all, :include => :comments)
207
+
208
+ respond_to do |format|
209
+ format.html # index.html.erb
210
+ format.xml { render :xml => @posts }
211
+ end
212
+ end
213
+ </code></pre>
214
+
215
+ <pre><code>
216
+ <% @posts.each do |post| %>
217
+ <tr>
218
+ <td><%=h post.name %></td>
219
+ <td><%= link_to 'Show', post %></td>
220
+ <td><%= link_to 'Edit', edit_post_path(post) %></td>
221
+ <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
222
+ </tr>
223
+ <% end %>
224
+ </code></pre>
225
+
226
+ 12. refresh http://localhost:3000/posts page, then you will see a popup alert box says
227
+
228
+ <pre><code>
229
+ The request has unused preload associations as follows:
230
+ model: Post => associations: [comment]
231
+ The request has N+1 queries as follows:
232
+ None
233
+ </code></pre>
234
+
235
+ In the meanwhile, there's a log appended into <code>log/bullet.log</code> file
236
+
237
+ <pre><code>
238
+ 2009-08-25 21:13:22[INFO] Unused preload associations: PATH_INFO: /posts; model: Post => associations: [comments]·
239
+ Remove from your finder: :include => [:comments]
240
+ </code></pre>
241
+
242
+
243
+ Copyright (c) 2009 Richard Huang (flyerhzm@gmail.com), released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+ require 'rake/rdoctask'
4
+ require 'jeweler'
5
+
6
+ desc 'Default: run unit tests.'
7
+ task :default => :spec
8
+
9
+ desc 'Generate documentation for the sitemap plugin.'
10
+ Rake::RDocTask.new(:rdoc) do |rdoc|
11
+ rdoc.rdoc_dir = 'rdoc'
12
+ rdoc.title = 'Bullet'
13
+ rdoc.options << '--line-numbers' << '--inline-source'
14
+ rdoc.rdoc_files.include('README')
15
+ rdoc.rdoc_files.include('lib/**/*.rb')
16
+ end
17
+
18
+ desc "Run all specs in spec directory"
19
+ Spec::Rake::SpecTask.new(:spec) do |t|
20
+ t.spec_files = FileList['spec/spec_helper.rb', 'spec/**/*_spec.rb']
21
+ end
22
+
23
+ Jeweler::Tasks.new do |gemspec|
24
+ gemspec.name = "bullet"
25
+ gemspec.summary = "A plugin to kill N+1 queries and unused eager loading"
26
+ gemspec.description = "This plugin is aimed to give you some performance suggestion about ActiveRecord usage, what should use but not use, such as eager loading, counter cache and so on, what should not use but use, such as unused eager loading. Now it provides you the suggestion of eager loading and unused eager loading. The others are todo, next may be couter cache."
27
+ gemspec.email = "flyerhzm@gmail.com"
28
+ gemspec.homepage = "http://www.huangzhimin.com/projects/4-bullet"
29
+ gemspec.authors = ["Richard Huang"]
30
+ gemspec.files.exclude '.gitignore'
31
+ gemspec.files.exclude 'log/'
32
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/bullet.gemspec ADDED
@@ -0,0 +1,54 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{bullet}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Richard Huang"]
12
+ s.date = %q{2009-08-26}
13
+ s.description = %q{This plugin is aimed to give you some performance suggestion about ActiveRecord usage, what should use but not use, such as eager loading, counter cache and so on, what should not use but use, such as unused eager loading. Now it provides you the suggestion of eager loading and unused eager loading. The others are todo, next may be couter cache.}
14
+ s.email = %q{flyerhzm@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "README.textile"
17
+ ]
18
+ s.files = [
19
+ "MIT-LICENSE",
20
+ "README.textile",
21
+ "Rakefile",
22
+ "VERSION",
23
+ "bullet.gemspec",
24
+ "lib/bullet.rb",
25
+ "lib/bullet/association.rb",
26
+ "lib/bullet/logger.rb",
27
+ "lib/bulletware.rb",
28
+ "lib/hack/active_record.rb",
29
+ "rails/init.rb",
30
+ "spec/bullet_association_spec.rb",
31
+ "spec/spec.opts",
32
+ "spec/spec_helper.rb",
33
+ "tasks/bullet_tasks.rake"
34
+ ]
35
+ s.homepage = %q{http://www.huangzhimin.com/projects/4-bullet}
36
+ s.rdoc_options = ["--charset=UTF-8"]
37
+ s.require_paths = ["lib"]
38
+ s.rubygems_version = %q{1.3.5}
39
+ s.summary = %q{A plugin to kill N+1 queries and unused eager loading}
40
+ s.test_files = [
41
+ "spec/spec_helper.rb",
42
+ "spec/bullet_association_spec.rb"
43
+ ]
44
+
45
+ if s.respond_to? :specification_version then
46
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
47
+ s.specification_version = 3
48
+
49
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
50
+ else
51
+ end
52
+ else
53
+ end
54
+ end
@@ -0,0 +1,150 @@
1
+ module Bullet
2
+ class Association
3
+ class <<self
4
+ @@logger_file = File.open(Bullet::BulletLogger::LOG_FILE, 'a+')
5
+ @@logger = Bullet::BulletLogger.new(@@logger_file)
6
+ @@alert = true
7
+
8
+ def start_request
9
+ # puts "start request"
10
+ @@object_associations ||= {}
11
+ @@call_object_associations ||= {}
12
+ @@unpreload_associations ||= {}
13
+ @@unused_preload_associations ||= {}
14
+ @@callers ||= []
15
+ @@possible_objects ||= {}
16
+ @@impossible_objects ||= {}
17
+ end
18
+
19
+ def end_request
20
+ # puts "end request"
21
+ @@object_associations = nil
22
+ @@unpreload_associations = nil
23
+ @@unused_preload_associations = nil
24
+ @@callers = nil
25
+ @@possible_objects = nil
26
+ @@impossible_objects = nil
27
+ @@call_object_associations = nil
28
+ end
29
+
30
+ def alert=(alert)
31
+ @@alert = alert
32
+ end
33
+
34
+ def logger=(logger)
35
+ if logger == false
36
+ @@logger = nil
37
+ end
38
+ end
39
+
40
+ def check_unused_preload_associations
41
+ @@object_associations.each do |object, association|
42
+ call_association = @@call_object_associations[object] || []
43
+ association.uniq! unless association.flatten!.nil?
44
+ call_association.uniq! unless call_association.flatten!.nil?
45
+ klazz = object.class
46
+ unless (association - call_association).empty?
47
+ @@unused_preload_associations[klazz] ||= []
48
+ @@unused_preload_associations[klazz] << (association - call_association)
49
+ @@unused_preload_associations[klazz].flatten!.uniq!
50
+ end
51
+ end
52
+ end
53
+
54
+ def has_bad_assocations?
55
+ check_unused_preload_associations
56
+ has_unpreload_associations? or has_unused_preload_associations?
57
+ end
58
+
59
+ def has_unused_preload_associations?
60
+ !@@unused_preload_associations.empty?
61
+ end
62
+
63
+ def has_unpreload_associations?
64
+ !@@unpreload_associations.empty?
65
+ end
66
+
67
+ def bad_associations_alert
68
+ str = ''
69
+ if @@alert
70
+ str = "<script type='text/javascript'>"
71
+ str << "alert('The request has unused preload assocations as follows:\\n"
72
+ str << (has_unused_preload_associations? ? @@unused_preload_associations.to_a.collect{|klazz, associations| "model: #{klazz} => associations: [#{associations.join(', ')}]"}.join('\\n') : "None")
73
+ str << "\\nThe request has N+1 queries as follows:\\n"
74
+ str << (has_unpreload_associations? ? @@unpreload_associations.to_a.collect{|klazz, associations| "model: #{klazz} => associations: [#{associations.join(', ')}]"}.join('\\n') : "None")
75
+ str << "')"
76
+ str << "</script>\n"
77
+ end
78
+ str
79
+ end
80
+
81
+ def log_bad_associations(path)
82
+ if @@logger
83
+ @@unused_preload_associations.each do |klazz, associations|
84
+ @@logger.info "Unused preload associations: PATH_INFO: #{path}; model: #{klazz} => associations: [#{associations.join(', ')}] \n Remove from your finder: :include => #{associations.map{|a| a.to_sym}.inspect}"
85
+ end
86
+ @@unpreload_associations.each do |klazz, associations|
87
+ @@logger.info "N+1 Query: PATH_INFO: #{path}; model: #{klazz} => associations: [#{associations.join(', ')}] \n Add to your finder: :include => #{associations.map{|a| a.to_sym}.inspect}"
88
+ end
89
+ @@callers.each do |c|
90
+ @@logger.info "N+1 Query: method call stack: \n" + c.join("\n")
91
+ end
92
+ @@logger_file.flush
93
+ end
94
+ end
95
+
96
+ def has_klazz_association(klazz)
97
+ !@@klazz_associations[klazz].nil? and @@klazz_associations.keys.include?(klazz)
98
+ end
99
+
100
+ def define_association(klazz, associations)
101
+ # puts "define association, #{klazz} => #{associations}"
102
+ @@klazz_associations ||= {}
103
+ @@klazz_associations[klazz] ||= []
104
+ @@klazz_associations[klazz] << associations
105
+ end
106
+
107
+ def add_possible_objects(objects)
108
+ # puts "add possible object, #{objects}"
109
+ klazz= objects.first.class
110
+ @@possible_objects[klazz] ||= []
111
+ @@possible_objects[klazz] << objects
112
+ @@possible_objects[klazz].flatten!.uniq!
113
+ end
114
+
115
+ def add_impossible_object(object)
116
+ # puts "add impossible object, #{object}"
117
+ klazz = object.class
118
+ @@impossible_objects[klazz] ||= []
119
+ @@impossible_objects[klazz] << object
120
+ end
121
+
122
+ def add_association(object, associations)
123
+ # puts "add association, #{object} => #{associations}"
124
+ @@object_associations[object] ||= []
125
+ @@object_associations[object] << associations
126
+ end
127
+
128
+ def call_association(object, associations)
129
+ # puts "call association, #{object} => #{associations}"
130
+ klazz = object.class
131
+ @@possible_objects ||= {}
132
+ @@impossible_objects ||= {}
133
+ if (!@@possible_objects[klazz].nil? and @@possible_objects[klazz].include?(object)) and (@@impossible_objects[klazz].nil? or !@@impossible_objects[klazz].include?(object)) and (@@object_associations[object].nil? or !@@object_associations[object].include?(associations))
134
+ @@unpreload_associations[klazz] ||= []
135
+ @@unpreload_associations[klazz] << associations
136
+ @@unpreload_associations[klazz].uniq!
137
+ @@call_object_associations[object] ||= []
138
+ @@call_object_associations[object] << associations
139
+ caller_in_project
140
+ end
141
+ end
142
+
143
+ VENDOR_ROOT = File.join(RAILS_ROOT, 'vendor')
144
+ def caller_in_project
145
+ @@callers << caller.select {|c| c =~ /#{RAILS_ROOT}/}.reject {|c| c =~ /#{VENDOR_ROOT}/}
146
+ @@callers.uniq!
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,9 @@
1
+ module Bullet
2
+ class BulletLogger < Logger
3
+ LOG_FILE = ::RAILS_ROOT + '/log/bullet.log'
4
+
5
+ def format_message(severity, timestamp, progname, msg)
6
+ "#{timestamp.to_formatted_s(:db)}[#{severity}] #{msg}\n"
7
+ end
8
+ end
9
+ end
data/lib/bullet.rb ADDED
@@ -0,0 +1,14 @@
1
+ module Bullet
2
+ class <<self
3
+ def enable=(enable)
4
+ @@enable = enable
5
+ end
6
+
7
+ def enable?
8
+ class_variables.include?('@@enable') and @@enable == true
9
+ end
10
+ end
11
+
12
+ autoload :Association, 'bullet/association'
13
+ autoload :BulletLogger, 'bullet/logger'
14
+ end
data/lib/bulletware.rb ADDED
@@ -0,0 +1,25 @@
1
+ class Bulletware
2
+ def initialize(app)
3
+ @app = app
4
+ end
5
+
6
+ def call(env)
7
+ return @app.call(env) unless Bullet.enable?
8
+
9
+ Bullet::Association.start_request
10
+ status, headers, response = @app.call(env)
11
+ return [status, headers, response] if response.empty?
12
+
13
+ if Bullet::Association.has_bad_assocations?
14
+ if !headers['Content-Type'].nil? and headers['Content-Type'].include? 'text/html'
15
+ response_body = response.body.insert(-17, Bullet::Association.bad_associations_alert)
16
+ headers['Content-Length'] = response_body.length.to_s
17
+ end
18
+
19
+ Bullet::Association.log_bad_associations(env['PATH_INFO'])
20
+ end
21
+ response_body ||= response.body
22
+ Bullet::Association.end_request
23
+ [status, headers, response_body]
24
+ end
25
+ end
@@ -0,0 +1,95 @@
1
+ if Bullet.enable?
2
+ ActiveRecord::ActiveRecordError # An ActiveRecord bug
3
+
4
+ module ActiveRecord
5
+ class Base
6
+ class <<self
7
+ # if select a collection of objects, then these objects have possible to cause N+1 query
8
+ # if select only one object, then the only one object has impossible to cause N+1 query
9
+ alias_method :origin_find_every, :find_every
10
+
11
+ def find_every(options)
12
+ records = origin_find_every(options)
13
+
14
+ if records
15
+ if records.size > 1
16
+ Bullet::Association.add_possible_objects(records)
17
+ elsif records.size == 1
18
+ Bullet::Association.add_impossible_object(records.first)
19
+ end
20
+ end
21
+
22
+ records
23
+ end
24
+ end
25
+ end
26
+
27
+ module AssociationPreload
28
+ module ClassMethods
29
+ # add include for one to many associations query
30
+ alias_method :origin_preload_associations, :preload_associations
31
+
32
+ def preload_associations(records, associations, preload_options={})
33
+ records = [records].flatten.compact.uniq
34
+ return if records.empty?
35
+ records.each do |record|
36
+ Bullet::Association.add_association(record, associations)
37
+ end
38
+ origin_preload_associations(records, associations, preload_options={})
39
+ end
40
+ end
41
+ end
42
+
43
+ module Associations
44
+ module ClassMethods
45
+ # define one to many associations
46
+ alias_method :origin_collection_reader_method, :collection_reader_method
47
+
48
+ def collection_reader_method(reflection, association_proxy_class)
49
+ Bullet::Association.define_association(self, reflection.name)
50
+ origin_collection_reader_method(reflection, association_proxy_class)
51
+ end
52
+ end
53
+
54
+ class AssociationCollection
55
+ # call one to many associations
56
+ alias_method :origin_load_target, :load_target
57
+
58
+ def load_target
59
+ Bullet::Association.call_association(@owner, @reflection.name)
60
+ origin_load_target
61
+ end
62
+ end
63
+
64
+ class HasOneAssociation
65
+ # call has_one association
66
+ alias_method :origin_find_target, :find_target
67
+
68
+ def find_target
69
+ Bullet::Association.call_association(@owner, @reflection.name)
70
+ origin_find_target
71
+ end
72
+ end
73
+
74
+ class BelongsToAssociation
75
+ # call belongs_to association
76
+ alias_method :origin_find_target, :find_target
77
+
78
+ def find_target
79
+ Bullet::Association.call_association(@owner, @reflection.name)
80
+ origin_find_target
81
+ end
82
+ end
83
+
84
+ class BelongsToPolymorphicAssociation
85
+ # call belongs_to association
86
+ alias_method :origin_find_target, :find_target
87
+
88
+ def find_target
89
+ Bullet::Association.call_association(@owner, @reflection.name)
90
+ origin_find_target
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,4 @@
1
+ # Include hook code here
2
+ require 'bullet'
3
+ require 'hack/active_record'
4
+ ActionController::Dispatcher.middleware.use Bulletware