activerecord-precount 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -0
  3. data/README.md +3 -3
  4. data/lib/active_record/precount.rb +27 -1
  5. data/lib/active_record/precount/base_extension.rb +1 -9
  6. data/lib/active_record/precount/collection_proxy_extension.rb +6 -6
  7. data/lib/active_record/precount/count_loader_builder.rb +66 -0
  8. data/lib/active_record/precount/has_many_extension.rb +22 -60
  9. data/lib/active_record/precount/join_dependency_extension.rb +1 -1
  10. data/lib/active_record/precount/preloader_extension.rb +30 -35
  11. data/lib/active_record/precount/reflection_checker.rb +15 -0
  12. data/lib/active_record/precount/reflection_extension.rb +24 -4
  13. data/lib/active_record/precount/relation_extension.rb +5 -16
  14. data/lib/active_record/precount/version.rb +1 -1
  15. data/sample/Gemfile +1 -1
  16. data/sample/app/models/blog.rb +3 -0
  17. data/sample/app/models/comment.rb +3 -0
  18. data/sample/app/models/page.rb +3 -0
  19. data/sample/config/database.yml +1 -1
  20. data/sample/db/migrate/20160603161811_create_blogs.rb +8 -0
  21. data/sample/db/migrate/20160603161819_create_pages.rb +8 -0
  22. data/sample/db/migrate/20160603161833_create_comments.rb +10 -0
  23. data/sample/db/schema.rb +18 -1
  24. data/test/cases/associations/eager_count_test.rb +13 -4
  25. data/test/cases/associations/precount_test.rb +13 -4
  26. data/test/cases/helper.rb +2 -0
  27. data/test/cases/test_case.rb +6 -0
  28. data/test/models/favorite.rb +1 -0
  29. data/test/models/notification.rb +3 -0
  30. data/test/models/tweet.rb +1 -0
  31. data/test/models/user.rb +4 -0
  32. data/test/schema/schema.rb +11 -0
  33. metadata +13 -3
  34. data/lib/active_record/precount/association_reflection_extension.rb +0 -45
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d9a9eb42c1ba767640277c85e4f3ac5253e4527b
4
- data.tar.gz: 94b98083c83b85e45e1349c88d8560311d3c2d79
3
+ metadata.gz: b261414eec5a1fd723fb2fcea8cbe708c48415ca
4
+ data.tar.gz: 1d522f1cb7b9a7f0ac94d479feed434805d3077f
5
5
  SHA512:
