ams_lazy_relationships 0.1.4 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +16 -1
- data/Appraisals +20 -0
- data/CHANGELOG.md +63 -1
- data/Gemfile.lock +169 -123
- data/README.md +57 -4
- data/RELEASE.md +3 -2
- data/Rakefile +2 -1
- data/ams_lazy_relationships.gemspec +8 -6
- data/gemfiles/ams_0.10.0.gemfile +7 -0
- data/gemfiles/ams_0.10.10.gemfile +7 -0
- data/gemfiles/ams_0.10.3.gemfile +7 -0
- data/gemfiles/batch_loader_1.gemfile +7 -0
- data/gemfiles/batch_loader_2.gemfile +7 -0
- data/lib/ams_lazy_relationships.rb +4 -0
- data/lib/ams_lazy_relationships/core.rb +3 -1
- data/lib/ams_lazy_relationships/core/evaluation.rb +41 -6
- data/lib/ams_lazy_relationships/core/lazy_dig_method.rb +94 -0
- data/lib/ams_lazy_relationships/core/lazy_relationship_meta.rb +0 -12
- data/lib/ams_lazy_relationships/core/lazy_relationship_method.rb +1 -1
- data/lib/ams_lazy_relationships/core/relationship_wrapper_methods.rb +15 -10
- data/lib/ams_lazy_relationships/extensions.rb +8 -0
- data/lib/ams_lazy_relationships/extensions/reflection.rb +71 -0
- data/lib/ams_lazy_relationships/loaders/association.rb +26 -22
- data/lib/ams_lazy_relationships/loaders/base.rb +48 -0
- data/lib/ams_lazy_relationships/loaders/direct.rb +15 -21
- data/lib/ams_lazy_relationships/loaders/simple_belongs_to.rb +7 -17
- data/lib/ams_lazy_relationships/loaders/simple_has_many.rb +11 -18
- data/lib/ams_lazy_relationships/version.rb +1 -1
- metadata +42 -34
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
[![Build Status](https://travis-ci.
|
1
|
+
[![Build Status](https://travis-ci.com/Bajena/ams_lazy_relationships.svg?branch=master)](https://travis-ci.com/Bajena/ams_lazy_relationships)
|
2
2
|
[![Maintainability](https://api.codeclimate.com/v1/badges/c21b988e09db63396309/maintainability)](https://codeclimate.com/github/Bajena/ams_lazy_relationships/maintainability)
|
3
3
|
[![Test Coverage](https://api.codeclimate.com/v1/badges/c21b988e09db63396309/test_coverage)](https://codeclimate.com/github/Bajena/ams_lazy_relationships/test_coverage)
|
4
4
|
|
@@ -63,8 +63,10 @@ class UserSerializer < BaseSerializer
|
|
63
63
|
|
64
64
|
# The previous one is a shorthand for the following lines:
|
65
65
|
lazy_relationship :blog_posts, loader: AmsLazyRelationships::Loaders::Association.new("User", :blog_posts)
|
66
|
-
has_many :blog_posts, serializer: BlogPostSerializer do
|
67
|
-
|
66
|
+
has_many :blog_posts, serializer: BlogPostSerializer do |serializer|
|
67
|
+
# non-proc custom finder will work as well, but it can produce redundant sql
|
68
|
+
# queries, please see [Example 2: Modifying the relationship before rendering](#example-2-modifying-the-relationship-before-rendering)
|
69
|
+
-> { serializer.lazy_blog_posts }
|
68
70
|
end
|
69
71
|
|
70
72
|
lazy_has_one :poro_model, loader: AmsLazyRelationships::Loaders::Direct.new(:poro_model) { |object| PoroModel.new(object) }
|
@@ -116,10 +118,43 @@ end
|
|
116
118
|
#### Example 2: Modifying the relationship before rendering
|
117
119
|
Sometimes it may happen that you need to process the relationship before rendering, e.g. decorate the records. In this case the gem provides a special method (in our case `lazy_comments`) for each defined relationship. Check out the example - we'll decorate every comment before serializing:
|
118
120
|
|
121
|
+
```ruby
|
122
|
+
class BlogPostSerializer < BaseSerializer
|
123
|
+
lazy_has_many :comments do |serializer|
|
124
|
+
-> { serializer.lazy_comments.map(&:decorate) }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
Despite the fact that non-block custom finder such as
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
class BlogPostSerializer < BaseSerializer
|
133
|
+
lazy_has_many :comments do |serializer|
|
134
|
+
serializer.lazy_comments.map(&:decorate)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
will work still, it's better to implement it in a form of lambda, in order to avoid redundant SQL queries when `include_data` AMS setting appears to be `false`:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class BlogPostSerializer < BaseSerializer
|
143
|
+
lazy_has_many :comments do |serializer|
|
144
|
+
include_data :if_sideloaded
|
145
|
+
-> { serializer.lazy_comments.map(&:decorate) }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
Feel free to skip custom lazy finder for association if your goal is just to define `include_data` setting and/or to specify some links and metas:
|
151
|
+
|
119
152
|
```ruby
|
120
153
|
class BlogPostSerializer < BaseSerializer
|
121
154
|
lazy_has_many :comments do
|
122
|
-
|
155
|
+
include_data :if_sideloaded
|
156
|
+
link :self, 'a link'
|
157
|
+
meta name: 'Dan Brown'
|
123
158
|
end
|
124
159
|
end
|
125
160
|
```
|
@@ -163,6 +198,24 @@ class BlogPostSerializer < BaseSerializer
|
|
163
198
|
end
|
164
199
|
```
|
165
200
|
|
201
|
+
#### Example 6: Lazy dig through relationships
|
202
|
+
In additional to previous example you may want to make use of nested lazy relationship without rendering of any nested record.
|
203
|
+
There is an `lazy_dig` method to be used for that:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
class AuthorSerializer < BaseSerializer
|
207
|
+
lazy_relationship :address
|
208
|
+
end
|
209
|
+
|
210
|
+
class BlogPostSerializer < BaseSerializer
|
211
|
+
lazy_relationship :author
|
212
|
+
|
213
|
+
attribute :author_address do
|
214
|
+
lazy_dig(:author, :address)&.full_address
|
215
|
+
end
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
166
219
|
## Performance comparison with vanilla AMS
|
167
220
|
|
168
221
|
In general the bigger and more complex your serialized records hierarchy is and the more latency you have in your DB the more you'll benefit from using this gem.
|
data/RELEASE.md
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# Release steps
|
2
2
|
1. Bump VERSION constant
|
3
|
-
2. Generate changelog and update
|
3
|
+
2. Generate changelog and update
|
4
4
|
```shell
|
5
5
|
CHANGELOG_GITHUB_TOKEN=<token> bundle exec rake changelog
|
6
6
|
```
|
7
7
|
3. Run `bundle` to regenerate Gemfile.lock
|
8
|
-
4.
|
8
|
+
4. Commit & push a new tag
|
9
|
+
5. Build and push to rubygems
|
9
10
|
```shell
|
10
11
|
gem build ams_lazy_relationships
|
11
12
|
gem push ams_lazy_relationships-x.y.z.gem
|
data/Rakefile
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "rspec/core/rake_task"
|
5
5
|
require "github_changelog_generator/task"
|
6
|
+
require "ams_lazy_relationships/version"
|
6
7
|
|
7
8
|
RSpec::Core::RakeTask.new(:spec)
|
8
9
|
|
@@ -11,5 +12,5 @@ task default: :spec
|
|
11
12
|
GitHubChangelogGenerator::RakeTask.new :changelog do |config|
|
12
13
|
config.user = "Bajena"
|
13
14
|
config.project = "ams_lazy_relationships"
|
14
|
-
config.future_release = "
|
15
|
+
config.future_release = "v#{AmsLazyRelationships::VERSION}"
|
15
16
|
end
|
@@ -33,30 +33,32 @@ Gem::Specification.new do |spec|
|
|
33
33
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
34
34
|
spec.require_paths = ["lib"]
|
35
35
|
|
36
|
-
spec.add_dependency "active_model_serializers"
|
37
|
-
spec.add_dependency "batch-loader"
|
36
|
+
spec.add_dependency "active_model_serializers", ">= 0.10.0.rc4"
|
37
|
+
spec.add_dependency "batch-loader"
|
38
38
|
|
39
39
|
spec.add_development_dependency "activerecord"
|
40
40
|
# A Ruby library for testing against different versions of dependencies
|
41
41
|
spec.add_development_dependency "appraisal"
|
42
|
-
spec.add_development_dependency "bundler", "~> 1.17"
|
43
42
|
# Rspec matchers for SQL query counts
|
44
43
|
spec.add_development_dependency "db-query-matchers"
|
45
44
|
spec.add_development_dependency "github_changelog_generator"
|
46
45
|
spec.add_development_dependency "pry"
|
47
46
|
spec.add_development_dependency "pry-nav"
|
48
|
-
spec.add_development_dependency "rake", "~>
|
47
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
49
48
|
spec.add_development_dependency "rspec", "~> 3.0"
|
50
49
|
spec.add_development_dependency "rspec-rails", "~> 3.5"
|
51
50
|
spec.add_development_dependency "rubocop", "= 0.61.0"
|
52
51
|
spec.add_development_dependency "rubocop-rspec", "= 1.20.1"
|
53
52
|
spec.add_development_dependency "simplecov"
|
54
53
|
spec.add_development_dependency "simplecov-lcov"
|
55
|
-
spec.add_development_dependency "sqlite3"
|
54
|
+
spec.add_development_dependency "sqlite3"
|
56
55
|
# Detect untested code blocks in recent changes
|
57
56
|
spec.add_development_dependency "undercover"
|
58
57
|
# Dynamically build an Active Record model (with table) within a test context
|
59
|
-
spec.add_development_dependency "with_model"
|
58
|
+
spec.add_development_dependency "with_model"
|
59
|
+
|
60
|
+
# Implicit dependency of AMS - used to be a part of Rails
|
61
|
+
spec.add_development_dependency "thread_safe"
|
60
62
|
|
61
63
|
spec.add_development_dependency "benchmark-memory", "~> 0.1"
|
62
64
|
end
|
@@ -1,6 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "batch-loader"
|
4
|
+
require "active_model_serializers"
|
5
|
+
|
3
6
|
require "ams_lazy_relationships/version"
|
7
|
+
require "ams_lazy_relationships/extensions"
|
4
8
|
require "ams_lazy_relationships/loaders"
|
5
9
|
require "ams_lazy_relationships/core"
|
6
10
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "ams_lazy_relationships/core/lazy_relationship_method"
|
4
|
+
require "ams_lazy_relationships/core/lazy_dig_method"
|
4
5
|
require "ams_lazy_relationships/core/relationship_wrapper_methods"
|
5
6
|
require "ams_lazy_relationships/core/evaluation"
|
6
7
|
|
@@ -18,6 +19,7 @@ module AmsLazyRelationships::Core
|
|
18
19
|
|
19
20
|
def self.included(klass)
|
20
21
|
klass.send :extend, ClassMethods
|
22
|
+
klass.send :include, LazyDigMethod
|
21
23
|
klass.send :prepend, Initializer
|
22
24
|
|
23
25
|
klass.send(:define_relationship_wrapper_methods)
|
@@ -48,7 +50,7 @@ module AmsLazyRelationships::Core
|
|
48
50
|
def initialize(*)
|
49
51
|
super
|
50
52
|
|
51
|
-
self.class.send(:
|
53
|
+
self.class.send(:init_all_lazy_relationships, object)
|
52
54
|
end
|
53
55
|
end
|
54
56
|
end
|
@@ -8,26 +8,47 @@ module AmsLazyRelationships::Core
|
|
8
8
|
LAZY_NESTING_LEVELS = 3
|
9
9
|
NESTING_START_LEVEL = 1
|
10
10
|
|
11
|
+
# Loads the lazy relationship
|
12
|
+
#
|
13
|
+
# @param relation_name [Symbol] relation name to be loaded
|
14
|
+
# @param object [Object] Lazy relationships will be loaded for this record.
|
15
|
+
def load_lazy_relationship(relation_name, object)
|
16
|
+
lrm = lazy_relationships[relation_name]
|
17
|
+
unless lrm
|
18
|
+
raise ArgumentError, "Undefined lazy '#{relation_name}' relationship for '#{name}' serializer"
|
19
|
+
end
|
20
|
+
|
21
|
+
# We need to evaluate the promise right before serializer tries
|
22
|
+
# to touch it. Otherwise the various side effects can happen:
|
23
|
+
# 1. AMS will attempt to serialize nil values with a specific V1 serializer
|
24
|
+
# 2. `lazy_association ? 'exists' : 'missing'` expression will always
|
25
|
+
# equal to 'exists'
|
26
|
+
# 3. `lazy_association&.id` expression can raise NullPointer exception
|
27
|
+
#
|
28
|
+
# Calling `__sync` will evaluate the promise.
|
29
|
+
init_lazy_relationship(lrm, object).__sync
|
30
|
+
end
|
31
|
+
|
11
32
|
# Recursively loads the tree of lazy relationships
|
12
33
|
# The nesting is limited to 3 levels.
|
13
34
|
#
|
14
35
|
# @param object [Object] Lazy relationships will be loaded for this record.
|
15
36
|
# @param level [Integer] Current nesting level
|
16
|
-
def
|
37
|
+
def init_all_lazy_relationships(object, level = NESTING_START_LEVEL)
|
17
38
|
return if level >= LAZY_NESTING_LEVELS
|
18
39
|
return unless object
|
19
40
|
|
20
41
|
return unless lazy_relationships
|
21
42
|
|
22
43
|
lazy_relationships.each_value do |lrm|
|
23
|
-
|
44
|
+
init_lazy_relationship(lrm, object, level)
|
24
45
|
end
|
25
46
|
end
|
26
47
|
|
27
48
|
# @param lrm [LazyRelationshipMeta] relationship data
|
28
49
|
# @param object [Object] Object to load the relationship for
|
29
50
|
# @param level [Integer] Current nesting level
|
30
|
-
def
|
51
|
+
def init_lazy_relationship(lrm, object, level = NESTING_START_LEVEL)
|
31
52
|
load_for_object = if lrm.load_for.present?
|
32
53
|
object.public_send(lrm.load_for)
|
33
54
|
else
|
@@ -35,7 +56,7 @@ module AmsLazyRelationships::Core
|
|
35
56
|
end
|
36
57
|
|
37
58
|
lrm.loader.load(load_for_object) do |batch_records|
|
38
|
-
|
59
|
+
deep_init_for_yielded_records(
|
39
60
|
batch_records,
|
40
61
|
lrm,
|
41
62
|
level
|
@@ -43,14 +64,28 @@ module AmsLazyRelationships::Core
|
|
43
64
|
end
|
44
65
|
end
|
45
66
|
|
46
|
-
def
|
67
|
+
def deep_init_for_yielded_records(batch_records, lrm, level)
|
47
68
|
# There'll be no more nesting if there's no
|
48
69
|
# reflection for this relationship. We can skip deeper lazy loading.
|
49
70
|
return unless lrm.reflection
|
50
71
|
|
51
72
|
Array.wrap(batch_records).each do |r|
|
52
|
-
|
73
|
+
deep_init_for_yielded_record(r, lrm, level)
|
53
74
|
end
|
54
75
|
end
|
76
|
+
|
77
|
+
def deep_init_for_yielded_record(batch_record, lrm, level)
|
78
|
+
serializer = lazy_serializer_for(batch_record, lrm: lrm)
|
79
|
+
return unless serializer
|
80
|
+
|
81
|
+
serializer.send(:init_all_lazy_relationships, batch_record, level + 1)
|
82
|
+
end
|
83
|
+
|
84
|
+
def lazy_serializer_for(object, lrm: nil, relation_name: nil)
|
85
|
+
lrm ||= lazy_relationships[relation_name]
|
86
|
+
return unless lrm&.reflection
|
87
|
+
|
88
|
+
serializer_for(object, lrm.reflection.options)
|
89
|
+
end
|
55
90
|
end
|
56
91
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AmsLazyRelationships::Core
|
4
|
+
# Provides `lazy_dig` as an instance method for serializers, in order to make
|
5
|
+
# possible to dig relationships in depth just like `Hash#dig` do, keeping the
|
6
|
+
# laziness and N+1-free evaluation.
|
7
|
+
module LazyDigMethod
|
8
|
+
# @param relation_names [Array<Symbol>] the sequence of relation names
|
9
|
+
# to dig through.
|
10
|
+
# @return [ActiveRecord::Base, Array<ActiveRecord::Base>, nil] ActiveRecord
|
11
|
+
# objects found by digging through the sequence of nested relationships.
|
12
|
+
# Singular or plural nature of returned value depends from the
|
13
|
+
# singular/plural nature of the chain of relation_names.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# class AuthorSerializer < BaseSerializer
|
17
|
+
# lazy_belongs_to :address
|
18
|
+
# lazy_has_many :rewards
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# class BlogPostSerializer < BaseSerializer
|
22
|
+
# lazy_belongs_to :author
|
23
|
+
#
|
24
|
+
# attribute :author_address do
|
25
|
+
# # returns single AR object or nil
|
26
|
+
# lazy_dig(:author, :address)&.full_address
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# attribute :author_rewards do
|
30
|
+
# # returns an array of AR objects
|
31
|
+
# lazy_dig(:author, :rewards).map(&:description)
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
def lazy_dig(*relation_names)
|
35
|
+
relationships = {
|
36
|
+
multiple: false,
|
37
|
+
data: [{
|
38
|
+
serializer: self.class,
|
39
|
+
object: object
|
40
|
+
}]
|
41
|
+
}
|
42
|
+
|
43
|
+
relation_names.each do |relation_name|
|
44
|
+
lazy_dig_relationship!(relation_name, relationships)
|
45
|
+
end
|
46
|
+
|
47
|
+
objects = relationships[:data].map { |r| r[:object] }
|
48
|
+
|
49
|
+
relationships[:multiple] ? objects : objects.first
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def lazy_dig_relationship!(relation_name, relationships)
|
55
|
+
relationships[:data].map! do |data|
|
56
|
+
serializer = data[:serializer]
|
57
|
+
object = data[:object]
|
58
|
+
|
59
|
+
next_objects = lazy_dig_next_objects!(relation_name, serializer, object)
|
60
|
+
next unless next_objects
|
61
|
+
|
62
|
+
relationships[:multiple] ||= next_objects.respond_to?(:to_ary)
|
63
|
+
|
64
|
+
lazy_dig_next_relationships!(relation_name, serializer, next_objects)
|
65
|
+
end
|
66
|
+
|
67
|
+
relationships[:data].flatten!
|
68
|
+
relationships[:data].compact!
|
69
|
+
end
|
70
|
+
|
71
|
+
def lazy_dig_next_objects!(relation_name, serializer, object)
|
72
|
+
serializer&.send(
|
73
|
+
:load_lazy_relationship,
|
74
|
+
relation_name,
|
75
|
+
object
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def lazy_dig_next_relationships!(relation_name, serializer, next_objects)
|
80
|
+
Array.wrap(next_objects).map do |next_object|
|
81
|
+
next_serializer = serializer.send(
|
82
|
+
:lazy_serializer_for,
|
83
|
+
next_object,
|
84
|
+
relation_name: relation_name
|
85
|
+
)
|
86
|
+
|
87
|
+
{
|
88
|
+
serializer: next_serializer,
|
89
|
+
object: next_object
|
90
|
+
}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -19,17 +19,5 @@ module AmsLazyRelationships::Core
|
|
19
19
|
end
|
20
20
|
|
21
21
|
attr_reader :name, :loader, :reflection, :load_for
|
22
|
-
|
23
|
-
# @return [ActiveModel::Serializer] AMS Serializer class for the relationship
|
24
|
-
def serializer_class
|
25
|
-
return @serializer_class if defined?(@serializer_class)
|
26
|
-
|
27
|
-
@serializer_class =
|
28
|
-
if AmsLazyRelationships::Core.ams_version <= Gem::Version.new("0.10.0.rc2")
|
29
|
-
reflection[:association_options][:serializer]
|
30
|
-
else
|
31
|
-
reflection.options[:serializer]
|
32
|
-
end
|
33
|
-
end
|
34
22
|
end
|
35
23
|
end
|