serega 0.36.0 → 0.37.1
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.
- checksums.yaml +4 -4
- data/README.md +85 -75
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +0 -5
- data/lib/serega/attribute_normalizer.rb +28 -70
- data/lib/serega/batch/attribute_loader.rb +35 -20
- data/lib/serega/batch/attribute_loaders.rb +64 -15
- data/lib/serega/batch/level.rb +48 -0
- data/lib/serega/config.rb +8 -10
- data/lib/serega/object_serializer.rb +7 -1
- data/lib/serega/plan.rb +9 -16
- data/lib/serega/plan_point.rb +0 -19
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +9 -54
- data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +1 -1
- data/lib/serega/plugins/if/if.rb +5 -2
- data/lib/serega/validations/attribute/check_opt_preload.rb +11 -0
- data/lib/serega/validations/check_attribute_params.rb +0 -1
- data/lib/serega.rb +38 -12
- metadata +3 -7
- data/lib/serega/plugins/activerecord_preloads/lib/active_record_objects.rb +0 -31
- data/lib/serega/utils/format_user_preloads.rb +0 -58
- data/lib/serega/utils/preload_paths.rb +0 -53
- data/lib/serega/utils/preloads_constructor.rb +0 -77
- data/lib/serega/validations/attribute/check_opt_preload_path.rb +0 -89
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca01b1c5668c5ce5963e5f2b71540dae1d023eed44c6e9e026d43e63fcead4c6
|
|
4
|
+
data.tar.gz: f526d737921efe1789751b51ff15b503b0febe2d5aafbb1a57666625a14b2228
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 643c12f65a16dbce5d05a8262df0f85fc6a5055436a65d2590f0615ccba5a9cb2d1287d4ec1262b9d0f252432262e88799b55d13ca7ce5fed252c58faf9b7a1a
|
|
7
|
+
data.tar.gz: fa8ad847f04617c8ea70cf227aca9bf9ede02c1b1449afb6547b821f09cd8a27638cb5d8904ddc576dfe12c01d5cd6991a99e7cb9fa1cf28d46d2053c7650978
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Serega Ruby Serializer
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/serega)
|
|
4
|
-
[![GitHub Actions]
|
|
4
|
+
[![GitHub Actions][build-badge]][build]
|
|
5
5
|
|
|
6
6
|
The Serega Ruby Serializer provides easy and powerful DSL to describe your
|
|
7
7
|
objects and serialize them to Hash or JSON.
|
|
@@ -428,28 +428,38 @@ end
|
|
|
428
428
|
|
|
429
429
|
## Preloads
|
|
430
430
|
|
|
431
|
-
Serega includes built-in preloads functionality that
|
|
432
|
-
`:
|
|
433
|
-
|
|
431
|
+
Serega includes built-in preloads functionality that lets you declare
|
|
432
|
+
`:preload` on attributes. It is used by the `:activerecord_preloads` plugin:
|
|
433
|
+
enable it and every association you declare is loaded once during
|
|
434
|
+
serialization, automatically, with no N+1.
|
|
434
435
|
|
|
435
436
|
Configuration options:
|
|
436
437
|
|
|
437
438
|
- `config.auto_preload` - default `false` (use `true` or `{ has_delegate_option: true, has_serializer_option: true }`)
|
|
438
|
-
- `config.hide_by_default` - default `false` (use `
|
|
439
|
+
- `config.hide_by_default` - default `false` (use `:auto` to hide attributes with preloads or batch loaders)
|
|
439
440
|
|
|
440
441
|
These options are extremely useful if you want to forget about finding
|
|
441
442
|
preloads manually.
|
|
442
443
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
`preload: :
|
|
444
|
+
The `:preload` value is passed to the preload handler exactly as written — a
|
|
445
|
+
Symbol, Array, Hash, or any custom value your ORM understands. Preloads can be
|
|
446
|
+
disabled with `preload: false` (or `preload: nil`); `preload: true` is not a
|
|
447
|
+
valid value. Automatically added preloads can be overwritten with the manually
|
|
448
|
+
specified `preload: :xxx` option.
|
|
449
|
+
|
|
450
|
+
The actual loading is performed by a handler registered with `preload_with`. The
|
|
451
|
+
`:activerecord_preloads` plugin registers one for you; to preload with another
|
|
452
|
+
ORM — or to attach data to plain non-ORM objects by your own rules — register
|
|
453
|
+
your own (see [Custom preloading](#custom-preloading)). Declaring `:preload`
|
|
454
|
+
without a registered handler raises an error, so a preload never silently does
|
|
455
|
+
nothing.
|
|
446
456
|
|
|
447
457
|
For some examples, **please read the comments in the code below**
|
|
448
458
|
|
|
449
459
|
```ruby
|
|
450
460
|
class AppSerializer < Serega
|
|
451
461
|
config.auto_preload = true
|
|
452
|
-
config.hide_by_default =
|
|
462
|
+
config.hide_by_default = :auto
|
|
453
463
|
end
|
|
454
464
|
|
|
455
465
|
class UserSerializer < AppSerializer
|
|
@@ -473,36 +483,30 @@ class AlbumSerializer < AppSerializer
|
|
|
473
483
|
attribute :images_count, delegate: { to: :album_stats }
|
|
474
484
|
end
|
|
475
485
|
|
|
476
|
-
#
|
|
477
|
-
#
|
|
478
|
-
|
|
479
|
-
#
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
#
|
|
483
|
-
|
|
484
|
-
UserSerializer.new(with: %i[followers_count comments_count]).preloads
|
|
485
|
-
# => {:user_stats=>{}}
|
|
486
|
-
|
|
487
|
-
UserSerializer.new(
|
|
488
|
-
with: [:followers_count, :comments_count, { albums: :images_count }]
|
|
489
|
-
).preloads
|
|
490
|
-
# => {:user_stats=>{}, :albums=>{:album_stats=>{}}}
|
|
486
|
+
# With `hide_by_default = :auto`, attributes that declare a preload are hidden
|
|
487
|
+
# unless explicitly requested with `:with`. When requested, their associations
|
|
488
|
+
# are preloaded automatically. For example:
|
|
489
|
+
#
|
|
490
|
+
# UserSerializer.to_h(users, with: [:followers_count, { albums: :images_count }])
|
|
491
|
+
#
|
|
492
|
+
# preloads `:user_stats` and `:albums` on the users, and `:album_stats` on the
|
|
493
|
+
# albums.
|
|
491
494
|
```
|
|
492
495
|
|
|
493
496
|
---
|
|
494
497
|
|
|
495
|
-
### SPECIFIC CASE
|
|
498
|
+
### SPECIFIC CASE: Serializing the same object in association
|
|
496
499
|
|
|
497
500
|
For example, you show your current user as "user" and use the same user object
|
|
498
501
|
to serialize "user_stats". `UserStatSerializer` relies on user fields and any
|
|
499
|
-
other user associations.
|
|
500
|
-
|
|
502
|
+
other user associations. Specify `preload: nil` so that nothing is preloaded for
|
|
503
|
+
this attribute on the "user" object — the associations declared in
|
|
504
|
+
`UserStatSerializer` are still preloaded onto the same user object.
|
|
501
505
|
|
|
502
506
|
```ruby
|
|
503
507
|
class AppSerializer < Serega
|
|
504
508
|
config.auto_preload = true
|
|
505
|
-
config.hide_by_default =
|
|
509
|
+
config.hide_by_default = :auto
|
|
506
510
|
end
|
|
507
511
|
|
|
508
512
|
class UserSerializer < AppSerializer
|
|
@@ -514,69 +518,74 @@ class UserSerializer < AppSerializer
|
|
|
514
518
|
end
|
|
515
519
|
```
|
|
516
520
|
|
|
517
|
-
|
|
521
|
+
---
|
|
518
522
|
|
|
519
|
-
|
|
520
|
-
profiles have the "avatar" association. And you decided to serialize profiles in
|
|
521
|
-
one array. You can specify `preload_path: [[:new_profile], [:old_profile]]` to
|
|
522
|
-
achieve this:
|
|
523
|
+
### Custom preloading
|
|
523
524
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
class UserSerializer < AppSerializer
|
|
530
|
-
attribute :username
|
|
531
|
-
attribute :profiles,
|
|
532
|
-
serializer: 'ProfileSerializer',
|
|
533
|
-
value: proc { |user| [user.new_profile, user.old_profile] },
|
|
534
|
-
preload: [:new_profile, :old_profile],
|
|
535
|
-
preload_path: [[:new_profile], [:old_profile]] # <--- like here
|
|
536
|
-
end
|
|
525
|
+
The `:activerecord_preloads` plugin works out of the box, but the preload
|
|
526
|
+
mechanism itself is not tied to ActiveRecord — or to ORMs at all. Register a
|
|
527
|
+
handler with `preload_with` and it is called once per preloaded attribute with
|
|
528
|
+
the gathered objects and that attribute's `:preload` value, before the
|
|
529
|
+
attribute is serialized:
|
|
537
530
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
531
|
+
```ruby
|
|
532
|
+
class UserSerializer < Serega
|
|
533
|
+
attribute :name
|
|
534
|
+
attribute :posts, serializer: PostSerializer, preload: :posts
|
|
541
535
|
|
|
542
|
-
|
|
536
|
+
# `objects` is the gathered users; `preloads` is this attribute's
|
|
537
|
+
# `:preload` value (`:posts`). `MyORM.preload` does the eager loading.
|
|
538
|
+
preload_with { |objects, preloads| MyORM.preload(objects, preloads) }
|
|
543
539
|
end
|
|
544
|
-
|
|
545
|
-
UserSerializer.new.preloads
|
|
546
|
-
# => {:new_profile=>{:avatar=>{}}, :old_profile=>{:avatar=>{}}}
|
|
547
540
|
```
|
|
548
541
|
|
|
549
|
-
|
|
542
|
+
The handler can be a block or any callable taking `(objects, preloads)`. It is
|
|
543
|
+
inherited by child serializers and can be overridden per subclass. The
|
|
544
|
+
`:preload` value reaches the handler untouched, so you can pass whatever your
|
|
545
|
+
handler expects — not necessarily something an ORM understands. The gathered
|
|
546
|
+
objects can be plain Ruby objects too: the handler decides where the data comes
|
|
547
|
+
from (another ORM, an HTTP API, a cache) and how to attach it to the objects,
|
|
548
|
+
by whatever rules you define.
|
|
549
|
+
|
|
550
|
+
The handler runs **once per preloaded attribute**, not once per distinct
|
|
551
|
+
`:preload` value. If the same `:preload` is declared on several attributes, the
|
|
552
|
+
handler is invoked once for each of them over the same objects, so it should
|
|
553
|
+
check whether the data is already loaded before loading it again. (The
|
|
554
|
+
`:activerecord_preloads` handler relies on `ActiveRecord::Associations::Preloader`,
|
|
555
|
+
which already skips associations that are loaded.)
|
|
556
|
+
|
|
557
|
+
Because you choose the `:preload` value, it doubles as a **discriminator**: give
|
|
558
|
+
two attributes different `:preload` values and branch on `preloads` inside the
|
|
559
|
+
handler — no need for the value to be a real association name.
|
|
550
560
|
|
|
551
561
|
```ruby
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
562
|
+
class PostSerializer < Serega
|
|
563
|
+
attribute :owner, serializer: UserSerializer, preload: :owner
|
|
564
|
+
attribute :author, serializer: UserSerializer, preload: :author
|
|
565
|
+
|
|
566
|
+
# Both attributes load the same person, but from different sources.
|
|
567
|
+
preload_with do |objects, preloads|
|
|
568
|
+
case preloads
|
|
569
|
+
when :owner then load_owners(objects) # e.g. from the DB
|
|
570
|
+
when :author then load_authors(objects) # e.g. from an external API
|
|
571
|
+
else MyORM.preload(objects, preloads)
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
end
|
|
557
575
|
```
|
|
558
576
|
|
|
559
|
-
In this case, we don't know if preloads defined in ImageSerializer, should be
|
|
560
|
-
preloaded to `attachment` or `blob`, so `preload_path` must be specified manually.
|
|
561
|
-
You can specify `preload_path: nil` if you are sure that there are no preloads
|
|
562
|
-
inside ImageSerializer.
|
|
563
|
-
|
|
564
577
|
---
|
|
565
578
|
|
|
566
|
-
📌 The built-in preloads functionality only allows to group preloads together
|
|
567
|
-
in single Hash, but they should be preloaded manually.
|
|
568
|
-
|
|
569
|
-
There is the [activerecord_preloads][activerecord_preloads] plugin that can be used to preload these associations automatically.
|
|
570
|
-
|
|
571
579
|
## Plugins
|
|
572
580
|
|
|
573
581
|
### Plugin :activerecord_preloads
|
|
574
582
|
|
|
575
583
|
Automatically preloads associations to serialized objects.
|
|
576
584
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
585
|
+
Every association you declare with `:preload` is loaded once during
|
|
586
|
+
serialization using `ActiveRecord::Associations::Preloader`, so there are no
|
|
587
|
+
N+1 queries. The plugin does this by registering a [`preload_with`](#custom-preloading)
|
|
588
|
+
handler; to use a different ORM, register your own instead.
|
|
580
589
|
|
|
581
590
|
```ruby
|
|
582
591
|
class AppSerializer < Serega
|
|
@@ -599,11 +608,10 @@ class AlbumSerializer < AppSerializer
|
|
|
599
608
|
end
|
|
600
609
|
|
|
601
610
|
UserSerializer.to_h(user)
|
|
602
|
-
#
|
|
611
|
+
# preloads :user_stats and :albums on the user; the AlbumSerializer level
|
|
612
|
+
# preloads :downloads on the albums
|
|
603
613
|
```
|
|
604
614
|
|
|
605
|
-
For testing purposes preloading can be done manually with `#preload_associations_to(obj)` instance method
|
|
606
|
-
|
|
607
615
|
### Plugin :root
|
|
608
616
|
|
|
609
617
|
Allows to add root key to your serialized data
|
|
@@ -1028,3 +1036,5 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
1028
1036
|
[root]: #plugin-root
|
|
1029
1037
|
[string_modifiers]: #plugin-string_modifiers
|
|
1030
1038
|
[if]: #plugin-if
|
|
1039
|
+
[build-badge]: https://github.com/aglushkov/serega/actions/workflows/main.yml/badge.svg?event=push
|
|
1040
|
+
[build]: https://github.com/aglushkov/serega/actions/workflows/main.yml
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.37.1
|
data/lib/serega/attribute.rb
CHANGED
|
@@ -33,10 +33,6 @@ class Serega
|
|
|
33
33
|
# @return [Hash, nil] Attribute :preloads option
|
|
34
34
|
attr_reader :preloads
|
|
35
35
|
|
|
36
|
-
# Attribute :preloads_path option
|
|
37
|
-
# @return [Array, nil] Attribute :preloads_path option
|
|
38
|
-
attr_reader :preloads_path
|
|
39
|
-
|
|
40
36
|
# Batch loader names required to detect attribute value
|
|
41
37
|
# @return [Array<Symbol>] Batch loader names
|
|
42
38
|
attr_reader :batch_loaders
|
|
@@ -143,7 +139,6 @@ class Serega
|
|
|
143
139
|
@hide = normalizer.hide
|
|
144
140
|
@serializer = normalizer.serializer
|
|
145
141
|
@preloads = normalizer.preloads
|
|
146
|
-
@preloads_path = normalizer.preloads_path
|
|
147
142
|
@batch_loaders = normalizer.batch_loaders
|
|
148
143
|
end
|
|
149
144
|
end
|
|
@@ -9,6 +9,10 @@ class Serega
|
|
|
9
9
|
# AttributeNormalizer instance methods
|
|
10
10
|
#
|
|
11
11
|
module AttributeNormalizerInstanceMethods
|
|
12
|
+
# Identity loader that routes relation/preload attributes through the batch
|
|
13
|
+
# mechanism. Its result is never read — the attribute's value block still
|
|
14
|
+
# computes the value.
|
|
15
|
+
AUTO_BATCH_LOADER = proc { |records| records.to_h { |record| [record, record] } }
|
|
12
16
|
# Attribute initial params
|
|
13
17
|
# @return [Hash] Attribute initial params
|
|
14
18
|
attr_reader :init_name, :init_opts, :init_block
|
|
@@ -119,17 +123,6 @@ class Serega
|
|
|
119
123
|
@preloads = prepare_preloads
|
|
120
124
|
end
|
|
121
125
|
|
|
122
|
-
#
|
|
123
|
-
# Shows normalized preloads_path for current attribute
|
|
124
|
-
#
|
|
125
|
-
# @return [Array] normalized preloads_path of current attribute
|
|
126
|
-
#
|
|
127
|
-
def preloads_path
|
|
128
|
-
return @preloads_path if instance_variable_defined?(:@preloads_path)
|
|
129
|
-
|
|
130
|
-
@preloads_path = prepare_preloads_path
|
|
131
|
-
end
|
|
132
|
-
|
|
133
126
|
# Shows specified batch loaders names
|
|
134
127
|
# @return [Array<Symbol>] specified serializer
|
|
135
128
|
#
|
|
@@ -158,14 +151,8 @@ class Serega
|
|
|
158
151
|
return hide if (hide == true) || (hide == false)
|
|
159
152
|
|
|
160
153
|
hide_setting = config.hide_by_default
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
when true
|
|
164
|
-
return true
|
|
165
|
-
when Array
|
|
166
|
-
return true if hide_setting.include?(:preload) && preloads # hide when attribute has `:preload` option
|
|
167
|
-
return true if hide_setting.include?(:batch) && batch_loaders.any? # hide when attribute has `:batch` option
|
|
168
|
-
end
|
|
154
|
+
return true if hide_setting == true
|
|
155
|
+
return true if hide_setting == :auto && (preloads || init_opts.key?(:batch))
|
|
169
156
|
|
|
170
157
|
# Return nil for undefined value which means "not hide" but allows
|
|
171
158
|
# to change this value by plugins
|
|
@@ -207,10 +194,16 @@ class Serega
|
|
|
207
194
|
|
|
208
195
|
def prepare_batch_loaders
|
|
209
196
|
batch_opt = init_opts[:batch]
|
|
197
|
+
return explicit_batch_loaders(batch_opt) if batch_opt
|
|
198
|
+
return FROZEN_EMPTY_ARRAY unless serializer || preloads
|
|
210
199
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
200
|
+
loader_name = :"__auto_batch_#{name}__"
|
|
201
|
+
self.class.serializer_class.batch(loader_name, AUTO_BATCH_LOADER)
|
|
202
|
+
[loader_name]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def explicit_batch_loaders(batch_opt)
|
|
206
|
+
if batch_opt == true || batch_opt.respond_to?(:call)
|
|
214
207
|
[name]
|
|
215
208
|
elsif batch_opt.is_a?(Symbol)
|
|
216
209
|
[batch_opt]
|
|
@@ -237,64 +230,29 @@ class Serega
|
|
|
237
230
|
AttributeValueResolvers::DelegateResolver.get(delegate_to, key_method_name, allow_nil)
|
|
238
231
|
end
|
|
239
232
|
|
|
240
|
-
# Prepares preloads
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
#
|
|
233
|
+
# Prepares preloads for this attribute.
|
|
234
|
+
#
|
|
235
|
+
# The value is passed through as provided (Symbol, Array, Hash, or any
|
|
236
|
+
# custom value an ORM understands) — it is the data handed to the
|
|
237
|
+
# serializer's `preload_with` handler.
|
|
238
|
+
#
|
|
239
|
+
# @return [Object, nil] preloads as provided, or nil when the attribute has none
|
|
245
240
|
def prepare_preloads
|
|
246
|
-
preload
|
|
241
|
+
# Explicit preload option (false or nil disables preloading)
|
|
242
|
+
return init_opts[:preload] || nil if init_opts.key?(:preload)
|
|
247
243
|
|
|
248
|
-
#
|
|
249
|
-
if init_opts.key?(:preload)
|
|
250
|
-
return nil unless preload # return nil when false or nil
|
|
251
|
-
return SeregaUtils::FormatUserPreloads.call(preload)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
# Auto-preload for delegate
|
|
244
|
+
# Auto-preload the delegated association
|
|
255
245
|
if config.auto_preload.fetch(:has_delegate_option) && init_opts[:delegate]
|
|
256
|
-
|
|
257
|
-
return SeregaUtils::FormatUserPreloads.call(delegate_to)
|
|
246
|
+
return init_opts[:delegate][:to]
|
|
258
247
|
end
|
|
259
248
|
|
|
260
|
-
# Auto-preload
|
|
249
|
+
# Auto-preload the nested serializer's association
|
|
261
250
|
if config.auto_preload.fetch(:has_serializer_option) && init_opts[:serializer] && !init_opts.key?(:batch)
|
|
262
|
-
return
|
|
251
|
+
return method_name
|
|
263
252
|
end
|
|
264
253
|
|
|
265
254
|
nil
|
|
266
255
|
end
|
|
267
|
-
|
|
268
|
-
def prepare_preloads_path
|
|
269
|
-
path = init_opts.fetch(:preload_path) { default_preload_path(preloads) }
|
|
270
|
-
|
|
271
|
-
if path && path[0].is_a?(Array)
|
|
272
|
-
prepare_many_preload_paths(path)
|
|
273
|
-
else
|
|
274
|
-
prepare_one_preload_path(path)
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def prepare_one_preload_path(path)
|
|
279
|
-
return unless path
|
|
280
|
-
|
|
281
|
-
case path
|
|
282
|
-
when Array
|
|
283
|
-
path.map(&:to_sym).freeze
|
|
284
|
-
else
|
|
285
|
-
[path.to_sym].freeze
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def prepare_many_preload_paths(paths)
|
|
290
|
-
paths.map { |path| prepare_one_preload_path(path) }.freeze
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def default_preload_path(preloads)
|
|
294
|
-
return FROZEN_EMPTY_ARRAY if !preloads || preloads.empty?
|
|
295
|
-
|
|
296
|
-
[preloads.keys.first]
|
|
297
|
-
end
|
|
298
256
|
end
|
|
299
257
|
|
|
300
258
|
extend Serega::SeregaHelpers::SerializerClassHelper
|
|
@@ -10,9 +10,10 @@ class Serega
|
|
|
10
10
|
# Instance methods for AttributeLoader
|
|
11
11
|
module InstanceMethods
|
|
12
12
|
# @param point [SeregaPlanPoint]
|
|
13
|
-
|
|
13
|
+
# @param level [SeregaBatch::Level] Shared objects and results for this point's plan level
|
|
14
|
+
def initialize(point, level)
|
|
14
15
|
@point = point
|
|
15
|
-
@
|
|
16
|
+
@level = level
|
|
16
17
|
@serialized_object_attachers = []
|
|
17
18
|
end
|
|
18
19
|
|
|
@@ -21,35 +22,51 @@ class Serega
|
|
|
21
22
|
# @param attacher [Proc] Proc that attaches found values to originated attributes
|
|
22
23
|
# @return [void]
|
|
23
24
|
def store(object, attacher)
|
|
24
|
-
objects << object
|
|
25
25
|
serialized_object_attachers << [object, attacher]
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
28
|
+
# Preloads this attribute's associations onto the level's objects, once for
|
|
29
|
+
# the attribute (not per named batch loader), via the serializer's registered
|
|
30
|
+
# `preload_with` handler. Raises when `:preload` is declared but no handler
|
|
31
|
+
# exists, so a declared preload never silently does nothing.
|
|
30
32
|
# @return [void]
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
def preload_associations
|
|
34
|
+
preloads = point.attribute.preloads
|
|
35
|
+
return unless preloads
|
|
34
36
|
|
|
35
|
-
point.
|
|
36
|
-
|
|
37
|
+
handler = point.class.serializer_class.preload_with
|
|
38
|
+
unless handler
|
|
39
|
+
raise SeregaError, "The :preload option requires a preload handler. Register one with `preload_with` (the :activerecord_preloads plugin does this for you)."
|
|
37
40
|
end
|
|
38
41
|
|
|
42
|
+
handler.call(level.objects, preloads)
|
|
43
|
+
rescue => error
|
|
44
|
+
reraise_with_serialized_attribute_details(error)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Loads a single named batch for this level's objects. Caching across
|
|
48
|
+
# attributes that reuse the loader is handled by the Level.
|
|
49
|
+
# @param loader [SeregaBatch::Loader] Named batch loader
|
|
50
|
+
# @return [Object] Loaded batch values
|
|
51
|
+
def load_batch(loader)
|
|
52
|
+
level.load(loader)
|
|
53
|
+
rescue => error
|
|
54
|
+
reraise_with_serialized_attribute_details(error)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Attaches already loaded batch values to every stored object.
|
|
58
|
+
# @param batches [Hash] Loaded values grouped by batch loader name
|
|
59
|
+
# @return [void]
|
|
60
|
+
def attach(batches)
|
|
39
61
|
serialized_object_attachers.each do |object, attacher|
|
|
40
62
|
attacher.call(object, batches)
|
|
41
63
|
end
|
|
42
64
|
end
|
|
43
65
|
|
|
44
|
-
|
|
66
|
+
attr_reader :point
|
|
67
|
+
attr_reader :level
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
# - plugin :activerecord_preloads (preloads associations to found AR objects)
|
|
48
|
-
def load_one(serializer_class, batch_loader_name, context)
|
|
49
|
-
serializer_class.batch_loaders[batch_loader_name].load(objects, context)
|
|
50
|
-
rescue => error
|
|
51
|
-
reraise_with_serialized_attribute_details(error)
|
|
52
|
-
end
|
|
69
|
+
private
|
|
53
70
|
|
|
54
71
|
def reraise_with_serialized_attribute_details(error)
|
|
55
72
|
raise error.exception(<<~MESSAGE.strip)
|
|
@@ -58,8 +75,6 @@ class Serega
|
|
|
58
75
|
MESSAGE
|
|
59
76
|
end
|
|
60
77
|
|
|
61
|
-
attr_reader :point
|
|
62
|
-
attr_reader :objects
|
|
63
78
|
attr_reader :serialized_object_attachers
|
|
64
79
|
end
|
|
65
80
|
|
|
@@ -13,9 +13,24 @@ class Serega
|
|
|
13
13
|
#
|
|
14
14
|
# Initializes new batch loaders
|
|
15
15
|
#
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# @param context [Hash] Serialization context, constant for the whole run
|
|
17
|
+
#
|
|
18
|
+
def initialize(context)
|
|
19
|
+
@context = context
|
|
20
|
+
@attribute_loaders = []
|
|
18
21
|
@point_index = {}.compare_by_identity
|
|
22
|
+
@levels = {}.compare_by_identity
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Gathers a chunk of objects serialized at a plan level, so every attribute at
|
|
26
|
+
# that level shares one set of objects to batch load against.
|
|
27
|
+
#
|
|
28
|
+
# @param plan [SeregaPlan] Plan serializing these objects
|
|
29
|
+
# @param objects [Array] Objects being serialized at this level
|
|
30
|
+
#
|
|
31
|
+
# @return [void]
|
|
32
|
+
def add_objects(plan, objects)
|
|
33
|
+
level_for(plan).add_objects(objects)
|
|
19
34
|
end
|
|
20
35
|
|
|
21
36
|
# Remembers data for batch serialization:
|
|
@@ -26,34 +41,68 @@ class Serega
|
|
|
26
41
|
#
|
|
27
42
|
# @return [void]
|
|
28
43
|
def remember(point, object, attacher)
|
|
29
|
-
|
|
44
|
+
attribute_loader = point_index[point]
|
|
30
45
|
|
|
31
|
-
unless
|
|
46
|
+
unless attribute_loader
|
|
32
47
|
serializer_class = point.class.serializer_class
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
point_index[point] =
|
|
48
|
+
attribute_loader = serializer_class::SeregaBatchAttributeLoader.new(point, level_for(point.plan))
|
|
49
|
+
attribute_loaders << attribute_loader
|
|
50
|
+
point_index[point] = attribute_loader
|
|
36
51
|
end
|
|
37
52
|
|
|
38
|
-
|
|
53
|
+
attribute_loader.store(object, attacher)
|
|
39
54
|
end
|
|
40
55
|
|
|
41
56
|
#
|
|
42
|
-
# Loads all registered batches and
|
|
57
|
+
# Loads all registered batches and attaches loaded values.
|
|
43
58
|
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
# Iterates over each loader including loaders added during iteration
|
|
60
|
+
# (child serializers discovered inside a batch flush add new loaders).
|
|
61
|
+
def load_all
|
|
62
|
+
i = 0
|
|
63
|
+
while i < attribute_loaders.size
|
|
64
|
+
attribute_loader = attribute_loaders[i]
|
|
65
|
+
attribute_loader.attach(load_batches(attribute_loader))
|
|
66
|
+
i += 1
|
|
47
67
|
end
|
|
48
68
|
end
|
|
49
69
|
|
|
50
70
|
private
|
|
51
71
|
|
|
52
|
-
#
|
|
53
|
-
attr_reader :
|
|
72
|
+
# Serialization context shared by every level
|
|
73
|
+
attr_reader :context
|
|
54
74
|
|
|
55
|
-
# keeps
|
|
75
|
+
# keeps all AttributeLoader instances
|
|
76
|
+
attr_reader :attribute_loaders
|
|
77
|
+
|
|
78
|
+
# keeps tracking of already added points
|
|
56
79
|
attr_reader :point_index
|
|
80
|
+
|
|
81
|
+
# keeps one Level per plan (compare_by_identity: plan instance identifies a level)
|
|
82
|
+
attr_reader :levels
|
|
83
|
+
|
|
84
|
+
def level_for(plan)
|
|
85
|
+
levels[plan] ||= Level.new(context)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Builds the { batch_loader_name => loaded_values } hash for one attribute.
|
|
89
|
+
#
|
|
90
|
+
# Loading is delegated to the attribute's Level, which loads each named batch
|
|
91
|
+
# once for the whole level: attributes sharing a loader over the same objects
|
|
92
|
+
# reuse a single result, deeper levels load separately.
|
|
93
|
+
def load_batches(attribute_loader)
|
|
94
|
+
attribute_loader.preload_associations
|
|
95
|
+
|
|
96
|
+
point = attribute_loader.point
|
|
97
|
+
serializer_class = point.class.serializer_class
|
|
98
|
+
|
|
99
|
+
batches = {}
|
|
100
|
+
point.batch_loaders.each do |batch_loader_name|
|
|
101
|
+
loader = serializer_class.batch_loaders[batch_loader_name]
|
|
102
|
+
batches[batch_loader_name] = attribute_loader.load_batch(loader)
|
|
103
|
+
end
|
|
104
|
+
batches
|
|
105
|
+
end
|
|
57
106
|
end
|
|
58
107
|
end
|
|
59
108
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Batch feature main module
|
|
6
|
+
#
|
|
7
|
+
module SeregaBatch
|
|
8
|
+
#
|
|
9
|
+
# Objects serialized at one plan level, together with their loaded batch results.
|
|
10
|
+
#
|
|
11
|
+
# Every attribute serialized at the same level shares one Level, so a named batch
|
|
12
|
+
# loader reused by several of them runs once for the whole set of objects. The same
|
|
13
|
+
# loader over a different level (deeper nesting) uses a different Level and loads
|
|
14
|
+
# again.
|
|
15
|
+
#
|
|
16
|
+
class Level
|
|
17
|
+
# @return [Array] Objects gathered for this level
|
|
18
|
+
attr_reader :objects
|
|
19
|
+
|
|
20
|
+
# @param context [Hash] Serialization context
|
|
21
|
+
def initialize(context)
|
|
22
|
+
@context = context
|
|
23
|
+
@objects = []
|
|
24
|
+
@results = {}.compare_by_identity
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Adds a chunk of serialized objects to this level.
|
|
28
|
+
# @param objects [Array] Objects being serialized at this level
|
|
29
|
+
# @return [void]
|
|
30
|
+
def add_objects(objects)
|
|
31
|
+
@objects.concat(objects)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Loads and returns values for the given batch loader, once per loader.
|
|
35
|
+
#
|
|
36
|
+
# Freezes the gathered objects on the first load: all of a level's objects
|
|
37
|
+
# are added before any of its batches load, so sealing the set here guards
|
|
38
|
+
# against a later stray `add_objects` and against a loader mutating it.
|
|
39
|
+
#
|
|
40
|
+
# @param loader [SeregaBatch::Loader] Named batch loader
|
|
41
|
+
# @return [Object] Loaded values
|
|
42
|
+
def load(loader)
|
|
43
|
+
@objects.freeze
|
|
44
|
+
@results[loader] ||= loader.load(@objects, @context)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|