ams_lazy_relationships 0.1.4 → 0.3.2

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.
@@ -39,7 +39,7 @@ module AmsLazyRelationships::Core
39
39
  @lazy_relationships[name] = lrm
40
40
 
41
41
  define_method :"lazy_#{name}" do
42
- self.class.send(:load_lazy_relationship, lrm, object)
42
+ self.class.send(:load_lazy_relationship, name, object)
43
43
  end
44
44
  end
45
45
 
@@ -19,7 +19,7 @@ module AmsLazyRelationships::Core
19
19
  define_singleton_method(
20
20
  "lazy_#{relationship_type}"
21
21
  ) do |relationship_name, options = {}, &block|
22
- send(:define_lazy_association, relationship_type, relationship_name, options, block)
22
+ define_lazy_association(relationship_type, relationship_name, options, block)
23
23
  end
24
24
  end
25
25
  end
@@ -29,17 +29,22 @@ module AmsLazyRelationships::Core
29
29
 
30
30
  real_relationship_options = options.except(*lazy_relationship_option_keys)
31
31
 
32
- block ||= lambda do |serializer|
33
- # We need to evaluate the promise right before AMS tries
34
- # to serialize it. Otherwise AMS will attempt to serialize nil values
35
- # with a specific V1 serializer.
36
- # Calling `itself` will evaluate the promise.
37
- serializer.public_send("lazy_#{name}").tap(&:itself)
38
- end
32
+ public_send(type, name.to_sym, real_relationship_options) do |serializer|
33
+ block_value = instance_exec(serializer, &block) if block
39
34
 
40
- public_send(type, name.to_sym, real_relationship_options, &block)
35
+ if block && block_value != :nil
36
+ # respect the custom finder for lazy association
37
+ # @see https://github.com/rails-api/active_model_serializers/blob/v0.10.10/lib/active_model/serializer/reflection.rb#L165-L168
38
+ block_value
39
+ else
40
+ # provide default lazy association finder in a form of lambda,
41
+ # in order to play nice with possible `include_data` setting.
42
+ # @see lib/ams_lazy_relationships/extensions/reflection.rb
43
+ serializer.method("lazy_#{name}")
44
+ end
45
+ end
41
46
 
42
- lazy_relationship(name, options.slice(*lazy_relationship_option_keys))
47
+ lazy_relationship(name, **options.slice(*lazy_relationship_option_keys))
43
48
  end
44
49
  end
45
50
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ams_lazy_relationships/extensions/reflection"
4
+
5
+ module AmsLazyRelationships
6
+ module Extensions
7
+ end
8
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # There is a general problem inside AMS related to custom association finder
4
+ # combined with `include_data` setting:
5
+ #
6
+ # class BlogPostSerializer < BaseSerializer
7
+ # belongs_to :category do
8
+ # include_data :if_sideloaded
9
+ # object.categories.last
10
+ # end
11
+ # end
12
+ #
13
+ # The problem is that `belongs_to` block will be fully evaluated each time for
14
+ # each object, and only after that AMS is able to take into account
15
+ # `include_data` mode -
16
+ # https://github.com/rails-api/active_model_serializers/blob/v0.10.10/lib/active_model/serializer/reflection.rb#L162-L163
17
+ #
18
+ # def value(serializer, include_slice)
19
+ # # ...
20
+ # block_value = instance_exec(serializer, &block) if block
21
+ # return unless include_data?(include_slice)
22
+ # # ...
23
+ # end
24
+ #
25
+ # That causing redundant (and so huge potentially!) SQL queries and AR objects
26
+ # allocation when `include_data` appears to be `false` but `belongs_to` block
27
+ # defines instant (not a kind of AR::Relation) custom association finder.
28
+ #
29
+ # Described problem is a very specific use case for pure AMS applications.
30
+ # The bad news is that `ams_lazy_relationships` always utilizes the
31
+ # association block -
32
+ # https://github.com/Bajena/ams_lazy_relationships/blob/v0.2.0/lib/ams_lazy_relationships/core/relationship_wrapper_methods.rb#L32-L36
33
+ #
34
+ # def define_lazy_association(type, name, options, block)
35
+ # #...
36
+ # block ||= lambda do |serializer|
37
+ # serializer.public_send("lazy_#{name}")
38
+ # end
39
+ #
40
+ # public_send(type, name.to_sym, real_relationship_options, &block)
41
+ # #...
42
+ # end
43
+ #
44
+ # This way we break `include_data` optimizations for the host application.
45
+ #
46
+ # In order to overcome that we are forced to monkey-patch
47
+ # `AmsLazyRelationships::Extensions::Reflection#value` method and make it to be
48
+ # ready for Proc returned by association block. This way we will use a kind of
49
+ #
50
+ # block ||= lambda do |serializer|
51
+ # -> { serializer.public_send("lazy_#{name}") }
52
+ # end
53
+ #
54
+ # as association block, then AMS will evaluate it, get the value of `include_data`
55
+ # setting, make a decision do we need to continue with that association, if so -
56
+ # will finally evaluate the proc with lazy relationship inside it.
57
+
58
+ module AmsLazyRelationships
59
+ module Extensions
60
+ module Reflection
61
+ def value(*)
62
+ case (block_value = super)
63
+ when Proc, Method then block_value.call
64
+ else block_value
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ ::ActiveModel::Serializer::Reflection.prepend AmsLazyRelationships::Extensions::Reflection
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ams_lazy_relationships/loaders/base"
4
+
3
5
  module AmsLazyRelationships