6
- metadata.gz: c743dde55020595f377651df070394db415ea62f6ef12614d5eaca8b0bb4a5c2b6861d426bf5d87122e652511868b1c9c4d166bb26f70aa077a28e3bb7c7de82
7
- data.tar.gz: fc1020127a928f74bf6f99846f542d469e5664a4d10a6341d6316c9759d2d912d43b49435ac65939ea22f197ac3d42c63c5bf2b7bb9267276f59a0a673a141c1
6
+ metadata.gz: 9ddc79eeb3385aa2be6a1973c5e1bb11dba1fe123e3e9e6064fb3c16445922ff8273d8268a0ca37a8801b31fa9edd3cb5f70c091f5f3d23efa8fa2d4fde7915e
7
+ data.tar.gz: 4340c6a29f8040a154d1f6d6c5a401064efc60bfab6ec654c808da6a2e58fc804c834507d8c2d35229034ead2dc72340e85218eaa31fd053f91d7a9a3d59c99a
@@ -0,0 +1,87 @@
1
+ # Change Log
2
+
3
+ ## [v0.6.1](https://github.com/k0kubun/activerecord-precount/tree/v0.6.1) (2016-06-02)
4
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.6.0...v0.6.1)
5
+
6
+ - Support polymorphic association for `precount` [\#17](https://github.com/k0kubun/activerecord-precount/pull/17)
7
+
8
+ ## [v0.6.0](https://github.com/k0kubun/activerecord-precount/tree/v0.6.0) (2016-06-02)
9
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.5.1...v0.6.0)
10
+
11
+ - Support Rails 5.0 [\#14](https://github.com/k0kubun/activerecord-precount/pull/14) ([k0kubun](https://github.com/k0kubun))
12
+ - Drop Rails 4.1 support
13
+
14
+ ## [v0.5.1](https://github.com/k0kubun/activerecord-precount/tree/v0.5.1) (2015-09-09)
15
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.5.0...v0.5.1)
16
+
17
+ - Support an association with a scope [\#10](https://github.com/k0kubun/activerecord-precount/pull/10) ([tkawa](https://github.com/tkawa))
18
+
19
+ ## [v0.5.0](https://github.com/k0kubun/activerecord-precount/tree/v0.5.0) (2015-02-01)
20
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.4.3...v0.5.0)
21
+
22
+ - Add `eager_count` method for eager loading by JOIN
23
+
24
+ ## [v0.4.3](https://github.com/k0kubun/activerecord-precount/tree/v0.4.3) (2015-01-31)
25
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.4.2...v0.4.3)
26
+
27
+ ## [v0.4.2](https://github.com/k0kubun/activerecord-precount/tree/v0.4.2) (2015-01-31)
28
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.4.1...v0.4.2)
29
+
30
+ ## [v0.4.1](https://github.com/k0kubun/activerecord-precount/tree/v0.4.1) (2015-01-31)
31
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.4.0...v0.4.1)
32
+
33
+ ## [v0.4.0](https://github.com/k0kubun/activerecord-precount/tree/v0.4.0) (2015-01-31)
34
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.3.3...v0.4.0)
35
+
36
+ ## [v0.3.3](https://github.com/k0kubun/activerecord-precount/tree/v0.3.3) (2015-01-31)
37
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.3.2...v0.3.3)
38
+
39
+ ## [v0.3.2](https://github.com/k0kubun/activerecord-precount/tree/v0.3.2) (2015-01-30)
40
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.3.1...v0.3.2)
41
+
42
+ ## [v0.3.1](https://github.com/k0kubun/activerecord-precount/tree/v0.3.1) (2015-01-08)
43
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.3.0...v0.3.1)
44
+
45
+ ## [v0.3.0](https://github.com/k0kubun/activerecord-precount/tree/v0.3.0) (2015-01-08)
46
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.2.2...v0.3.0)
47
+
48
+ **Closed issues:**
49
+
50
+ - Rails 4.2.0 [\#8](https://github.com/k0kubun/activerecord-precount/issues/8)
51
+
52
+ ## [v0.2.2](https://github.com/k0kubun/activerecord-precount/tree/v0.2.2) (2014-11-23)
53
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.2.1...v0.2.2)
54
+
55
+ ## [v0.2.1](https://github.com/k0kubun/activerecord-precount/tree/v0.2.1) (2014-11-23)
56
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.2.0...v0.2.1)
57
+
58
+ **Merged pull requests:**
59
+
60
+ - Add test cases [\#7](https://github.com/k0kubun/activerecord-precount/pull/7) ([k0kubun](https://github.com/k0kubun))
61
+
62
+ ## [v0.2.0](https://github.com/k0kubun/activerecord-precount/tree/v0.2.0) (2014-11-22)
63
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.1.0...v0.2.0)
64
+
65
+ **Merged pull requests:**
66
+
67
+ - Change API [\#6](https://github.com/k0kubun/activerecord-precount/pull/6) ([k0kubun](https://github.com/k0kubun))
68
+
69
+ ## [v0.1.0](https://github.com/k0kubun/activerecord-precount/tree/v0.1.0) (2014-11-20)
70
+ [Full Changelog](https://github.com/k0kubun/activerecord-precount/compare/v0.0.5...v0.1.0)
71
+
72
+ **Merged pull requests:**
73
+
74
+ - Support :class\_name option on has\_count [\#5](https://github.com/k0kubun/activerecord-precount/pull/5) ([r7kamura](https://github.com/r7kamura))
75
+
76
+ ## [v0.0.5](https://github.com/k0kubun/activerecord-precount/tree/v0.0.5) (2014-11-13)
77
+ **Closed issues:**
78
+
79
+ - Select limited columns [\#3](https://github.com/k0kubun/activerecord-precount/issues/3)
80
+
81
+ **Merged pull requests:**
82
+
83
+ - Support `has\_count :xxx, foreign\_key: :yyy` [\#4](https://github.com/k0kubun/activerecord-precount/pull/4) ([r7kamura](https://github.com/r7kamura))
84
+
85
+
86
+
87
+ \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
data/README.md CHANGED
@@ -32,7 +32,7 @@ Like `preload`, it loads counts by multiple queries
32
32
 
33
33
  ```rb
34
34
  Tweet.all.precount(:favorites).each do |tweet|
35
- p tweet.favorites.count
35
+ p tweet.favorites_count
36
36
  end
37
37
  # SELECT `tweets`.* FROM `tweets`
38
38
  # SELECT COUNT(`favorites`.`tweet_id`), `favorites`.`tweet_id` FROM `favorites` WHERE `favorites`.`tweet_id` IN (1, 2, 3, 4, 5) GROUP BY `favorites`.`tweet_id`
@@ -44,7 +44,7 @@ Like `eager_load`, `eager_count` method allows you to load counts by one JOIN qu
44
44
 
45
45
  ```rb
46
46
  Tweet.all.eager_count(:favorites).each do |tweet|
47
- p tweet.favorites.count
47
+ p tweet.favorites_count
48
48
  end
49
49
  # SELECT `tweets`.`id` AS t0_r0, `tweets`.`tweet_id` AS t0_r1, `tweets`.`user_id` AS t0_r2, `tweets`.`created_at` AS t0_r3, `tweets`.`updated_at` AS t0_r4, COUNT(`favorites`.`id`) AS t1_r0 FROM `tweets` LEFT OUTER JOIN `favorites` ON `favorites`.`tweet_id` = `tweets`.`id` GROUP BY tweets.id
50
50
  ```
@@ -114,7 +114,7 @@ With this condition, you can eagerly load nested association by preload.
114
114
  Hoge.preload(foo: :bars_count)
115
115
  ```
116
116
 
117
- ### Performance issue
117
+ ### `count` method is not recommended
118
118
 
119
119
  With activerecord-precount gem installed, `bars.count` fallbacks to `bars_count` if `bars_count` is defined.
120
120
  Though precounted `bars.count` is faster than not-precounted one, the fallback is currently much slower than just calling `bars_count`.
@@ -1,7 +1,33 @@
1
1
  require "active_support/lazy_load_hooks"
2
2
 
3
+ # How it works:
4
+ #
5
+ # 1. has_many :foo, count_loader: true
6
+ # * has_many_extension: Create and add a reflection for :count_loader in has_many method
7
+ # * reflection_extension: CountLoaderReflection is required in reflection creation
8
+ # * reflection_extension: Return CountLoaderReflection in Reflection.create
9
+ # * reflection_extension: CountLoader#load_target loads counts when not eager-loaded
10
+ # * preloader_extension: Return CountLoader preloader and preload in it
11
+ # * relation_extension: Apply GROUP in #apply_join_dependency for eager_load
12
+ # * join_dependency_extension: Use COUNT query in #aliases for eager_load and map it in #construct
13
+ # * collection_proxy_extension: Fallback to eager-loaded values when foo.count is called
14
+ #
15
+ # 2. precount(:foo)
16
+ # * relation_extension: Add #precount query method, which defines count_loader association and adds preload_values
17
+ # * base_extension: Delegate it for class method
18
+ # * reflection_extension: Return CountLoaderReflection in Reflection.create
19
+ # * preloader_extension: Return CountLoader preloader and preload in it
20
+ # * collection_proxy_extension: Fallback to eager-loaded values when foo.count is called
21
+ #
22
+ # 3. eager_count(:foo)
23
+ # * relation_extension: Add #eager_count query method, which defines count_loader association and adds eager_load_values
24
+ # * base_extension: Delegate it for class method
25
+ # * reflection_extension: Return CountLoaderReflection in Reflection.create
26
+ # * relation_extension: Apply GROUP in #apply_join_dependency for eager_load
27
+ # * join_dependency_extension: Use COUNT query in #aliases for eager_load and map it in #construct
28
+ # * collection_proxy_extension: Fallback to eager-loaded values when foo.count is called
29
+ #
3
30
  ActiveSupport.on_load(:active_record) do
4
- require "active_record/precount/association_reflection_extension"
5
31
  require "active_record/precount/base_extension"
6
32
  require "active_record/precount/collection_proxy_extension"
7
33
  require "active_record/precount/has_many_extension"
@@ -2,16 +2,8 @@ module ActiveRecord
2
2
  module Precount
3
3
  module BaseExtension
4
4
  delegate :precount, :eager_count, to: :all
5
-
6
- def has_reflection?(name)
7
- reflection_for(name).present?
8
- end
9
-
10
- def reflection_for(name)
11
- reflections[name.to_s]
12
- end
13
5
  end
14
6
  end
15
7
 
16
- Base.send(:extend, Precount::BaseExtension)
8
+ Base.extend(Precount::BaseExtension)
17
9
  end
@@ -1,16 +1,16 @@
1
+ require 'active_record/precount/reflection_checker'
2
+
1
3
  module ActiveRecord
2
4
  module Precount
3
5
  module CollectionProxyExtension
4
6
  def count(*args)
5
- return super(*args) if args.present?
7
+ return super if args.present?
6
8
 
7
9
  counter_name = :"#{@association.reflection.name}_count"
8
- owner = @association.owner
9
-
10
- if owner.class.has_reflection?(counter_name) && owner.association(counter_name).loaded?
11
- owner.association(counter_name).target
10
+ if ReflectionChecker.count_loaded?(@association.owner, counter_name)
11
+ @association.owner.association(counter_name).target
12
12
  else
13
- super(*args)
13
+ super
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,66 @@
1
+ require 'active_record/precount/reflection_checker'
2
+
3
+ module ActiveRecord
4
+ module Precount
5
+ class CountLoaderBuilder
6
+ def initialize(model)
7
+ @model = model
8
+ end
9
+
10
+ def build_from_has_many(name, scope, options)
11
+ name_with_count =
12
+ if options[:count_loader].is_a?(Symbol)
13
+ options[:count_loader]
14
+ else
15
+ :"#{name}_count"
16
+ end
17
+
18
+ add_reflection(name_with_count, scope, options)
19
+ end
20
+
21
+ def build_from_query_methods(*args)
22
+ args.each do |arg|
23
+ next if ReflectionChecker.has_reflection?(@model, counter_name = :"#{arg}_count")
24
+ unless ReflectionChecker.has_reflection?(@model, arg)
25
+ raise ArgumentError, "Association named '#{arg}' was not found on #{@model.name}."
26
+ end
27
+
28
+ original_reflection = @model.reflections[arg.to_s]
29
+ add_reflection(counter_name, original_reflection.scope, original_reflection.options)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def add_reflection(name, scope, options)
36
+ valid_options = options.slice(*Associations::Builder::CountLoader.valid_options)
37
+ reflection = Associations::Builder::CountLoader.build(@model, name, scope, valid_options)
38
+ Reflection.add_reflection(@model, name, reflection)
39
+ end
40
+ end
41
+ end
42
+
43
+ module Associations
44
+ module Builder
45
+ class CountLoader < SingularAssociation
46
+ def self.valid_options(*)
47
+ [:class, :class_name, :foreign_key]
48
+ end
49
+
50
+ if ActiveRecord.version.segments.first >= 5
51
+ def self.macro
52
+ :count_loader
53
+ end
54
+ else
55
+ def macro
56
+ :count_loader
57
+ end
58
+ end
59
+
60
+ def self.valid_dependent_options
61
+ []
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,78 +1,40 @@
1
- module ActiveRecord
2
- module Associations
3
- module Builder
4
- class CountLoader < SingularAssociation
5
- def self.valid_options(*)
6
- [:class, :class_name, :foreign_key]
7
- end
8
-
9
- if ActiveRecord.version.segments.first >= 5
10
- def self.macro
11
- :count_loader
12
- end
13
- else
14
- def macro
15
- :count_loader
16
- end
17
- end
18
-
19
- def self.valid_dependent_options
20
- []
21
- end
22
- end
23
- end
24
- end
1
+ require 'active_record/precount/count_loader_builder'
25
2
 
3
+ module ActiveRecord
26
4
  module Precount
27
5
  module Builder
28
- module Rails4HasManyExtension
29
- def valid_options
30
- super + [:count_loader]
31
- end
32
-
33
- def build(model)
34
- define_count_loader(model) if options[:count_loader]
35
- super
36
- end
37
-
38
- def define_count_loader(model)
39
- name_with_count = :"#{name}_count"
40
- name_with_count = options[:count_loader] if options[:count_loader].is_a?(Symbol)
41
-
42
- valid_options = options.slice(*Associations::Builder::CountLoader.valid_options)
43
- reflection = Associations::Builder::CountLoader.build(model, name_with_count, scope, valid_options)
44
- Reflection.add_reflection(model, name_with_count, reflection)
45
- end
46
- end
47
-
48
- module Rails5HasManyExtension
6
+ module HasManyExtension
49
7
  def valid_options(*)
50
8
  super + [:count_loader]
51
9
  end
52
10
 
53
- def build(model, name, scope, options, &block)
54
- if scope.is_a?(Hash)
55
- options = scope
56
- scope = nil
11
+ if ActiveRecord.version.segments.first >= 5
12
+ def build(model, name, scope, options, &block)
13
+ if scope.is_a?(Hash)
14
+ options = scope
15
+ scope = nil
16
+ end
17
+
18
+ if options[:count_loader]
19
+ CountLoaderBuilder.new(model).build_from_has_many(name, scope, options)
20
+ end
21
+ super
57
22
  end
58
-
59
- if options[:count_loader]
60
- name_with_count = :"#{name}_count"
61
- name_with_count = options[:count_loader] if options[:count_loader].is_a?(Symbol)
62
-
63
- valid_options = options.slice(*Associations::Builder::CountLoader.valid_options)
64
- reflection = Associations::Builder::CountLoader.build(model, name_with_count, scope, valid_options)
65
- Reflection.add_reflection(model, name_with_count, reflection)
23
+ else
24
+ def build(model)
25
+ if options[:count_loader]
26
+ CountLoaderBuilder.new(model).build_from_has_many(name, scope, options)
27
+ end
28
+ super
66
29
  end
67
- super
68
30
  end
69
31
  end
70
32
  end
71
33
  end
72
34
 
73
35
  if ActiveRecord.version.segments.first >= 5
74
- Associations::Builder::HasMany.send(:extend, Precount::Builder::Rails5HasManyExtension)
36
+ Associations::Builder::HasMany.extend(Precount::Builder::HasManyExtension)
75
37
  else
76
- Associations::Builder::HasMany.prepend(Precount::Builder::Rails4HasManyExtension)
38
+ Associations::Builder::HasMany.prepend(Precount::Builder::HasManyExtension)
77
39
  end
78
40
  end
@@ -42,7 +42,7 @@ module ActiveRecord
42
42
  return if normal_children.blank?
43
43
 
44
44
  normal_parent = Associations::JoinDependency::JoinBase.new(parent.base_klass, normal_children)
45
- super(ar_parent, normal_parent, row, rs, seen, model_cache, aliases)
45
+ super
46
46
  end
47
47
  end
48
48
  end
@@ -2,23 +2,42 @@ module ActiveRecord
2
2
  module Associations
3
3
  class Preloader
4
4
  class CountLoader < SingularAssociation
5
- if ActiveRecord.version.segments.first >= 5
6
- def association_key_name
7
- reflection.foreign_key
8
- end
5
+ def association_key_name
6
+ reflection.foreign_key
7
+ end
8
+
9
+ def owner_key_name
10
+ reflection.active_record_primary_key
11
+ end
12
+
13
+ private
9
14
 
10
- def owner_key_name
11
- reflection.active_record_primary_key
15
+ def key_conversion_required?
16
+ # Are you sure this is always false? But this method is required to map result for polymorphic association.
17
+ false
18
+ end
19
+
20
+ def preload(preloader)
21
+ associated_records_by_owner(preloader).each do |owner, associated_records|
22
+ owner.association(reflection.name).target = associated_records.first.to_i
12
23
  end
24
+ end
13
25
 
14
- private
26
+ def query_scope(ids)
27
+ key = model.reflections[reflection.name.to_s.sub(/_count\z/, '')].foreign_key
28
+ scope.where(key => ids).group(key).count(key)
29
+ end
15
30
 
16
- def preload(preloader)
17
- associated_records_by_owner(preloader).each do |owner, associated_records|
18
- owner.association(reflection.name).target = associated_records.first.to_i
31
+ def build_scope
32
+ super.tap do |scope|
33
+ has_many_reflection = model.reflections[reflection.name.to_s.sub(/_count\z/, '')]
34
+ if has_many_reflection.options[:as]
35
+ scope.where!(klass.table_name => { has_many_reflection.type => model.base_class.sti_name })
19
36
  end
20
37
  end
38
+ end
21
39
 
40
+ if ActiveRecord.version.segments.first >= 5
22
41
  def load_records
23
42
  return {} if owner_keys.empty?
24
43
 
@@ -29,27 +48,7 @@ module ActiveRecord
29
48
 
30
49
  Hash[@preloaded_records.first.map { |key, count| [key, [count]] }]
31
50
  end
32
-
33
- def query_scope(ids)
34
- scope.where(association_key.in(ids)).group(association_key_name).count(association_key_name)
35
- end
36
51
  else
37
- def association_key_name
38
- reflection.foreign_key
39
- end
40
-
41
- def owner_key_name
42
- reflection.active_record_primary_key
43
- end
44
-
45
- private
46
-
47
- def preload(preloader)
48
- associated_records_by_owner(preloader).each do |owner, associated_records|
49
- owner.association(reflection.name).target = associated_records.first.to_i
50
- end
51
- end
52
-
53
52
  def load_slices(slices)
54
53
  @preloaded_records = slices.flat_map { |slice|
55
54
  records_for(slice)
@@ -59,10 +58,6 @@ module ActiveRecord
59
58
  [count, key]
60
59
  }
61
60
  end
62
-
63
- def query_scope(ids)
64
- scope.where(association_key.in(ids)).group(association_key_name).count(association_key_name)
65
- end
66
61
  end
67
62
  end
68
63
  end
@@ -71,7 +66,7 @@ module ActiveRecord
71
66
  module Precount
72
67
  module PreloaderExtension
73
68
  def preloader_for(reflection, owners, rhs_klass)
74
- preloader = super(reflection, owners, rhs_klass)
69
+ preloader = super
75
70
  return preloader if preloader
76
71
 
77
72
  case reflection.macro
@@ -0,0 +1,15 @@
1
+ module ActiveRecord
2
+ module Precount
3
+ module ReflectionChecker
4
+ class << self
5
+ def has_reflection?(klass, name)
6
+ klass.reflections[name.to_s].present?
7
+ end
8
+
9
+ def count_loaded?(owner, name)
10
+ has_reflection?(owner.class, name) && owner.association(name).loaded?
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,11 +1,31 @@
1
1
  module ActiveRecord
2
+ module Associations
3
+ class CountLoader < SingularAssociation
4
+ # Not preloaded behaviour of count_loader association
5
+ # When this method is called, it will be N+1 query
6
+ def load_target
7
+ count_target = reflection.name.to_s.sub(/_count\z/, '').to_sym
8
+ @target = owner.association(count_target).count
9
+
10
+ loaded! unless loaded?
11
+ target
12
+ rescue ActiveRecord::RecordNotFound
13
+ reset
14
+ end
15
+ end
16
+ end
17
+
2
18
  module Reflection
3
19
  class CountLoaderReflection < AssociationReflection
4
- def initialize(name, scope, options, active_record)
5
- super(name, scope, options, active_record)
20
+ def macro; :count_loader; end
21
+
22
+ def association_class
23
+ ActiveRecord::Associations::CountLoader
6
24
  end
7
25
 
8
- def macro; :count_loader; end
26
+ def klass
27
+ @klass ||= active_record.send(:compute_type, options[:class_name] || name.to_s.sub(/_count\z/, '').singularize.classify)
28
+ end
9
29
  end
10
30
  end
11
31
 
@@ -23,7 +43,7 @@ module ActiveRecord
23
43
  when :count_loader
24
44
  Reflection::CountLoaderReflection.new(name, scope, options, ar)
25
45
  else
26
- super(macro, name, scope, options, ar)
46
+ super
27
47
  end
28
48
  end
29
49
  end
@@ -1,3 +1,5 @@
1
+ require 'active_record/precount/count_loader_builder'
2
+
1
3
  module ActiveRecord
2
4
  module Precount
3
5
  module RelationExtension
@@ -7,7 +9,7 @@ module ActiveRecord
7
9
  end
8
10
 
9
11
  def precount!(*args)
10
- define_count_loader!(*args)
12
+ CountLoaderBuilder.new(klass).build_from_query_methods(*args)
11
13
 
12
14
  self.preload_values += args.map { |arg| :"#{arg}_count" }
13
15
  self
@@ -19,7 +21,7 @@ module ActiveRecord
19
21
  end
20
22
 
21
23
  def eager_count!(*args)
22
- define_count_loader!(*args)
24
+ CountLoaderBuilder.new(klass).build_from_query_methods(*args)
23
25
 
24
26
  self.eager_load_values += args.map { |arg| :"#{arg}_count" }
25
27
  self
@@ -27,21 +29,8 @@ module ActiveRecord
27
29
 
28
30
  private
29
31
 
30
- def define_count_loader!(*args)
31
- args.each do |arg|
32
- raise ArgumentError, "Association named '#{arg}' was not found on #{klass.name}." unless has_reflection?(arg)
33
- next if has_reflection?(counter_name = :"#{arg}_count")
34
-
35
- original_reflection = reflection_for(arg)
36
- scope = original_reflection.scope
37
- options = original_reflection.options.slice(*Associations::Builder::CountLoader.valid_options)
38
- reflection = Associations::Builder::CountLoader.build(klass, counter_name, scope, options)
39
- Reflection.add_reflection(model, counter_name, reflection)
40
- end
41
- end
42
-
43
32
  def apply_join_dependency(relation, join_dependency)
44
- relation = super(relation, join_dependency)
33
+ relation = super
45
34
 
46
35
  # to count associated records in JOIN query, group scope is necessary
47
36
  join_dependency.reflections.each do |reflection|
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Precount
3
- VERSION = "0.6.0"
3
+ VERSION = "0.6.1"
4
4
  end
5
5
  end
@@ -1,6 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'rails', '4.2.0'
3
+ gem 'rails', '4.2.6'
4
4
  gem 'mysql2'
5
5
  gem 'sass-rails', '~> 4.0.3'
6
6
  gem 'uglifier', '>= 1.3.0'
@@ -0,0 +1,3 @@
1
+ class Blog < ActiveRecord::Base
2
+ has_many :comments, as: :commentable
3
+ end
@@ -0,0 +1,3 @@
1
+ class Comment < ActiveRecord::Base
2
+ belongs_to :commentable, polymorphic: true
3
+ end
@@ -0,0 +1,3 @@
1
+ class Page < ActiveRecord::Base
2
+ has_many :comments, as: :commentable
3
+ end
@@ -4,7 +4,7 @@ default: &default
4
4
  pool: 5
5
5
  username: root
6
6
  password:
7
- socket: /tmp/mysql.sock
7
+ socket: /var/run/mysqld/mysqld.sock
8
8
 
9
9
  development:
10
10
  <<: *default
@@ -0,0 +1,8 @@
1
+ class CreateBlogs < ActiveRecord::Migration
2
+ def change
3
+ create_table :blogs do |t|
4
+
5
+ t.timestamps null: false
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class CreatePages < ActiveRecord::Migration
2
+ def change
3
+ create_table :pages do |t|
4
+
5
+ t.timestamps null: false
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ class CreateComments < ActiveRecord::Migration
2
+ def change
3
+ create_table :comments do |t|
4
+ t.integer :commentable_id
5
+ t.string :commentable_type
6
+
7
+ t.timestamps null: false
8
+ end
9
+ end
10
+ end
@@ -11,7 +11,19 @@
11
11
  #
12
12
  # It's strongly recommended that you check this file into your version control system.
13
13
 
14
- ActiveRecord::Schema.define(version: 20141122002555) do
14
+ ActiveRecord::Schema.define(version: 20160603161833) do
15
+
16
+ create_table "blogs", force: :cascade do |t|
17
+ t.datetime "created_at", null: false
18
+ t.datetime "updated_at", null: false
19
+ end
20
+
21
+ create_table "comments", force: :cascade do |t|
22
+ t.integer "commentable_id", limit: 4
23
+ t.string "commentable_type", limit: 255
24
+ t.datetime "created_at", null: false
25
+ t.datetime "updated_at", null: false
26
+ end
15
27
 
16
28
  create_table "favorites", force: :cascade do |t|
17
29
  t.integer "tweet_id", limit: 4
@@ -22,6 +34,11 @@ ActiveRecord::Schema.define(version: 20141122002555) do
22
34
 
23
35
  add_index "favorites", ["tweet_id"], name: "index_favorites_on_tweet_id", using: :btree
24
36
 
37
+ create_table "pages", force: :cascade do |t|
38
+ t.datetime "created_at", null: false
39
+ t.datetime "updated_at", null: false
40
+ end
41
+
25
42
  create_table "tweets", force: :cascade do |t|
26
43
  t.integer "in_reply_to_tweet_id", limit: 4
27
44
  t.integer "user_id", limit: 4
@@ -5,13 +5,15 @@ class EagerCountTest < ActiveRecord::CountLoader::TestCase
5
5
  tweets_count.times do |i|
6
6
  tweet = Tweet.create
7
7
  i.times do |j|
8
- Favorite.create(tweet: tweet, user_id: j + 1)
8
+ favorite = Favorite.create(tweet: tweet, user_id: j + 1)
9
+ Notification.create(notifiable: favorite)
10
+ Notification.create(notifiable: tweet)
9
11
  end
10
12
  end
11
13
  end
12
14
 
13
15
  def teardown
14
- if Tweet.has_reflection?(:favs_count)
16
+ if has_reflection?(Tweet, :favs_count)
15
17
  Tweet.reflections.delete('favs_count')
16
18
  end
17
19
 
@@ -23,9 +25,9 @@ class EagerCountTest < ActiveRecord::CountLoader::TestCase
23
25
  end
24
26
 
25
27
  def test_eager_count_defines_count_loader
26
- assert_equal(false, Tweet.has_reflection?(:favs_count))
28
+ assert_equal(false, has_reflection?(Tweet, :favs_count))
27
29
  Tweet.eager_count(:favs).map(&:favs_count)
28
- assert_equal(true, Tweet.has_reflection?(:favs_count))
30
+ assert_equal(true, has_reflection?(Tweet, :favs_count))
29
31
  end
30
32
 
31
33
  def test_eager_count_has_many_with_count_loader_does_not_execute_n_1_queries
@@ -47,4 +49,11 @@ class EagerCountTest < ActiveRecord::CountLoader::TestCase
47
49
  assert_equal(expected, Tweet.order(id: :asc).eager_count(:my_favs).map { |t| t.my_favs.count })
48
50
  assert_equal(expected, Tweet.order(id: :asc).eager_count(:my_favs).map(&:my_favs_count))
49
51
  end
52
+
53
+ def test_polymorphic_eager_count
54
+ skip 'eager_count of polymorphic is not implemented yet'
55
+ expected = Tweet.all.map { |t| t.notifications.count }
56
+ assert_equal(expected, Tweet.eager_count(:notifications).map(&:notifications_count))
57
+ assert_equal(expected, Tweet.eager_count(:notifications).map { |t| t.notifications.count })
58
+ end
50
59
  end
@@ -5,13 +5,15 @@ class PrecountTest < ActiveRecord::CountLoader::TestCase
5
5
  tweets_count.times do |i|
6
6
  tweet = Tweet.create
7
7
  i.times do |j|
8
- Favorite.create(tweet: tweet, user_id: j + 1)
8
+ favorite = Favorite.create(tweet: tweet, user_id: j + 1)
9
+ Notification.create(notifiable: favorite)
10
+ Notification.create(notifiable: tweet)
9
11
  end
10
12
  end
11
13
  end
12
14
 
13
15
  def teardown
14
- if Tweet.has_reflection?(:favs_count)
16
+ if has_reflection?(Tweet, :favs_count)
15
17
  Tweet.reflections.delete('favs_count')
16
18
  end
17
19
 
@@ -23,9 +25,9 @@ class PrecountTest < ActiveRecord::CountLoader::TestCase
23
25
  end
24
26
 
25
27
  def test_precount_defines_count_loader
26
- assert_equal(false, Tweet.has_reflection?(:favs_count))
28
+ assert_equal(false, has_reflection?(Tweet, :favs_count))
27
29
  Tweet.precount(:favs).map(&:favs_count)
28
- assert_equal(true, Tweet.has_reflection?(:favs_count))
30
+ assert_equal(true, has_reflection?(Tweet, :favs_count))
29
31
  end
30
32
 
31
33
  def test_precount_has_many_with_count_loader_does_not_execute_n_1_queries
@@ -47,4 +49,11 @@ class PrecountTest < ActiveRecord::CountLoader::TestCase
47
49
  assert_equal(expected, Tweet.precount(:my_favs).map { |t| t.my_favs.count })
48
50
  assert_equal(expected, Tweet.precount(:my_favs).map(&:my_favs_count))
49
51
  end
52
+
53
+ def test_polymorphic_precount
54
+ expected = Tweet.all.map { |t| t.notifications.count }
55
+ assert_equal(expected, Tweet.precount(:notifications).map(&:notifications_count))
56
+ assert_equal(expected, Tweet.precount(:notifications).map { |t| t.notifications.count })
57
+ assert_equal(expected, Tweet.all.map(&:notifications_count))
58
+ end
50
59
  end
@@ -4,4 +4,6 @@ require 'support/autorun'
4
4
  require 'cases/test_case'
5
5
 
6
6
  require 'models/favorite'
7
+ require 'models/notification'
7
8
  require 'models/tweet'
9
+ require 'models/user'
@@ -1,5 +1,11 @@
1
+ require 'active_record/precount/reflection_checker'
2
+
1
3
  module ActiveRecord::CountLoader
2
4
  class TestCase < Minitest::Test
5
+ def has_reflection?(klass, name)
6
+ ActiveRecord::Precount::ReflectionChecker.has_reflection?(klass, name)
7
+ end
8
+
3
9
  def teardown
4
10
  SQLCounter.clear_log
5
11
  end
@@ -1,3 +1,4 @@
1
1
  class Favorite < ActiveRecord::Base
2
2
  belongs_to :tweet, counter_cache: :favorites_count_cache
3
+ has_many :notifications, as: :notifiable, foreign_key: :notifiable_id
3
4
  end
@@ -0,0 +1,3 @@
1
+ class Notification < ActiveRecord::Base
2
+ belongs_to :notifiable, polymorphic: true
3
+ end
@@ -3,4 +3,5 @@ class Tweet < ActiveRecord::Base
3
3
  has_many :favs, class_name: 'Favorite'
4
4
  has_many :my_favorites, -> { where(user_id: 1) }, class_name: 'Favorite', count_loader: true
5
5
  has_many :my_favs, -> { where(user_id: 1) }, class_name: 'Favorite'
6
+ has_many :notifications, as: :notifiable, foreign_key: :notifiable_id
6
7
  end
@@ -0,0 +1,4 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :tweets
3
+ has_many :favorites, through: :tweets
4
+ end
@@ -6,6 +6,13 @@ ActiveRecord::Schema.define do
6
6
  end
7
7
  add_index :favorites, :tweet_id
8
8
 
9
+ create_table :notifications, force: true do |t|
10
+ t.integer :notifiable_id
11
+ t.string :notifiable_type
12
+ t.timestamps null: false
13
+ end
14
+ add_index :notifications, [:notifiable_id, :notifiable_type]
15
+
9
16
  create_table :tweets, force: true do |t|
10
17
  t.integer :in_reply_to_tweet_id
11
18
  t.integer :user_id
@@ -13,4 +20,8 @@ ActiveRecord::Schema.define do
13
20
  t.timestamps null: false
14
21
  end
15
22
  add_index :tweets, :in_reply_to_tweet_id
23
+
24
+ create_table :users, force: true do |t|
25
+ t.timestamps null: false
26
+ end
16
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-precount
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takashi Kokubun
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-02 00:00:00.000000000 Z
11
+ date: 2016-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -193,6 +193,7 @@ extra_rdoc_files: []
193
193
  files:
194
194
  - ".gitignore"
195
195
  - ".travis.yml"
196
+ - CHANGELOG.md
196
197
  - Gemfile
197
198
  - LICENSE
198
199
  - README.md
@@ -204,12 +205,13 @@ files:
204
205
  - ci/Gemfile.activerecord-5.0.x
205
206
  - ci/travis.rb
206
207
  - lib/active_record/precount.rb
207
- - lib/active_record/precount/association_reflection_extension.rb
208
208
  - lib/active_record/precount/base_extension.rb
209
209
  - lib/active_record/precount/collection_proxy_extension.rb
210
+ - lib/active_record/precount/count_loader_builder.rb
210
211
  - lib/active_record/precount/has_many_extension.rb
211
212
  - lib/active_record/precount/join_dependency_extension.rb
212
213
  - lib/active_record/precount/preloader_extension.rb
214
+ - lib/active_record/precount/reflection_checker.rb
213
215
  - lib/active_record/precount/reflection_extension.rb
214
216
  - lib/active_record/precount/relation_extension.rb
215
217
  - lib/active_record/precount/version.rb
@@ -226,8 +228,11 @@ files:
226
228
  - sample/app/helpers/application_helper.rb
227
229
  - sample/app/mailers/.keep
228
230
  - sample/app/models/.keep
231
+ - sample/app/models/blog.rb
232
+ - sample/app/models/comment.rb
229
233
  - sample/app/models/concerns/.keep
230
234
  - sample/app/models/favorite.rb
235
+ - sample/app/models/page.rb
231
236
  - sample/app/models/tweet.rb
232
237
  - sample/app/models/user.rb
233
238
  - sample/app/views/application/index.html.erb
@@ -258,6 +263,9 @@ files:
258
263
  - sample/db/migrate/20141122002518_create_tweets.rb
259
264
  - sample/db/migrate/20141122002548_create_favorites.rb
260
265
  - sample/db/migrate/20141122002555_create_users.rb
266
+ - sample/db/migrate/20160603161811_create_blogs.rb
267
+ - sample/db/migrate/20160603161819_create_pages.rb
268
+ - sample/db/migrate/20160603161833_create_comments.rb
261
269
  - sample/db/schema.rb
262
270
  - sample/db/seeds.rb
263
271
  - sample/lib/assets/.keep
@@ -281,7 +289,9 @@ files:
281
289
  - test/config.example.yml
282
290
  - test/config.rb
283
291
  - test/models/favorite.rb
292
+ - test/models/notification.rb
284
293
  - test/models/tweet.rb
294
+ - test/models/user.rb
285
295
  - test/schema/schema.rb
286
296
  - test/support/autorun.rb
287
297
  - test/support/config.rb
@@ -1,45 +0,0 @@
1
- module ActiveRecord
2
- module Associations
3
- class CountLoader < SingularAssociation
4
- # Not preloaded behaviour of count_loader association
5
- # When this method is called, it will be N+1 query
6
- def load_target
7
- count_target = reflection.name_without_count.to_sym
8
- @target = owner.association(count_target).count
9
-
10
- loaded! unless loaded?
11
- target
12
- rescue ActiveRecord::RecordNotFound
13
- reset
14
- end
15
- end
16
- end
17
-
18
- module Precount
19
- module AssociationReflectionExtension
20
- def klass
21
- case macro
22
- when :count_loader
23
- @klass ||= active_record.send(:compute_type, options[:class_name] || name_without_count.singularize.classify)
24
- else
25
- super
26
- end
27
- end
28
-
29
- def name_without_count
30
- name.to_s.sub(/_count$/, "")
31
- end
32
-
33
- def association_class
34
- case macro
35
- when :count_loader
36
- ActiveRecord::Associations::CountLoader
37
- else
38
- super
39
- end
40
- end
41
- end
42
- end
43
-
44
- Reflection::AssociationReflection.prepend(Precount::AssociationReflectionExtension)
45
- end