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.
- 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
|