4
6
  module Loaders
5
7
  # Lazy loads (has_one/has_many/has_many_through/belongs_to) ActiveRecord
6
8
  # associations for ActiveRecord models
7
- class Association
9
+ class Association < Base
8
10
  # @param model_class_name [String] The name of AR class for which the
9
11
  # associations are loaded. E.g. When loading comment.blog_post
10
12
  # it'd be "BlogPost".
@@ -15,31 +17,13 @@ module AmsLazyRelationships
15
17
  @association_name = association_name
16
18
  end
17
19
 
18
- # Lazy loads and yields the data when evaluating
19
- # @param record [Object] an object for which we're loading the data
20
- # @param block [Proc] a block to execute when data is evaluated.
21
- # Loaded data is yielded as a block argument.
22
- def load(record, &block)
23
- BatchLoader.for(record).batch(key: batch_key, replace_methods: false) do |records, loader|
24
- data = load_data(records, loader)
25
-
26
- block&.call(data)
27
- end
28
- end
29
-
30
20
  private
31
21
 
32
22
  attr_reader :model_class_name, :association_name
33
23
 
34
24
  def load_data(records, loader)
35
- # It may happen that same record comes here twice (e.g. wrapped
36
- # in a decorator and non-wrapped). In this case Associations::Preloader
37
- # stores duplicated records in has_many relationships for some reason.
38
- # Calling uniq(&:id) solves the problem.
39
- records_to_preload = records.uniq(&:id)
40
-
41
25
  ::ActiveRecord::Associations::Preloader.new.preload(
42
- records_to_preload, association_name
26
+ records_to_preload(records), association_name
43
27
  )
44
28
 
45
29
  data = []
@@ -52,8 +36,28 @@ module AmsLazyRelationships
52
36
  data = data.flatten.compact.uniq
53
37
  end
54
38
 
55
- def batch_key
56
- "#{model_class_name}/#{association_name}"
39
+ def batch_key(_)
40
+ @batch_key ||= "#{model_class_name}/#{association_name}"
41
+ end
42
+
43
+ def records_to_preload(records)
44
+ # It may happen that same record comes here twice (e.g. wrapped
45
+ # in a decorator and non-wrapped). In this case Associations::Preloader
46
+ # stores duplicated records in has_many relationships for some reason.
47
+ # Calling uniq(&:id) solves the problem.
48
+ #
49
+ # One more case when duplicated records appear in has_many relationships
50
+ # is the recent assignation to `accept_nested_attributes_for` setter.
51
+ # ActiveRecord will not mark the association as `loaded` but in same
52
+ # time will keep internal representation of the nested records created
53
+ # by `accept_nested_attributes_for`. Then Associations::Preloader is
54
+ # going to merge internal state of associated records with the same
55
+ # records recently stored in DB. `r.association(association_name).reset`
56
+ # effectively fixes that.
57
+ records.
58
+ uniq(&:id).
59
+ reject { |r| r.association(association_name).loaded? }.
60
+ each { |r| r.association(association_name).reset }
57
61
  end
