activerecord-precount 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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