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.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Build Status](https://travis-ci.org/Bajena/ams_lazy_relationships.svg?branch=master)](https://travis-ci.org/Bajena/ams_lazy_relationships)
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
- lazy_blog_posts
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
- lazy_comments.map(&:decorate)
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. Build and push to rubygems
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 = "v0.1.4"
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", "~> 1"
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", "~> 10.0"
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", "~> 1.3.6"
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", "~> 2.0"
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
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "active_model_serializers", "0.10.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "active_model_serializers", "0.10.10"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "active_model_serializers", "0.10.3"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "batch-loader", "~> 1"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "batch-loader", "~> 2"
6
+
7
+ gemspec path: "../"
@@ -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(:load_all_lazy_relationships, object)
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 load_all_lazy_relationships(object, level = NESTING_START_LEVEL)
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
- load_lazy_relationship(lrm, object, level)
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 load_lazy_relationship(lrm, object, level = NESTING_START_LEVEL)
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
- deep_load_for_yielded_records(
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 deep_load_for_yielded_records(batch_records, lrm, level)
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
- lrm.serializer_class.send(:load_all_lazy_relationships, r, level + 1)
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