58
62
  end
59
63
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AmsLazyRelationships
4
+ module Loaders
5
+ # A base class for all the loaders. A correctly defined loader requires
6
+ # the `load_data` and `batch_key` methods.
7
+ class Base
8
+ # Lazy loads and yields the data when evaluating
9
+ # @param record [Object] an object for which we're loading the data
10
+ # @param block [Proc] a block to execute when data is evaluated.
11
+ # Loaded data is yielded as a block argument.
12
+ def load(record, &block)
13
+ BatchLoader.for(record).batch(
14
+ key: batch_key(record),
15
+ # Replacing methods can be costly, especially on objects with lots
16
+ # of methods (like AR methods). Let's disable it.
17
+ # More info:
18
+ # https://github.com/exAspArk/batch-loader/tree/v1.4.1#replacing-methods
19
+ replace_methods: false
20
+ ) do |records, loader|
21
+ data = load_data(records, loader)
22
+
23
+ block&.call(data)
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ # Loads required data for all records gathered by the batch loader.
30
+ # Assigns data to records by calling the `loader` lambda.
31
+ # @param records [Array<Object>] Array of all gathered records.
32
+ # @param loader [Proc] Proc used for assigning the batch loaded data to
33
+ # records. First argument is the record and the second is the data
34
+ # loaded for it.
35
+ # @returns [Array<Object>] Array of loaded objects
36
+ def load_data(_records, _loader)
37
+ raise "Implement in child"
38
+ end
39
+
40
+ # Computes a batching key based on currently evaluated record
41
+ # @param record [Object]
42
+ # @returns [String] Batching key
43
+ def batch_key(_record)
44
+ raise "Implement in child"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ams_lazy_relationships/loaders/base"
4
+
3
5
  module AmsLazyRelationships
4
6
  module Loaders
5
7
  # Lazy loads data in a "dumb" way - just executes the provided block when needed
6
- class Direct
8
+ class Direct < Base
7
9
  # @param relationship_name [Symbol] used for building cache key. Also if the
8
10
  # `load_block` param is `nil` the loader will just call `relationship_name`
9
11
  # method on the record being processed.
@@ -14,30 +16,22 @@ module AmsLazyRelationships
14
16
  @load_block = load_block
15
17
  end
16
18
 
17
- # Lazy loads and yields the data when evaluating
18
- # @param record [Object] an object for which we're loading the data
19
- # @param block [Proc] a block to execute when data is evaluated.
20
- # Loaded data is yielded as a block argument.
21
- def load(record, &block)
22
- BatchLoader.for(record).batch(key: cache_key(record), replace_methods: false) do |records, loader|
23
- data = []
24
- records.each do |r|
25
- value = calculate_value(r)
26
- data << value
27
- loader.call(r, value)
28
- end
29
-
30
- data = data.flatten.compact.uniq
31
-
32
- block&.call(data)
33
- end
34
- end
35
-
36
19
  private
37
20
 
38
21
  attr_reader :relationship_name, :load_block
39
22
 
40
- def cache_key(record)
23
+ def load_data(records, loader)
24
+ data = []
25
+ records.each do |r|
26
+ value = calculate_value(r)
27
+ data << value
28
+ loader.call(r, value)
29
+ end
30
+
31
+ data = data.flatten.compact.uniq
32
+ end
33
+
34
+ def batch_key(record)
41
35
  "#{record.class}/#{relationship_name}"
42
36
  end
43
37
 
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ams_lazy_relationships/loaders/base"
4
+
3
5
  module AmsLazyRelationships
4
6
  module Loaders
5
7
  # Batch loads parent ActiveRecord records for given record by foreign key.
6
8
  # Useful when the relationship is not a standard ActiveRecord relationship.
7
- class SimpleBelongsTo
9
+ class SimpleBelongsTo < Base
8
10
  # @param association_class_name [String] The name of AR class being the parent
9
11
  # record of the records being loaded. E.g. When loading comment.blog_post
10
12
  # it'd be "BlogPost".
@@ -18,25 +20,11 @@ module AmsLazyRelationships
18
20
  @foreign_key = foreign_key.to_sym
19
21
  end
20
22
 
