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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +87 -0
- data/README.md +3 -3
- data/lib/active_record/precount.rb +27 -1
- data/lib/active_record/precount/base_extension.rb +1 -9
- data/lib/active_record/precount/collection_proxy_extension.rb +6 -6
- data/lib/active_record/precount/count_loader_builder.rb +66 -0
- data/lib/active_record/precount/has_many_extension.rb +22 -60
- data/lib/active_record/precount/join_dependency_extension.rb +1 -1
- data/lib/active_record/precount/preloader_extension.rb +30 -35
- data/lib/active_record/precount/reflection_checker.rb +15 -0
- data/lib/active_record/precount/reflection_extension.rb +24 -4
- data/lib/active_record/precount/relation_extension.rb +5 -16
- data/lib/active_record/precount/version.rb +1 -1
- data/sample/Gemfile +1 -1
- data/sample/app/models/blog.rb +3 -0
- data/sample/app/models/comment.rb +3 -0
- data/sample/app/models/page.rb +3 -0
- data/sample/config/database.yml +1 -1
- data/sample/db/migrate/20160603161811_create_blogs.rb +8 -0
- data/sample/db/migrate/20160603161819_create_pages.rb +8 -0
- data/sample/db/migrate/20160603161833_create_comments.rb +10 -0
- data/sample/db/schema.rb +18 -1
- data/test/cases/associations/eager_count_test.rb +13 -4
- data/test/cases/associations/precount_test.rb +13 -4
- data/test/cases/helper.rb +2 -0
- data/test/cases/test_case.rb +6 -0
- data/test/models/favorite.rb +1 -0
- data/test/models/notification.rb +3 -0
- data/test/models/tweet.rb +1 -0
- data/test/models/user.rb +4 -0
- data/test/schema/schema.rb +11 -0
- metadata +13 -3
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b261414eec5a1fd723fb2fcea8cbe708c48415ca
|
4
|
+
data.tar.gz: 1d522f1cb7b9a7f0ac94d479feed434805d3077f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ddc79eeb3385aa2be6a1973c5e1bb11dba1fe123e3e9e6064fb3c16445922ff8273d8268a0ca37a8801b31fa9edd3cb5f70c091f5f3d23efa8fa2d4fde7915e
|
7
|
+
data.tar.gz: 4340c6a29f8040a154d1f6d6c5a401064efc60bfab6ec654c808da6a2e58fc804c834507d8c2d35229034ead2dc72340e85218eaa31fd053f91d7a9a3d59c99a
|
data/CHANGELOG.md
ADDED
@@ -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.
|
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.
|
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
|
-
###
|
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.
|
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
|
7
|
+
return super if args.present?
|
6
8
|
|
7
9
|
counter_name = :"#{@association.reflection.name}_count"
|
8
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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.
|
36
|
+
Associations::Builder::HasMany.extend(Precount::Builder::HasManyExtension)
|
75
37
|
else
|
76
|
-
Associations::Builder::HasMany.prepend(Precount::Builder::
|
38
|
+
Associations::Builder::HasMany.prepend(Precount::Builder::HasManyExtension)
|
77
39
|
end
|
78
40
|
end
|
@@ -2,23 +2,42 @@ module ActiveRecord
|
|
2
2
|
module Associations
|
3
3
|
class Preloader
|
4
4
|
class CountLoader < SingularAssociation
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
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
|
5
|
-
|
20
|
+
def macro; :count_loader; end
|
21
|
+
|
22
|
+
def association_class
|
23
|
+
ActiveRecord::Associations::CountLoader
|
6
24
|
end
|
7
25
|
|
8
|
-
def
|
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
|
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
|
-
|
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
|
-
|
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
|
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|
|
data/sample/Gemfile
CHANGED
data/sample/config/database.yml
CHANGED
data/sample/db/schema.rb
CHANGED
@@ -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:
|
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
|
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,
|
28
|
+
assert_equal(false, has_reflection?(Tweet, :favs_count))
|
27
29
|
Tweet.eager_count(:favs).map(&:favs_count)
|
28
|
-
assert_equal(true,
|
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
|
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,
|
28
|
+
assert_equal(false, has_reflection?(Tweet, :favs_count))
|
27
29
|
Tweet.precount(:favs).map(&:favs_count)
|
28
|
-
assert_equal(true,
|
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
|
data/test/cases/helper.rb
CHANGED
data/test/cases/test_case.rb
CHANGED
@@ -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
|
data/test/models/favorite.rb
CHANGED
data/test/models/tweet.rb
CHANGED
@@ -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
|
data/test/models/user.rb
ADDED
data/test/schema/schema.rb
CHANGED
@@ -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.
|
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-
|
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
|