store_base_sti_class_for_3_0 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source :rubygems
2
+
3
+ gem "activerecord", "~> 3.0.3"
4
+ gem "mysql2"
5
+
6
+ group :development do
7
+ gem "bundler", "~> 1.0.0"
8
+ gem "jeweler", "~> 1.5.2"
9
+ gem "rcov", ">= 0"
10
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,35 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.0.3)
5
+ activesupport (= 3.0.3)
6
+ builder (~> 2.1.2)
7
+ i18n (~> 0.4)
8
+ activerecord (3.0.3)
9
+ activemodel (= 3.0.3)
10
+ activesupport (= 3.0.3)
11
+ arel (~> 2.0.2)
12
+ tzinfo (~> 0.3.23)
13
+ activesupport (3.0.3)
14
+ arel (2.0.8)
15
+ builder (2.1.2)
16
+ git (1.2.5)
17
+ i18n (0.5.0)
18
+ jeweler (1.5.2)
19
+ bundler (~> 1.0.0)
20
+ git (>= 1.2.5)
21
+ rake
22
+ mysql2 (0.2.6)
23
+ rake (0.8.7)
24
+ rcov (0.9.9)
25
+ tzinfo (0.3.24)
26
+
27
+ PLATFORMS
28
+ ruby
29
+
30
+ DEPENDENCIES
31
+ activerecord (~> 3.0.3)
32
+ bundler (~> 1.0.0)
33
+ jeweler (~> 1.5.2)
34
+ mysql2
35
+ rcov
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Paul Kmiec
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,65 @@
1
+ == Description
2
+
3
+ Given the following class definitions,
4
+
5
+ class Address
6
+ belongs_to :addressable, :polymorphic => true
7
+ end
8
+
9
+ class Person
10
+ has_many :addresses, :as => addressable
11
+ end
12
+
13
+ class Vendor < Person
14
+ end
15
+
16
+ and given the following code,
17
+
18
+ vendor = Vendor.create(...)
19
+ address = vendor.addresses.create(...)
20
+
21
+ p vendor
22
+ p address
23
+
24
+ will output,
25
+
26
+ #<Vendor id: 1, type: "Vendor" ...>
27
+ #<Address id: 1, addressable_id: 1, addressable_type: 'Person' ...>
28
+
29
+ Notice that addressable_type column is Person even though the actual class is Vendor.
30
+
31
+ Normally, this isn't a problem, however it can have negative performance characteristic in certain circumstances. The most obvious one is that
32
+ a join with persons or an extra query is required to find out the actual type of addressable.
33
+
34
+ This gem add ActiveRecord::Base.store_base_sti_class configuration option. It defaults to true for backwards compatibility. Setting it false will alter ActiveRecord's behavior to store the actual class in polymorphic _type columns when STI is used.
35
+
36
+ In the example above, if the ActiveRecord::Base.store_base_sti_class is false, the output will be,
37
+
38
+ #<Vendor id: 1, type: "Vendor" ...>
39
+ #<Address id: 1, addressable_id: 1, addressable_type: 'Vendor' ...>
40
+
41
+ == Usage
42
+
43
+ Add the following line to your Gemfile,
44
+
45
+ gem 'store_base_sti_class_for_3_0'
46
+
47
+ then bundle install. Once you have the gem installed, add the following to one of the initializers (or make a new one) in config/initializers,
48
+
49
+ ActiveRecord::Base.store_base_sti_class = false
50
+
51
+ When changing this behavior you will have write a migration to update all of your existing _type columns accordingly. You may also need to change your application if it explicitly relies on the _type columns.
52
+
53
+ == Notes
54
+
55
+ The gem has been extracted out of https://github.com/pkmiec/rails/tree/store_base_sti_class_for_3_0_4 patch. It allows the functionality to be used in applications that include Rails as a gem.
56
+
57
+ I've tested this gem with Rails 3.0.3 and Rails 3.0.4. Please let me know if it works with earlier version of Rails 3.0.
58
+
59
+ This gem will not work with Rails 3.1 as much of its ActiveRecord internals have been replaced with Arel. Similar but different changes need to be applied to Rails 3.1. Those changes will come.
60
+
61
+ == Copyright
62
+
63
+ Copyright (c) 2011 Paul Kmiec. See LICENSE.txt for
64
+ further details.
65
+
data/Rakefile ADDED
@@ -0,0 +1,76 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "store_base_sti_class_for_3_0"
16
+ gem.homepage = "http://github.com/pkmiec/store_base_sti_class_for_3_0"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{
19
+ Modifies ActiveRecord 3.0.x with the ability to store the actual class (instead of the base class) in polymorhic _type columns when using STI
20
+ }
21
+ gem.description = %Q{
22
+ ActiveRecord has always stored the base class in polymorphic _type columns when using STI. This can have non-trivial
23
+ performance implications in certain cases. This gem adds 'store_base_sti_class' configuration options which controls
24
+ whether ActiveRecord will store the base class or the actual class. Default to true for backwards compatibility.
25
+ }
26
+ gem.email = "paul.kmiec@appfolio.com"
27
+ gem.authors = ["Paul Kmiec"]
28
+
29
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
30
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
31
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
32
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
33
+ end
34
+ Jeweler::RubygemsDotOrgTasks.new
35
+
36
+ require 'rake/testtask'
37
+ Rake::TestTask.new(:test) do |test|
38
+ test.libs << 'lib' << 'test'
39
+ test.pattern = 'test/**/test_*.rb'
40
+ test.verbose = true
41
+ end
42
+
43
+ require 'rcov/rcovtask'
44
+ Rcov::RcovTask.new do |test|
45
+ test.libs << 'test'
46
+ test.pattern = 'test/**/test_*.rb'
47
+ test.verbose = true
48
+ end
49
+
50
+ task :default => :test
51
+
52
+ require 'rake/rdoctask'
53
+ Rake::RDocTask.new do |rdoc|
54
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
55
+
56
+ rdoc.rdoc_dir = 'rdoc'
57
+ rdoc.title = "store_base_sti_class_for_3_0 #{version}"
58
+ rdoc.rdoc_files.include('README*')
59
+ rdoc.rdoc_files.include('lib/**/*.rb')
60
+ end
61
+
62
+ namespace :mysql do
63
+ desc 'Build the MySQL test databases'
64
+ task :build_databases do
65
+ %x( echo "create DATABASE storebasestiname_unittest DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci " | mysql --user=root)
66
+ end
67
+
68
+ desc 'Drop the MySQL test databases'
69
+ task :drop_databases do
70
+ %x( mysqladmin --user=root -f drop storebasestiname_unittest )
71
+ end
72
+
73
+ desc 'Rebuild the MySQL test databases'
74
+ task :rebuild_databases => [:drop_databases, :build_databases]
75
+ end
76
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,478 @@
1
+ require 'active_record'
2
+ require 'active_record/associations'
3
+ require 'active_record/reflection'
4
+ require 'active_record/association_preload'
5
+ require 'active_record/associations/has_many_association'
6
+ require 'active_record/associations/has_one_association'
7
+ require 'active_record/associations/through_association_scope'
8
+ require 'active_record/associations/association_proxy'
9
+ require 'active_record/associations/belongs_to_polymorphic_association'
10
+
11
+ class ActiveRecord::Base
12
+
13
+ # Determine whether to store the base class or the actual class in polymorhic type columns when using STI
14
+ superclass_delegating_accessor :store_base_sti_class
15
+ self.store_base_sti_class = true
16
+
17
+ class << self
18
+
19
+ def polymorphic_sti_name
20
+ store_base_sti_class ? base_class.sti_name : sti_name
21
+ end
22
+
23
+ def in_or_equals_sti_names
24
+ if store_base_sti_class
25
+ "= #{quote_value(base_class.sti_name)}"
26
+ else
27
+ names = sti_names.map { |name| quote_value(name) }
28
+ names.length > 1 ? "IN (#{names.join(',')})" : "= #{names}"
29
+ end
30
+ end
31
+
32
+ def sti_names
33
+ ([self] + descendants).map { |model| model.sti_name }
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+
40
+ module ActiveRecord
41
+ module Reflection
42
+ class AssociationReflection < MacroReflection #:nodoc:
43
+
44
+ def active_record_primary_key
45
+ @active_record_primary_key ||= options[:primary_key] || active_record.primary_key
46
+ end
47
+
48
+ end
49
+ end
50
+ end
51
+
52
+ module ActiveRecord
53
+ module AssociationPreload
54
+ module ClassMethods
55
+
56
+ private
57
+
58
+ def preload_through_records(records, reflection, through_association)
59
+ # p 'preload_through_records'
60
+ through_reflection = reflections[through_association]
61
+ through_primary_key = through_reflection.primary_key_name
62
+
63
+ through_records = []
64
+ if reflection.options[:source_type]
65
+ interface = reflection.source_reflection.options[:foreign_type]
66
+ source_type = reflection.options[:source_type].to_s.constantize
67
+ preload_options = { :conditions => "#{connection.quote_column_name interface} #{source_type.in_or_equals_sti_names}" }
68
+
69
+ records.compact!
70
+ records.first.class.preload_associations(records, through_association, preload_options)
71
+
72
+ # Dont cache the association - we would only be caching a subset
73
+ records.each do |record|
74
+ proxy = record.send(through_association)
75
+
76
+ if proxy.respond_to?(:target)
77
+ through_records.concat Array.wrap(proxy.target)
78
+ proxy.reset
79
+ else # this is a has_one :through reflection
80
+ through_records << proxy if proxy
81
+ end
82
+ end
83
+ else
84
+ options = {}
85
+ options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions] || reflection.options[:order]
86
+ options[:order] = reflection.options[:order]
87
+ options[:conditions] = reflection.options[:conditions]
88
+ records.first.class.preload_associations(records, through_association, options)
89
+
90
+ records.each do |record|
91
+ through_records.concat Array.wrap(record.send(through_association))
92
+ end
93
+ end
94
+ through_records
95
+ end
96
+
97
+ def find_associated_records(ids, reflection, preload_options)
98
+ # p 'find_associated_records'
99
+ options = reflection.options
100
+ table_name = reflection.klass.quoted_table_name
101
+
102
+ if interface = reflection.options[:as]
103
+ conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} #{self.in_or_equals_sti_names}"
104
+ else
105
+ foreign_key = reflection.primary_key_name
106
+ conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
107
+ end
108
+
109
+ # p 'append_conditions'
110
+ conditions << append_conditions(reflection, preload_options)
111
+
112
+ find_options = {
113
+ :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"),
114
+ :include => preload_options[:include] || options[:include],
115
+ :joins => options[:joins],
116
+ :group => preload_options[:group] || options[:group],
117
+ :order => preload_options[:order] || options[:order]
118
+ }
119
+
120
+ # p 'associated_records'
121
+ associated_records(ids) do |some_ids|
122
+ reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => [conditions, some_ids])).to_a
123
+ end
124
+ end
125
+
126
+ # Some databases impose a limit on the number of ids in a list (in Oracle its 1000)
127
+ # Make several smaller queries if necessary or make one query if the adapter supports it
128
+ def associated_records(ids)
129
+ # rails 3.0.4 introduced ids_in_list_limit
130
+ max_ids_in_a_list = (connection.respond_to?(:ids_in_list_limit) ? connection.ids_in_list_limit : nil) || ids.size
131
+ records = []
132
+ ids.each_slice(max_ids_in_a_list) do |some_ids|
133
+ records += yield(some_ids)
134
+ end
135
+ records
136
+ end
137
+
138
+ end
139
+ end
140
+ end
141
+
142
+ module ActiveRecord
143
+ module Associations
144
+ class HasManyAssociation < AssociationCollection
145
+
146
+ protected
147
+
148
+ def construct_sql
149
+ # p 'construct_sql :has_many'
150
+
151
+ case
152
+ when @reflection.options[:finder_sql]
153
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
154
+
155
+ when @reflection.options[:as]
156
+ @finder_sql =
157
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
158
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type #{@owner.class.in_or_equals_sti_names}"
159
+ @finder_sql << " AND (#{conditions})" if conditions
160
+
161
+ else
162
+ @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
163
+ @finder_sql << " AND (#{conditions})" if conditions
164
+ end
165
+
166
+ construct_counter_sql
167
+ end
168
+
169
+ end
170
+ end
171
+ end
172
+
173
+ module ActiveRecord
174
+ module Associations
175
+ class HasOneAssociation < AssociationProxy
176
+
177
+ protected
178
+
179
+ def construct_sql
180
+ # p 'construct_sql :has_one'
181
+
182
+ case
183
+ when @reflection.options[:as]
184
+ @finder_sql =
185
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
186
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type #{@owner.class.in_or_equals_sti_names}"
187
+ else
188
+ @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
189
+ end
190
+ @finder_sql << " AND (#{conditions})" if conditions
191
+ end
192
+
193
+ end
194
+ end
195
+ end
196
+
197
+ module ActiveRecord
198
+ module Associations
199
+ module ThroughAssociationScope
200
+
201
+ protected
202
+
203
+ def construct_quoted_owner_attributes(reflection)
204
+ # p 'construct_quoted_owner_attributes'
205
+
206
+ if as = reflection.options[:as]
207
+ {
208
+ "#{as}_id" => owner_quoted_id,
209
+ "#{as}_type" => @owner.class.quote_value(@owner.class.polymorphic_sti_name)
210
+ }
211
+ elsif reflection.macro == :belongs_to
212
+ { reflection.klass.primary_key => @owner.class.quote_value(@owner[reflection.primary_key_name]) }
213
+ else
214
+ { reflection.primary_key_name => owner_quoted_id }
215
+ end
216
+ end
217
+
218
+ def construct_joins(custom_joins = nil)
219
+ # p 'construct_joins'
220
+
221
+ polymorphic_join = nil
222
+ if @reflection.source_reflection.macro == :belongs_to
223
+ reflection_primary_key = @reflection.klass.primary_key
224
+ source_primary_key = @reflection.source_reflection.primary_key_name
225
+ if @reflection.options[:source_type]
226
+ source_type = @reflection.options[:source_type].to_s.constantize
227
+ polymorphic_join = "AND %s.%s #{source_type.in_or_equals_sti_names}" % [
228
+ @reflection.through_reflection.quoted_table_name,
229
+ "#{@reflection.source_reflection.options[:foreign_type]}"
230
+ ]
231
+ end
232
+ else
233
+ reflection_primary_key = @reflection.source_reflection.primary_key_name
234
+ source_primary_key = @reflection.through_reflection.klass.primary_key
235
+ if @reflection.source_reflection.options[:as]
236
+ polymorphic_join = "AND %s.%s #{@reflection.through_reflection.klass.in_or_equals_sti_names}" % [
237
+ @reflection.quoted_table_name,
238
+ "#{@reflection.source_reflection.options[:as]}_type"
239
+ ]
240
+ end
241
+ end
242
+
243
+ "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
244
+ @reflection.through_reflection.quoted_table_name,
245
+ @reflection.quoted_table_name, reflection_primary_key,
246
+ @reflection.through_reflection.quoted_table_name, source_primary_key,
247
+ polymorphic_join
248
+ ]
249
+ end
250
+
251
+ def construct_owner_attributes(reflection)
252
+ # p 'construct_owner_attributes'
253
+
254
+ if as = reflection.options[:as]
255
+ { "#{as}_id" => @owner.id,
256
+ "#{as}_type" => @owner.class.polymorphic_sti_name }
257
+ else
258
+ { reflection.primary_key_name => @owner.id }
259
+ end
260
+ end
261
+
262
+ end
263
+ end
264
+ end
265
+
266
+ module ActiveRecord
267
+ module Associations
268
+ class AssociationProxy
269
+
270
+ protected
271
+
272
+ def set_belongs_to_association_for(record)
273
+ # p 'set_belongs_to_association_for'
274
+
275
+ if @reflection.options[:as]
276
+ record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
277
+ record["#{@reflection.options[:as]}_type"] = @owner.class.polymorphic_sti_name
278
+ else
279
+ unless @owner.new_record?
280
+ primary_key = @reflection.options[:primary_key] || :id
281
+ record[@reflection.primary_key_name] = @owner.send(primary_key)
282
+ end
283
+ end
284
+ end
285
+
286
+ end
287
+ end
288
+ end
289
+
290
+ module ActiveRecord
291
+ module Associations
292
+ class BelongsToPolymorphicAssociation < AssociationProxy
293
+
294
+ def replace(record)
295
+ # p 'replace'
296
+
297
+ if record.nil?
298
+ @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
299
+ else
300
+ @target = (AssociationProxy === record ? record.target : record)
301
+
302
+ @owner[@reflection.primary_key_name] = record_id(record)
303
+ @owner[@reflection.options[:foreign_type]] = record.class.polymorphic_sti_name
304
+
305
+ @updated = true
306
+ end
307
+
308
+ set_inverse_instance(record, @owner)
309
+ loaded
310
+ record
311
+ end
312
+
313
+ end
314
+ end
315
+ end
316
+
317
+ module ActiveRecord
318
+ module Associations
319
+ module ThroughAssociationScope
320
+
321
+ protected
322
+
323
+ def construct_quoted_owner_attributes(reflection)
324
+ # p 'construct_quoted_owner_attributes'
325
+ if as = reflection.options[:as]
326
+ {
327
+ "#{as}_id" => owner_quoted_id,
328
+ "#{as}_type" => @owner.class.quote_value(@owner.class.polymorphic_sti_name)
329
+ }
330
+ elsif reflection.macro == :belongs_to
331
+ { reflection.klass.primary_key => @owner.class.quote_value(@owner[reflection.primary_key_name]) }
332
+ else
333
+ { reflection.primary_key_name => owner_quoted_id }
334
+ end
335
+ end
336
+
337
+ def construct_owner_attributes(reflection)
338
+ # p 'construct_owner_attributes'
339
+ if as = reflection.options[:as]
340
+ { "#{as}_id" => @owner.id,
341
+ "#{as}_type" => @owner.class.polymorphic_sti_name }
342
+ else
343
+ { reflection.primary_key_name => @owner.id }
344
+ end
345
+ end
346
+
347
+ def construct_join_attributes(associate)
348
+ # p 'construct_join_attributes'
349
+ # TODO: revisit this to allow it for deletion, supposing dependent option is supported
350
+ raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
351
+
352
+ join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
353
+
354
+ if @reflection.options[:source_type]
355
+ join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.polymorphic_sti_name)
356
+ end
357
+
358
+ if @reflection.through_reflection.options[:conditions].is_a?(Hash)
359
+ join_attributes.merge!(@reflection.through_reflection.options[:conditions])
360
+ end
361
+
362
+ join_attributes
363
+ end
364
+
365
+ end
366
+ end
367
+ end
368
+
369
+ module ActiveRecord
370
+ module Associations
371
+ module ClassMethods
372
+ class JoinDependency # :nodoc:
373
+ class JoinAssociation < JoinBase # :nodoc:
374
+
375
+ def association_join
376
+ # p 'association_join'
377
+
378
+ return @join if @join
379
+
380
+ aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name,
381
+ :engine => arel_engine,
382
+ :columns => klass.columns)
383
+
384
+ parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name,
385
+ :engine => arel_engine,
386
+ :columns => parent.active_record.columns)
387
+
388
+ @join = case reflection.macro
389
+ when :has_and_belongs_to_many
390
+ join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine)
391
+ fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
392
+ klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key
393
+
394
+ [
395
+ join_table[fk].eq(parent_table[reflection.active_record.primary_key]),
396
+ aliased_table[klass.primary_key].eq(join_table[klass_fk])
397
+ ]
398
+ when :has_many, :has_one
399
+ if reflection.options[:through]
400
+ join_table = Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine)
401
+ jt_as_extra = jt_source_extra = jt_sti_extra = nil
402
+ first_key = second_key = nil
403
+
404
+ if through_reflection.macro == :belongs_to
405
+ jt_primary_key = through_reflection.primary_key_name
406
+ jt_foreign_key = through_reflection.association_primary_key
407
+ else
408
+ jt_primary_key = through_reflection.active_record_primary_key
409
+ jt_foreign_key = through_reflection.primary_key_name
410
+
411
+ if through_reflection.options[:as] # has_many :through against a polymorphic join
412
+ jt_as_extra = join_table[through_reflection.options[:as].to_s + '_type'].in(parent.active_record.sti_names)
413
+ end
414
+ end
415
+
416
+ case source_reflection.macro
417
+ when :has_many
418
+ if source_reflection.options[:as]
419
+ first_key = "#{source_reflection.options[:as]}_id"
420
+ second_key = options[:foreign_key] || primary_key
421
+ else
422
+ first_key = through_reflection.klass.base_class.to_s.foreign_key
423
+ second_key = options[:foreign_key] || primary_key
424
+ end
425
+
426
+ unless through_reflection.klass.descends_from_active_record?
427
+ # there is no test for this condition
428
+ jt_sti_extra = join_table[through_reflection.active_record.inheritance_column].eq(through_reflection.klass.sti_name)
429
+ end
430
+ when :belongs_to
431
+ first_key = primary_key
432
+ if reflection.options[:source_type]
433
+ source_type = reflection.options[:source_type].to_s.constantize
434
+ second_key = source_reflection.association_foreign_key
435
+ jt_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].in(source_type.sti_names)
436
+ else
437
+ second_key = source_reflection.primary_key_name
438
+ end
439
+ end
440
+
441
+ [
442
+ [parent_table[jt_primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra].reject{|x| x.blank? },
443
+ aliased_table[first_key].eq(join_table[second_key])
444
+ ]
445
+ elsif reflection.options[:as]
446
+ id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key])
447
+ type_rel = aliased_table["#{reflection.options[:as]}_type"].in(parent.active_record.sti_names)
448
+ [id_rel, type_rel]
449
+ else
450
+ foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
451
+ [aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key])]
452
+ end
453
+ when :belongs_to
454
+ [aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name])]
455
+ end
456
+
457
+ unless klass.descends_from_active_record?
458
+ sti_column = aliased_table[klass.inheritance_column]
459
+ sti_condition = sti_column.eq(klass.sti_name)
460
+ klass.descendants.each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) }
461
+
462
+ @join << sti_condition
463
+ end
464
+
465
+ [through_reflection, reflection].each do |ref|
466
+ if ref && ref.options[:conditions]
467
+ @join << interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name))
468
+ end
469
+ end
470
+
471
+ @join
472
+ end
473
+
474
+ end
475
+ end
476
+ end
477
+ end
478
+ end
@@ -0,0 +1,77 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
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{store_base_sti_class_for_3_0}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Paul Kmiec"]
12
+ s.date = %q{2011-02-15}
13
+ s.description = %q{
14
+ ActiveRecord has always stored the base class in polymorphic _type columns when using STI. This can have non-trivial
15
+ performance implications in certain cases. This gem adds 'store_base_sti_class' configuration options which controls
16
+ whether ActiveRecord will store the base class or the actual class. Default to true for backwards compatibility.
17
+ }
18
+ s.email = %q{paul.kmiec@appfolio.com}
19
+ s.extra_rdoc_files = [
20
+ "LICENSE.txt",
21
+ "README.rdoc"
22
+ ]
23
+ s.files = [
24
+ ".document",
25
+ "Gemfile",
26
+ "Gemfile.lock",
27
+ "LICENSE.txt",
28
+ "README.rdoc",
29
+ "Rakefile",
30
+ "VERSION",
31
+ "lib/store_base_sti_class_for_3_0.rb",
32
+ "store_base_sti_class_for_3_0.gemspec",
33
+ "test/connection.rb",
34
+ "test/helper.rb",
35
+ "test/models.rb",
36
+ "test/schema.rb",
37
+ "test/test_store_base_sti_class_for_3_0.rb"
38
+ ]
39
+ s.homepage = %q{http://github.com/pkmiec/store_base_sti_class_for_3_0}
40
+ s.licenses = ["MIT"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = %q{1.3.7}
43
+ s.summary = %q{Modifies ActiveRecord 3.0.x with the ability to store the actual class (instead of the base class) in polymorhic _type columns when using STI}
44
+ s.test_files = [
45
+ "test/connection.rb",
46
+ "test/helper.rb",
47
+ "test/models.rb",
48
+ "test/schema.rb",
49
+ "test/test_store_base_sti_class_for_3_0.rb"
50
+ ]
51
+
52
+ if s.respond_to? :specification_version then
53
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
54
+ s.specification_version = 3
55
+
56
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
57
+ s.add_runtime_dependency(%q<activerecord>, ["~> 3.0.3"])
58
+ s.add_runtime_dependency(%q<mysql2>, [">= 0"])
59
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
60
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
61
+ s.add_development_dependency(%q<rcov>, [">= 0"])
62
+ else
63
+ s.add_dependency(%q<activerecord>, ["~> 3.0.3"])
64
+ s.add_dependency(%q<mysql2>, [">= 0"])
65
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
66
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
67
+ s.add_dependency(%q<rcov>, [">= 0"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<activerecord>, ["~> 3.0.3"])
71
+ s.add_dependency(%q<mysql2>, [">= 0"])
72
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
73
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
74
+ s.add_dependency(%q<rcov>, [">= 0"])
75
+ end
76
+ end
77
+
@@ -0,0 +1,16 @@
1
+ require 'logger'
2
+
3
+ ActiveRecord::Base.logger = Logger.new("debug.log")
4
+
5
+ # GRANT ALL PRIVILEGES ON storebasestiname_unittest.* to 'root'@'localhost';
6
+
7
+ ActiveRecord::Base.configurations = {
8
+ 'unittest' => {
9
+ :adapter => 'mysql2',
10
+ :username => 'root',
11
+ :encoding => 'utf8',
12
+ :database => 'storebasestiname_unittest',
13
+ }
14
+ }
15
+
16
+ ActiveRecord::Base.establish_connection 'unittest'
data/test/helper.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+
12
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ require 'store_base_sti_class_for_3_0'
15
+
16
+ require 'connection'
17
+
18
+ # silence verbose schema loading
19
+ original_stdout = $stdout
20
+ $stdout = StringIO.new
21
+ begin
22
+ require "schema.rb"
23
+ ensure
24
+ $stdout = original_stdout
25
+ end
26
+
27
+ require 'models'
data/test/models.rb ADDED
@@ -0,0 +1,38 @@
1
+ class Author < ActiveRecord::Base
2
+ has_many :posts
3
+
4
+ has_many :tagging, :through => :posts # through polymorphic has_one
5
+ has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many
6
+ has_many :tags, :through => :posts # through has_many :through
7
+ end
8
+
9
+ class Post < ActiveRecord::Base
10
+ belongs_to :author
11
+
12
+ has_one :tagging, :as => :taggable
13
+ has_many :taggings, :as => :taggable
14
+ has_many :tags, :through => :taggings
15
+ end
16
+
17
+ class SpecialPost < Post
18
+ end
19
+
20
+ class Tagging < ActiveRecord::Base
21
+ belongs_to :tag, :include => :tagging
22
+ belongs_to :polytag, :polymorphic => true
23
+ belongs_to :taggable, :polymorphic => true, :counter_cache => true
24
+ end
25
+
26
+ class Tag < ActiveRecord::Base
27
+ has_one :tagging
28
+
29
+ has_many :taggings
30
+ has_many :taggables, :through => :taggings
31
+ has_many :tagged_posts, :through => :taggings, :source => :taggable, :source_type => 'Post'
32
+
33
+ has_many :polytaggings, :as => :polytag, :class_name => 'Tagging'
34
+ has_many :polytagged_posts, :through => :polytaggings, :source => :taggable, :source_type => 'Post'
35
+ end
36
+
37
+ class SpecialTag < Tag
38
+ end
data/test/schema.rb ADDED
@@ -0,0 +1,35 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ # Please keep these create table statements in alphabetical order
4
+ # unless the ordering matters. In which case, define them below
5
+
6
+ create_table :authors, :force => true do |t|
7
+ t.string :name, :null => false
8
+ end
9
+
10
+ create_table :posts, :force => true do |t|
11
+ t.string :type
12
+
13
+ t.integer :author_id
14
+ t.string :title, :null => false
15
+ t.text :body, :null => false
16
+ t.integer :taggings_count, :default => 0
17
+ end
18
+
19
+ create_table :taggings, :force => true do |t|
20
+ t.integer :tag_id
21
+
22
+ t.integer :polytag_id
23
+ t.string :polytag_type
24
+
25
+ t.string :taggable_type
26
+ t.integer :taggable_id
27
+ end
28
+
29
+ create_table :tags, :force => true do |t|
30
+ t.string :type
31
+ t.string :name
32
+ t.integer :taggings_count, :default => 0
33
+ end
34
+
35
+ end
@@ -0,0 +1,133 @@
1
+ require 'helper'
2
+ require 'active_record/test_case'
3
+
4
+ class TestStoreBaseStiNameFor30 < ActiveRecord::TestCase
5
+
6
+ def setup
7
+ @old_store_base_sti_class = ActiveRecord::Base.store_base_sti_class
8
+ ActiveRecord::Base.store_base_sti_class = false
9
+
10
+ @thinking_post = SpecialPost.create(:title => 'Thinking')
11
+ @misc_tag = Tag.create(:name => 'Misc')
12
+ end
13
+
14
+ def teardown
15
+ ActiveRecord::Base.store_base_sti_class = @old_store_base_sti_class
16
+ end
17
+
18
+ def test_polymorphic_belongs_to_assignment_with_inheritance
19
+ # should update when assigning a saved record
20
+ tagging = Tagging.new
21
+ post = SpecialPost.create(:title => 'Budget Forecasts Bigger 2011 Deficit')
22
+ tagging.taggable = post
23
+ assert_equal post.id, tagging.taggable_id
24
+ assert_equal "SpecialPost", tagging.taggable_type
25
+
26
+ # should update when assigning a new record
27
+ tagging = Tagging.new
28
+ post = SpecialPost.new(:title => 'Budget Forecasts Bigger 2011 Deficit')
29
+ tagging.taggable = post
30
+ assert_nil tagging.taggable_id
31
+ assert_equal "SpecialPost", tagging.taggable_type
32
+ end
33
+
34
+ def test_polymorphic_has_many_create_model_with_inheritance
35
+ post = SpecialPost.new(:title => 'Budget Forecasts Bigger 2011 Deficit')
36
+
37
+ tagging = @misc_tag.taggings.create(:taggable => post)
38
+ assert_equal "SpecialPost", tagging.taggable_type
39
+
40
+ post.reload
41
+ assert_equal [tagging], post.taggings
42
+ end
43
+
44
+ def test_polymorphic_has_one_create_model_with_inheritance
45
+ post = SpecialPost.new(:title => 'Budget Forecasts Bigger 2011 Deficit')
46
+
47
+ tagging = @misc_tag.create_tagging(:taggable => post)
48
+ assert_equal "SpecialPost", tagging.taggable_type
49
+
50
+ post.reload
51
+ assert_equal tagging, post.tagging
52
+ end
53
+
54
+ def test_include_polymorphic_has_one
55
+ post = SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
56
+ tagging = post.create_tagging(:tag => @misc_tag)
57
+
58
+ post = Post.find(post.id, :include => :tagging)
59
+ assert_equal tagging, assert_no_queries { post.tagging }
60
+ end
61
+
62
+ def test_include_polymorphic_has_many
63
+ tag = SpecialTag.create!(:name => 'Special')
64
+ tag.polytagged_posts << SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
65
+ tag.polytagged_posts << @thinking_post
66
+
67
+ tag = Tag.find(tag.id, :include => :polytaggings)
68
+ assert_equal 2, assert_no_queries { tag.polytaggings.length }
69
+ end
70
+
71
+ def test_include_polymorphic_has_many_through
72
+ tag = SpecialTag.create!(:name => 'Special')
73
+ tag.polytagged_posts << SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
74
+ tag.polytagged_posts << @thinking_post
75
+
76
+ tag = Tag.find(tag.id, :include => :polytagged_posts)
77
+ assert_equal 2, assert_no_queries { tag.polytagged_posts.length }
78
+ end
79
+
80
+ def test_join_polymorhic_has_many
81
+ tag = SpecialTag.create!(:name => 'Special')
82
+ tag.polytagged_posts << SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
83
+ tag.polytagged_posts << @thinking_post
84
+
85
+ assert Tag.find_by_id(tag.id, :joins => :polytaggings, :conditions => [ 'taggings.id = ?', tag.polytaggings.first.id ])
86
+ end
87
+
88
+ def test_join_polymorhic_has_many_through
89
+ tag = SpecialTag.create!(:name => 'Special')
90
+ tag.polytagged_posts << SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
91
+ tag.polytagged_posts << @thinking_post
92
+
93
+ assert Tag.find_by_id(tag.id, :joins => :polytagged_posts, :conditions => [ 'posts.id = ?', tag.polytaggings.first.taggable_id ])
94
+ end
95
+
96
+ def test_has_many_through_polymorphic_has_one
97
+ author = Author.create!(:name => 'Bob')
98
+ post = Post.create!(:title => 'Budget Forecasts Bigger 2011 Deficit', :author => author)
99
+ special_post = SpecialPost.create!(:title => 'IBM Watson''s Jeopardy play', :author => author)
100
+ special_tag = SpecialTag.create!(:name => 'SpecialGeneral')
101
+
102
+ taggings = [ post.taggings.create(:tag => special_tag), special_post.taggings.create(:tag => special_tag) ]
103
+ assert_equal taggings.sort_by(&:id), author.tagging.sort_by(&:id)
104
+ end
105
+
106
+ def test_has_many_polymorphic_with_source_type
107
+ tag = SpecialTag.create!(:name => 'Special')
108
+ tag.polytagged_posts << SpecialPost.create!(:title => 'Budget Forecasts Bigger 2011 Deficit')
109
+ tag.polytagged_posts << @thinking_post
110
+
111
+ tag = Tag.find(tag.id)
112
+ assert_equal 2, tag.polytagged_posts.length
113
+ end
114
+
115
+ def test_polymorphic_has_many_through_with_double_sti_on_join_model
116
+ tag = SpecialTag.create!(:name => 'Special')
117
+ post = @thinking_post
118
+
119
+ tag.polytagged_posts << post
120
+ tag.reload
121
+
122
+ assert_equal 1, tag.polytaggings.length
123
+
124
+ tagging = tag.polytaggings.first
125
+
126
+ assert_equal 'SpecialTag', tagging.polytag_type
127
+ assert_equal 'SpecialPost', tagging.taggable_type
128
+
129
+ assert_equal tag, tagging.polytag
130
+ assert_equal post, tagging.taggable
131
+ end
132
+
133
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: store_base_sti_class_for_3_0
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Paul Kmiec
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-15 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ prerelease: false
23
+ type: :runtime
24
+ name: activerecord
25
+ version_requirements: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ hash: 1
31
+ segments:
32
+ - 3
33
+ - 0
34
+ - 3
35
+ version: 3.0.3
36
+ requirement: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ prerelease: false
39
+ type: :runtime
40
+ name: mysql2
41
+ version_requirements: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 3
47
+ segments:
48
+ - 0
49
+ version: "0"
50
+ requirement: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ prerelease: false
53
+ type: :development
54
+ name: bundler
55
+ version_requirements: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 23
61
+ segments:
62
+ - 1
63
+ - 0
64
+ - 0
65
+ version: 1.0.0
66
+ requirement: *id003
67
+ - !ruby/object:Gem::Dependency
68
+ prerelease: false
69
+ type: :development
70
+ name: jeweler
71
+ version_requirements: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ hash: 7
77
+ segments:
78
+ - 1
79
+ - 5
80
+ - 2
81
+ version: 1.5.2
82
+ requirement: *id004
83
+ - !ruby/object:Gem::Dependency
84
+ prerelease: false
85
+ type: :development
86
+ name: rcov
87
+ version_requirements: &id005 !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ hash: 3
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ requirement: *id005
97
+ description: "\n ActiveRecord has always stored the base class in polymorphic _type columns when using STI. This can have non-trivial\n performance implications in certain cases. This gem adds 'store_base_sti_class' configuration options which controls\n whether ActiveRecord will store the base class or the actual class. Default to true for backwards compatibility.\n "
98
+ email: paul.kmiec@appfolio.com
99
+ executables: []
100
+
101
+ extensions: []
102
+
103
+ extra_rdoc_files:
104
+ - LICENSE.txt
105
+ - README.rdoc
106
+ files:
107
+ - .document
108
+ - Gemfile
109
+ - Gemfile.lock
110
+ - LICENSE.txt
111
+ - README.rdoc
112
+ - Rakefile
113
+ - VERSION
114
+ - lib/store_base_sti_class_for_3_0.rb
115
+ - store_base_sti_class_for_3_0.gemspec
116
+ - test/connection.rb
117
+ - test/helper.rb
118
+ - test/models.rb
119
+ - test/schema.rb
120
+ - test/test_store_base_sti_class_for_3_0.rb
121
+ has_rdoc: true
122
+ homepage: http://github.com/pkmiec/store_base_sti_class_for_3_0
123
+ licenses:
124
+ - MIT
125
+ post_install_message:
126
+ rdoc_options: []
127
+
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ hash: 3
136
+ segments:
137
+ - 0
138
+ version: "0"
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ none: false
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ hash: 3
145
+ segments:
146
+ - 0
147
+ version: "0"
148
+ requirements: []
149
+
150
+ rubyforge_project:
151
+ rubygems_version: 1.3.7
152
+ signing_key:
153
+ specification_version: 3
154
+ summary: Modifies ActiveRecord 3.0.x with the ability to store the actual class (instead of the base class) in polymorhic _type columns when using STI
155
+ test_files:
156
+ - test/connection.rb
157
+ - test/helper.rb
158
+ - test/models.rb
159
+ - test/schema.rb
160
+ - test/test_store_base_sti_class_for_3_0.rb