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