21
- # Lazy loads and yields the data when evaluating
22
- # @param record [Object] an object for which we're loading the belongs to data
23
- # @param block [Proc] a block to execute when data is evaluated
24
- # Loaded data is yielded as a block argument.
25
- def load(record, &block)
26
- BatchLoader.for(record).batch(key: cache_key(record), replace_methods: false) do |records, loader|
27
- data = load_data(records)
28
-
29
- block&.call(data)
30
-
31
- resolve(records, data, loader)
32
- end
33
- end
34
-
35
23
  private
36
24
 
37
25
  attr_reader :association_class_name, :foreign_key
38
26
 
39
- def load_data(records)
27
+ def load_data(records, loader)
40
28
  data_ids = records.map(&foreign_key).compact.uniq
41
29
  data = if data_ids.present?
42
30
  association_class_name.constantize.where(id: data_ids)
@@ -44,6 +32,8 @@ module AmsLazyRelationships
44
32
  []
45
33
  end
46
34
 
35
+ resolve(records, data, loader)
36
+
47
37
  data
48
38
  end
49
39
 
@@ -56,7 +46,7 @@ module AmsLazyRelationships
56
46
  end
57
47
  end
58
48
 
59
- def cache_key(record)
49
+ def batch_key(record)
60
50
  "#{record.class}/#{association_class_name}"
61
51
  end
62
52
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ams_lazy_relationships/loaders/base"
4
+
3
5
  module AmsLazyRelationships
4
6
  module Loaders
5
7
  # Batch loads ActiveRecord records belonging to given record by foreign key.
6
8
  # Useful when the relationship is not a standard ActiveRecord relationship.
7
- class SimpleHasMany
9
+ class SimpleHasMany < Base
8
10
  # @param association_class_name [String] Name of the ActiveRecord class
9
11
  # e.g. in case when loading blog_post.comments it'd be "Comment"
10
12
  # @param foreign_key [Symbol] association's foreign key.
@@ -14,31 +16,18 @@ module AmsLazyRelationships
14
16
  @foreign_key = foreign_key.to_sym
15
17
  end
16
18
 
17
- # Lazy loads and yields the data when evaluating
18
- # @param record [Object] an object for which we're loading the has many data
19
- # @param block [Proc] a block to execute when data is evaluated.
20
- # Loaded data is yielded as a block argument.
21
- def load(record, &block)
22
- key = "#{record.class}/#{association_class_name}"
23
- BatchLoader.for(record).batch(key: key, replace_methods: false) do |records, loader|
24
- data = load_data(records)
25
-
26
- block&.call(data)
27
-
28
- resolve(records, data, loader)
29
- end
30
- end
31
-
32
19
  private
33
20
 
34
21
  attr_reader :association_class_name, :foreign_key
35
22
 
36
- def load_data(records)
23
+ def load_data(records, loader)
37
24
  # Some records use UUID class as id - it's safer to cast them to strings
38
25
  record_ids = records.map { |r| r.id.to_s }
39
26
  association_class_name.constantize.where(
40
27
  foreign_key => record_ids
41
- )
28
+ ).tap do |data|
29
+ resolve(records, data, loader)
30
+ end
42
31
  end
43
32
 
44
33
  def resolve(records, data, loader)
@@ -48,6 +37,10 @@ module AmsLazyRelationships
48
37
  loader.call(r, data[r.id.to_s] || [])
49
38
  end
50
39
  end
40
+
41
+ def batch_key(record)
42
+ "#{record.class}/#{association_class_name}"
43
+ end
51
44
  end
52
45
  end
53
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AmsLazyRelationships
4
- VERSION = "0.1.4"
4
+ VERSION = "0.3.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ams_lazy_relationships
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Bajena
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-06-02 00:00:00.000000000 Z
11
+ date: 2021-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: active_model_serializers
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.10.0.rc4
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.10.0.rc4
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: batch-loader
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1'
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: activerecord
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: bundler
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '1.17'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1.17'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: db-query-matchers
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -142,14 +128,14 @@ dependencies:
142
128
  requirements:
143
129
  - - "~>"
144
130
  - !ruby/object:Gem::Version
145
- version: '10.0'
131
+ version: '13.0'
146
132
  type: :development
147
133
  prerelease: false
148
134
  version_requirements: !ruby/object:Gem::Requirement
149
135
  requirements:
150
136
  - - "~>"
151
137
  - !ruby/object:Gem::Version
152
- version: '10.0'
138
+ version: '13.0'
153
139
  - !ruby/object:Gem::Dependency
154
140
  name: rspec
155
141
  requirement: !ruby/object:Gem::Requirement
@@ -238,16 +224,16 @@ dependencies:
238
224
  name: sqlite3
239
225
  requirement: !ruby/object:Gem::Requirement
240
226
  requirements:
241
- - - "~>"
227
+ - - ">="
242
228
  - !ruby/object:Gem::Version
243
- version: 1.3.6
229
+ version: '0'
244
230
  type: :development
245
231
  prerelease: false
246
232
  version_requirements: !ruby/object:Gem::Requirement
247
233
  requirements:
248
- - - "~>"
234
+ - - ">="
249
235
  - !ruby/object:Gem::Version
250
- version: 1.3.6
236
+ version: '0'
251
237
  - !ruby/object:Gem::Dependency
252
238
  name: undercover
253
239
  requirement: !ruby/object:Gem::Requirement
@@ -266,16 +252,30 @@ dependencies:
266
252
  name: with_model
267
253
  requirement: !ruby/object:Gem::Requirement
268
254
  requirements:
269
- - - "~>"
255
+ - - ">="
270
256
  - !ruby/object:Gem::Version
271
- version: '2.0'
257
+ version: '0'
272
258
  type: :development
273
259
  prerelease: false
274
260
  version_requirements: !ruby/object:Gem::Requirement
275
261
  requirements:
276
- - - "~>"
262
+ - - ">="
263
+ - !ruby/object:Gem::Version
264
+ version: '0'
265
+ - !ruby/object:Gem::Dependency
266
+ name: thread_safe
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - ">="
277
270
  - !ruby/object:Gem::Version
278
- version: '2.0'
271
+ version: '0'
272
+ type: :development
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - ">="
277
+ - !ruby/object:Gem::Version
278
+ version: '0'
279
279
  - !ruby/object:Gem::Dependency
280
280
  name: benchmark-memory
281
281
  requirement: !ruby/object:Gem::Requirement
@@ -314,17 +314,26 @@ files:
314
314
  - bin/console
315
315
  - bin/setup
316
316
  - gemfiles/.bundle/config
317
+ - gemfiles/ams_0.10.0.gemfile
317
318
  - gemfiles/ams_0.10.0.rc4.gemfile
319
+ - gemfiles/ams_0.10.10.gemfile
318
320
  - gemfiles/ams_0.10.2.gemfile
321
+ - gemfiles/ams_0.10.3.gemfile
319
322
  - gemfiles/ams_0.10.8.gemfile
323
+ - gemfiles/batch_loader_1.gemfile
324
+ - gemfiles/batch_loader_2.gemfile
320
325
  - lib/ams_lazy_relationships.rb
321
326
  - lib/ams_lazy_relationships/core.rb
322
327
  - lib/ams_lazy_relationships/core/evaluation.rb
328
+ - lib/ams_lazy_relationships/core/lazy_dig_method.rb
323
329
  - lib/ams_lazy_relationships/core/lazy_relationship_meta.rb
324
330
  - lib/ams_lazy_relationships/core/lazy_relationship_method.rb
325
331
  - lib/ams_lazy_relationships/core/relationship_wrapper_methods.rb
332
+ - lib/ams_lazy_relationships/extensions.rb
333
+ - lib/ams_lazy_relationships/extensions/reflection.rb
326
334
  - lib/ams_lazy_relationships/loaders.rb
327
335
  - lib/ams_lazy_relationships/loaders/association.rb
336
+ - lib/ams_lazy_relationships/loaders/base.rb
328
337
  - lib/ams_lazy_relationships/loaders/direct.rb
329
338
  - lib/ams_lazy_relationships/loaders/simple_belongs_to.rb
330
339
  - lib/ams_lazy_relationships/loaders/simple_has_many.rb
@@ -351,8 +360,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
351
360
  - !ruby/object:Gem::Version
352
361
  version: '0'
353
362
  requirements: []
354
- rubyforge_project:
355
- rubygems_version: 2.5.2.3
363
+ rubygems_version: 3.1.2
356
364
  signing_key:
357
365
  specification_version: 4
358
366
  summary: ActiveModel Serializers addon for eliminating N+1 queries problem from the