sequel-packer 0.3.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +424 -68
- data/lib/sequel/packer.rb +269 -54
- data/lib/sequel/packer/eager_hash.rb +0 -2
- data/lib/sequel/packer/eager_loading.rb +102 -0
- data/lib/sequel/packer/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8782014d5d4fd8a6da590674ebc1d8d7a248f13ab80a1c571a0036f5a158af4f
|
4
|
+
data.tar.gz: d58397494def47b14aba23ce8f3dd2006936116e1273016f5bafb8e22c20809a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a30ae3faee56c47a4da2efcddb0a09cb5cbf892451968d010fddd2c3b937b59caadeb3128c78c4ca94d8cfaf0d2741755459fb1729c4f6fbb9b97f2787289201
|
7
|
+
data.tar.gz: 109e270732bec0bb7311f5d5f5a7e5f9c6f5f91e4a685bbdec5e20f28a38ef92093fd64d56067e6e64a3a718c2076df700b82e401f2004898aee846dc6cd879b
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,30 @@
|
|
1
|
+
### 1.0.1 (2021-08-02)
|
2
|
+
|
3
|
+
* Update internal method call to remove "Using the last argument as
|
4
|
+
keyword parameters is deprecated" warning.
|
5
|
+
|
6
|
+
### 1.0.0 (2020-05-18)
|
7
|
+
|
8
|
+
* Version 1.0.0 release! No changes since 0.5.0 except some small changes to the
|
9
|
+
README.
|
10
|
+
|
11
|
+
### 0.5.0 (2020-05-17)
|
12
|
+
|
13
|
+
* Add `**context` argument to `#pack` method, exposed as `@context` in blocks
|
14
|
+
passed to `field` and `trait`.
|
15
|
+
* Add `::with_context(&block)`, for accessing `@context` to use in additional
|
16
|
+
DSL calls, or modify data fetching.
|
17
|
+
* Update README some re-organization and table of contents.
|
18
|
+
|
19
|
+
### 0.4.0 (2020-05-17)
|
20
|
+
|
21
|
+
* **_BREAKING CHANGE:_** `#pack_models` and `#pack_model` have been changed to
|
22
|
+
private methods. In their place `Sequel::Packer#pack` has been changed to
|
23
|
+
accept a dataset, an array of models or a single model, while still ensuring
|
24
|
+
eager loading takes place.
|
25
|
+
* Add `self.precompute(&block)` for performing bulk computations outside of
|
26
|
+
Packer paradigm.
|
27
|
+
|
1
28
|
### 0.3.0 (2020-05-14)
|
2
29
|
|
3
30
|
* Add `self.set_association_packer(association, packer_class, *traits)` and
|
data/README.md
CHANGED
@@ -1,24 +1,138 @@
|
|
1
1
|
# Sequel::Packer
|
2
2
|
|
3
|
-
`Sequel::Packer` is a Ruby
|
4
|
-
ORM
|
3
|
+
`Sequel::Packer` is a Ruby serialization library to be used with the [Sequel
|
4
|
+
ORM](https://github.com/jeremyevans/sequel) with the following qualities:
|
5
5
|
|
6
|
-
*
|
6
|
+
* **Declarative:** Define the shape of your serialized data with a simple,
|
7
7
|
straightforward DSL.
|
8
|
-
*
|
8
|
+
* **Flexible:** Certain contexts require different data. Packers provide an easy
|
9
9
|
way to opt-in to serializing certain data only when you need it. The library
|
10
10
|
also provides convenient escape hatches when you need to do something not
|
11
11
|
explicitly supported by the API.
|
12
|
-
*
|
12
|
+
* **Reusable:** The Packer library naturally composes well with itself. Nested
|
13
13
|
data can be serialized in the same way no matter what endpoint it's fetched
|
14
14
|
from.
|
15
|
-
*
|
15
|
+
* **Efficient:** When not using Sequel's
|
16
16
|
[`TacticalEagerLoading`](https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/TacticalEagerLoading.html)
|
17
17
|
plugin, the Packer library will intelligently determine which associations
|
18
18
|
and nested associations it needs to eager load in order to avoid any N+1 query
|
19
19
|
issues.
|
20
20
|
|
21
|
-
##
|
21
|
+
## Example
|
22
|
+
|
23
|
+
`Sequel::Packer` uses your existing `Sequel::Model` declarations and leverages
|
24
|
+
the use of associations to efficiently serialize data.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class User < Sequel::Model(:users)
|
28
|
+
one_to_many :posts
|
29
|
+
end
|
30
|
+
class Post < Sequel::Model(:posts); end
|
31
|
+
```
|
32
|
+
|
33
|
+
Packer definitions use a simple domain-specific language (DSL) to declare which
|
34
|
+
fields to serialize:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class PostPacker < Sequel::Packer
|
38
|
+
model Post
|
39
|
+
|
40
|
+
field :id
|
41
|
+
field :title
|
42
|
+
|
43
|
+
trait :truncated_content do
|
44
|
+
field :truncated_content do |post|
|
45
|
+
post.content[0..Post::PREVIEW_LENGTH]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class UserPacker < Sequel::Packer
|
51
|
+
model User
|
52
|
+
|
53
|
+
field :id
|
54
|
+
field :name
|
55
|
+
|
56
|
+
trait :posts do
|
57
|
+
field :posts, PostPacker, :truncated_content
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
Once defined, Packers are easy to use; just call `.pack` and pass in a Sequel
|
63
|
+
dataset, an array of models, or a single model, and get back Ruby hashes.
|
64
|
+
From there you can simply call `to_json` on the result!
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
UserPacker.pack(User.dataset)
|
68
|
+
=> [
|
69
|
+
{id: 1, name: 'Paul'},
|
70
|
+
{id: 2, name: 'Julius'},
|
71
|
+
...
|
72
|
+
]
|
73
|
+
|
74
|
+
UserPacker.pack(User[1], :posts)
|
75
|
+
=> {
|
76
|
+
id: 1,
|
77
|
+
name: 'Paul',
|
78
|
+
posts: [
|
79
|
+
{
|
80
|
+
id: 15,
|
81
|
+
title: 'Announcing Sequel::Packer!',
|
82
|
+
truncated_content: 'Sequel::Packer is a new gem...',
|
83
|
+
},
|
84
|
+
{
|
85
|
+
id: 21,
|
86
|
+
title: 'Postgres Internals',
|
87
|
+
truncated_content: 'I never quite understood autovacuum...',
|
88
|
+
},
|
89
|
+
...
|
90
|
+
],
|
91
|
+
}
|
92
|
+
```
|
93
|
+
|
94
|
+
## Contents
|
95
|
+
|
96
|
+
- [Example](#example)
|
97
|
+
- [Getting Started](#getting-started)
|
98
|
+
- [Installation](#installation)
|
99
|
+
- [Example Schema](#example-schema)
|
100
|
+
- [Basic Fields](#basic-fields)
|
101
|
+
- [Packing Associations by Nesting Packers](#packing-associations-by-nesting-packers)
|
102
|
+
- [Traits](#traits)
|
103
|
+
- [API Reference](#api-reference)
|
104
|
+
- [Using a Packer](#using-a-packer)
|
105
|
+
- [Defining a Packer](#defining-a-packer)
|
106
|
+
- [`self.model(sequel_model_class)`](#selfmodelsequel_model_class)
|
107
|
+
- [`self.field(column_name)` (or `self.field(method_name)`)](#selffieldcolumn_name-or-selffieldmethod_name)
|
108
|
+
- [`self.field(key, &block)`](#selffieldkey-block)
|
109
|
+
- [`self.field(association, subpacker, *traits)`](#selffieldassociation-subpacker-traits)
|
110
|
+
- [`self.field(&block)`](#selffieldblock)
|
111
|
+
- [`self.trait(trait_name, &block)`](#selftraittrait_name-block)
|
112
|
+
- [`self.eager(*associations)`](#selfeagerassociations)
|
113
|
+
- [`self.set_association_packer(association, subpacker, *traits)`](#selfset_association_packerassociation-subpacker-traits)
|
114
|
+
- [`self.pack_association(association, models)`](#selfpack_associationassociation-models)
|
115
|
+
- [`self.precompute(&block)`](#selfprecomputeblock)
|
116
|
+
- [Context](#context)
|
117
|
+
- [`self.with_context(&block)`](#selfwith_contextblock)
|
118
|
+
- [Potential Future Functionality](#potential-future-functionality)
|
119
|
+
- [Automatically Generated Type Declarations](#automatically-generated-type-declarations)
|
120
|
+
- [Lifecycle Hooks](#lifecycle-hooks)
|
121
|
+
- [Less Data Fetching](#less-data-fetching)
|
122
|
+
- [Other Enhancements](#other-enhancements)
|
123
|
+
- [Contributing](#contributing)
|
124
|
+
- [Development](#development)
|
125
|
+
- [Releases](#releases)
|
126
|
+
- [Attribution](#attribution)
|
127
|
+
- [License](#license)
|
128
|
+
|
129
|
+
## Getting Started
|
130
|
+
|
131
|
+
This section will explain the basic use of `Sequel::Packer`. Check out the [API
|
132
|
+
Reference](#api-reference) for an exhaustive coverage of the API and more
|
133
|
+
detailed documentation.
|
134
|
+
|
135
|
+
### Installation
|
22
136
|
|
23
137
|
Add this line to your application's Gemfile:
|
24
138
|
|
@@ -34,9 +148,9 @@ Or install it yourself as:
|
|
34
148
|
|
35
149
|
$ gem install sequel-packer
|
36
150
|
|
37
|
-
|
151
|
+
### Example Schema
|
38
152
|
|
39
|
-
|
153
|
+
Most of the following examples will use the following database schema:
|
40
154
|
|
41
155
|
```ruby
|
42
156
|
DB.create_table(:users) do
|
@@ -72,7 +186,7 @@ end
|
|
72
186
|
### Basic Fields
|
73
187
|
|
74
188
|
Suppose an endpoint wants to fetch all the ten most recent comments by a user.
|
75
|
-
After validating the user id, we end up with the Sequel dataset
|
189
|
+
After validating the user id, we end up with the Sequel dataset representing the
|
76
190
|
data we want to return:
|
77
191
|
|
78
192
|
```ruby
|
@@ -97,7 +211,7 @@ end
|
|
97
211
|
This can then be used as follows:
|
98
212
|
|
99
213
|
```ruby
|
100
|
-
CommentPacker.
|
214
|
+
CommentPacker.pack(recent_comments)
|
101
215
|
=> [
|
102
216
|
{id: 536, content: "Great post, man!"},
|
103
217
|
{id: 436, content: "lol"},
|
@@ -105,10 +219,10 @@ CommentPacker.new.pack(recent_comments)
|
|
105
219
|
]
|
106
220
|
```
|
107
221
|
|
108
|
-
### Packing
|
222
|
+
### Packing Associations by Nesting Packers
|
109
223
|
|
110
224
|
Now, suppose that we want to fetch a post and all of its comments. We can do
|
111
|
-
this by defining another packer for Post that uses the CommentPacker
|
225
|
+
this by defining another packer for `Post` that uses the `CommentPacker`:
|
112
226
|
|
113
227
|
```ruby
|
114
228
|
class PostPacker < Sequel::Packer
|
@@ -121,14 +235,15 @@ class PostPacker < Sequel::Packer
|
|
121
235
|
end
|
122
236
|
```
|
123
237
|
|
124
|
-
Since `post.comments` is an array of Sequel::Models and not a primitive value,
|
125
|
-
we must tell the Packer how to serialize them using another packer.
|
126
|
-
|
238
|
+
Since `post.comments` is an array of `Sequel::Models` and not a primitive value,
|
239
|
+
we must tell the Packer how to serialize them using another packer. The second
|
240
|
+
argument in `field :comments, CommentPacker` tells the `PostPacker` to use the
|
241
|
+
pack those comments using the `CommentPacker`.
|
127
242
|
|
128
243
|
We can then use this as follows:
|
129
244
|
|
130
245
|
```ruby
|
131
|
-
PostPacker.
|
246
|
+
PostPacker.pack(Post[validated_id])
|
132
247
|
=> [
|
133
248
|
{
|
134
249
|
id: 682,
|
@@ -158,8 +273,8 @@ end
|
|
158
273
|
```
|
159
274
|
|
160
275
|
We could now define a new packer, `CommentWithAuthorPacker`, and use that in the
|
161
|
-
PostPacker instead, but then we'd have to redeclare all the other fields we
|
162
|
-
on a packed Comment
|
276
|
+
`PostPacker` instead, but then we'd have to redeclare all the other fields we
|
277
|
+
want on a packed `Comment`:
|
163
278
|
|
164
279
|
```ruby
|
165
280
|
class CommentWithAuthorPacker < Sequel::Packer
|
@@ -184,7 +299,7 @@ end
|
|
184
299
|
Declaring these fields in two places could cause them to get out of sync
|
185
300
|
as more fields are added. Instead, we will use a _trait_. A _trait_ is a
|
186
301
|
way to define a set of fields that we only want to pack sometimes. Instead
|
187
|
-
of defining a totally new packer, we can extend the CommentPacker as follows:
|
302
|
+
of defining a totally new packer, we can extend the `CommentPacker` as follows:
|
188
303
|
|
189
304
|
```ruby
|
190
305
|
class CommentPacker < Sequel::Packer
|
@@ -199,18 +314,18 @@ class CommentPacker < Sequel::Packer
|
|
199
314
|
end
|
200
315
|
```
|
201
316
|
|
202
|
-
To use a trait, simply pass it in when
|
317
|
+
To use a trait, simply pass it in when calling `pack`:
|
203
318
|
|
204
319
|
```ruby
|
205
320
|
# Without the trait
|
206
|
-
CommentPacker.
|
321
|
+
CommentPacker.pack(Comment.dataset)
|
207
322
|
=> [
|
208
323
|
{id: 536, content: "Great post, man!"},
|
209
324
|
...
|
210
325
|
]
|
211
326
|
|
212
327
|
# With the trait
|
213
|
-
CommentPacker.
|
328
|
+
CommentPacker.pack(Comment.dataset, :author)
|
214
329
|
=> [
|
215
330
|
{
|
216
331
|
id: 536,
|
@@ -249,13 +364,64 @@ Custom packers are written by creating subclasses of `Sequel::Packer`. This
|
|
249
364
|
class defines a DSL for declaring how a Sequel Model will be converted into a
|
250
365
|
plain Ruby hash.
|
251
366
|
|
252
|
-
###
|
367
|
+
### Using a Packer
|
368
|
+
|
369
|
+
Using a Packer is dead simple. There's a single class method:
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
self.pack(data, *traits, **context)
|
373
|
+
```
|
374
|
+
|
375
|
+
`data` can be in the form of a Sequel dataset, an array of Sequel models, or
|
376
|
+
a single Sequel model. No matter which form the data is passed in, the Packer
|
377
|
+
class will ensure nested data is efficiently loaded.
|
378
|
+
|
379
|
+
To pack additional fields defined in a trait, pass the name of the trait as an
|
380
|
+
additional argument, e.g., `UserPacker.pack(users, :recent_posts)` to include
|
381
|
+
recent posts with each user.
|
382
|
+
|
383
|
+
Finally, additional context can be provided to the Packer by passing additional
|
384
|
+
keyword arguments to `pack`. This context is handled opaquely by the Packer, but
|
385
|
+
it can be accessed in the blocks passed to `field` declarations. Common uses of
|
386
|
+
`context` include passing in the current user making a request, or passing in
|
387
|
+
additional precomputed data.
|
388
|
+
|
389
|
+
The implementation of `pack` is very simple. It creates an instance of a Packer,
|
390
|
+
by passing in the traits and the context, then calls `pack` on that instance,
|
391
|
+
and passes in the data:
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
def self.pack(data, *traits, **context)
|
395
|
+
return nil if !data # small easy optimization to avoid unnecessary work
|
396
|
+
new(*traits, **context).pack(data)
|
397
|
+
end
|
398
|
+
```
|
399
|
+
|
400
|
+
It simply combines a constructor and single exposed instance method:
|
401
|
+
|
402
|
+
#### `initialize(*traits, **context)`
|
403
|
+
|
404
|
+
#### `pack(data)`
|
405
|
+
|
406
|
+
One instantiated, the same Packer could be used to pack data multiple times.
|
407
|
+
This is unlikely to be needed, but the functionality is there.
|
408
|
+
|
409
|
+
### Defining a Packer
|
410
|
+
|
411
|
+
#### `self.model(sequel_model_class)`
|
253
412
|
|
254
413
|
The beginning of each Packer class must begin with `model MySequelModel`, which
|
255
414
|
specifies which Sequel Model this Packer class will serialize. This is mostly
|
256
|
-
to catch certain errors at load time, rather than at run time
|
415
|
+
to catch certain errors at load time, rather than at run time:
|
416
|
+
|
417
|
+
```ruby
|
418
|
+
class UserPacker < Sequel::Packer
|
419
|
+
model User
|
420
|
+
...
|
421
|
+
end
|
422
|
+
```
|
257
423
|
|
258
|
-
|
424
|
+
#### `self.field(column_name)` (or `self.field(method_name)`)
|
259
425
|
|
260
426
|
Defining the shape of the outputted data is done using the `field` method, which
|
261
427
|
exists in four different variants. This first variant is the simplest. It simply
|
@@ -283,7 +449,7 @@ Then when `User.create(first_name: "Paul", last_name: "Martinez")` gets packed
|
|
283
449
|
with `field :full_name` specified, the outputted hash will contain
|
284
450
|
`full_name: "Paul Martinez"`.
|
285
451
|
|
286
|
-
|
452
|
+
#### `self.field(key, &block)`
|
287
453
|
|
288
454
|
A block can be passed to `field` to perform arbitrary computation and store the
|
289
455
|
result under the specified `key`. The block will be passed the model as a single
|
@@ -309,7 +475,7 @@ class MyPacker < Sequel::Packer
|
|
309
475
|
end
|
310
476
|
```
|
311
477
|
|
312
|
-
|
478
|
+
#### `self.field(association, subpacker, *traits)`
|
313
479
|
|
314
480
|
A Sequel association (defined in the model file using `one_to_many`, or
|
315
481
|
`many_to_one`, etc.), can be packed using another Packer class, possibly with
|
@@ -317,15 +483,16 @@ multiple traits specified. A similar output could be generated by doing:
|
|
317
483
|
|
318
484
|
```ruby
|
319
485
|
field :association do |model|
|
320
|
-
|
486
|
+
subpacker.pack(model.association_dataset, *traits)
|
321
487
|
end
|
322
488
|
```
|
323
489
|
|
324
|
-
|
325
|
-
|
326
|
-
|
490
|
+
This form is very inefficient though, because it would result in a new subpacker
|
491
|
+
getting instantiated for every packed model. Additionally, unless the subpacker
|
492
|
+
is declared up-front, the Packer won't know to eager load that association,
|
493
|
+
potentially resulting in many unnecessary database queries.
|
327
494
|
|
328
|
-
|
495
|
+
#### `self.field(&block)`
|
329
496
|
|
330
497
|
Passing a block but no `key` to `field` allows for arbitrary manipulation of the
|
331
498
|
packed hash. The block will be passed the model and the partially packed hash.
|
@@ -338,7 +505,7 @@ field do |model, hash|
|
|
338
505
|
end
|
339
506
|
```
|
340
507
|
|
341
|
-
|
508
|
+
#### `self.trait(trait_name, &block)`
|
342
509
|
|
343
510
|
Define optional serialization behavior by defining additional fields within a
|
344
511
|
`trait` block. Traits can be opted into when initializing a packer by passing
|
@@ -347,15 +514,19 @@ the name of the trait as an argument:
|
|
347
514
|
```ruby
|
348
515
|
class MyPacker < Sequel::Packer
|
349
516
|
model MyObj
|
517
|
+
field :id
|
518
|
+
|
350
519
|
trait :my_trait do
|
351
|
-
field :
|
520
|
+
field :trait_field
|
352
521
|
end
|
353
522
|
end
|
354
523
|
|
355
|
-
# packed objects don't have
|
356
|
-
MyPacker.
|
357
|
-
|
358
|
-
|
524
|
+
# packed objects don't have trait_field
|
525
|
+
MyPacker.pack(dataset)
|
526
|
+
=> [{id: 1}, {id: 2}, ...]
|
527
|
+
# packed objects do have trait_field
|
528
|
+
MyPacker.pack(dataset, :my_trait)
|
529
|
+
=> [{id: 1, trait_field: 'foo'}, {id: 2, trait_field: 'bar'}, ...]
|
359
530
|
```
|
360
531
|
|
361
532
|
Traits can also be used when packing associations by passing the name of the
|
@@ -368,7 +539,7 @@ class MyOtherPacker < Sequel::Packer
|
|
368
539
|
end
|
369
540
|
```
|
370
541
|
|
371
|
-
|
542
|
+
#### `self.eager(*associations)`
|
372
543
|
|
373
544
|
When packing an association, a Packer will automatically ensure that association
|
374
545
|
is eager loaded, but there may be cases when an association will be accessed
|
@@ -392,7 +563,7 @@ class UserPacker < Sequel::Packer
|
|
392
563
|
end
|
393
564
|
end
|
394
565
|
|
395
|
-
UserPacker.
|
566
|
+
UserPacker.pack(User.dataset)
|
396
567
|
=> [
|
397
568
|
{id: 123, num_posts: 7},
|
398
569
|
{id: 456, num_posts: 3},
|
@@ -400,7 +571,7 @@ UserPacker.new.pack(User.dataset)
|
|
400
571
|
]
|
401
572
|
```
|
402
573
|
|
403
|
-
|
574
|
+
Using `eager` can help prevent N+1 query problems when not using Sequel's
|
404
575
|
[`TacticalEagerLoading`](https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/TacticalEagerLoading.html)
|
405
576
|
plugin.
|
406
577
|
|
@@ -421,22 +592,28 @@ class UserPacker < Sequel::Packer
|
|
421
592
|
end
|
422
593
|
```
|
423
594
|
|
424
|
-
|
425
|
-
|
426
|
-
|
595
|
+
**IMPORTANT NOTE:** Eager procs are not guaranteed to be executed when passing
|
596
|
+
in models, rather than a dataset, to `pack`. Specifically, if the models already
|
597
|
+
have fetched the association, the Packer won't refetch it. Because of this, it's
|
598
|
+
good practice to use `set_association_packer` and `pack_association` (see next
|
599
|
+
section) in a `field` block and duplicate the filtering action.
|
600
|
+
|
601
|
+
Also keep in mind that this limits the association that gets used by ALL fields,
|
602
|
+
so if another field actually needs access to all the users posts, it might not
|
603
|
+
make sense to use `eager`.
|
427
604
|
|
428
|
-
|
429
|
-
multiple procs, each proc will get applied to the dataset, likely resulting
|
430
|
-
overly restrictive filtering.
|
605
|
+
Additionally, it's important to note that if `eager` is called multiple times,
|
606
|
+
with multiple procs, each proc will get applied to the dataset, likely resulting
|
607
|
+
in overly restrictive filtering.
|
431
608
|
|
432
|
-
|
609
|
+
#### `self.set_association_packer(association, subpacker, *traits)`
|
433
610
|
|
434
611
|
See `self.pack_association(association, models)` below.
|
435
612
|
|
436
|
-
|
613
|
+
#### `self.pack_association(association, models)`
|
437
614
|
|
438
615
|
The simplest way to pack an association is to use
|
439
|
-
`self.field(association,
|
616
|
+
`self.field(association, subpacker, *traits)`, but sometimes this doesn't do
|
440
617
|
exactly what we want. We may want to pack the association under a different key
|
441
618
|
than the name of the association. Or we may only want to pack some of the
|
442
619
|
associated models (and it may be difficult or impossible to express which subset
|
@@ -475,7 +652,7 @@ but if it is passed a single model, it will return just that packed model.
|
|
475
652
|
|
476
653
|
Examples:
|
477
654
|
|
478
|
-
|
655
|
+
##### Use a different field name than the name of the association
|
479
656
|
```ruby
|
480
657
|
set_association_packer :ugly_internal_names, InternalPacker
|
481
658
|
field :nice_external_names do |model|
|
@@ -483,7 +660,7 @@ field :nice_external_names do |model|
|
|
483
660
|
end
|
484
661
|
```
|
485
662
|
|
486
|
-
|
663
|
+
##### Pack a single instance of a `one_to_many` association
|
487
664
|
```ruby
|
488
665
|
class PostPacker < Sequel::Packer
|
489
666
|
set_association_packer :comments, CommentPacker
|
@@ -493,34 +670,213 @@ class PostPacker < Sequel::Packer
|
|
493
670
|
end
|
494
671
|
```
|
495
672
|
|
673
|
+
#### `self.precompute(&block)`
|
496
674
|
|
497
|
-
|
675
|
+
Occasionally packing a model may require a computation that doesn't fit in with
|
676
|
+
the rest of the Packer paradigm. This may be a Sequel query that is particularly
|
677
|
+
difficult to express as an association, or even a call to an external service.
|
678
|
+
If such a computation can be performed in bulk, then the `precompute` method can
|
679
|
+
be used as an entry point for that operation.
|
498
680
|
|
499
|
-
|
500
|
-
|
681
|
+
The `precompute` method will execute a given block and pass it all of the models
|
682
|
+
that will be packed using that packer. This block will be executed a single
|
683
|
+
time, even when called by a deeply nested packer.
|
501
684
|
|
502
|
-
|
685
|
+
The `precompute` block is `instance_exec`ed in the context of the packer
|
686
|
+
instance, the result of any computation can be saved in a simple instance
|
687
|
+
variable (`@precomputed_result`) and later referenced inside the blocks that are
|
688
|
+
passed to `field` methods.
|
503
689
|
|
504
|
-
|
505
|
-
|
690
|
+
As an example, suppose a video uploading platform performs additional video
|
691
|
+
processing on every uploaded video and exposes the status of that processing as
|
692
|
+
a separate service over the network, rather than directly with the upload
|
693
|
+
metadata in the database. `precompute` could be used as follows:
|
506
694
|
|
507
|
-
|
695
|
+
```ruby
|
696
|
+
class VideoUploadPacker < Sequel::Packer
|
697
|
+
model VideoUpload
|
508
698
|
|
509
|
-
|
510
|
-
|
511
|
-
|
699
|
+
precompute do |video_uploads|
|
700
|
+
@processing_statuses = ResolutionService
|
701
|
+
.get_status_bulk(ids: video_uploads.map(&:id))
|
702
|
+
end
|
512
703
|
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
704
|
+
field :id
|
705
|
+
field :filename
|
706
|
+
field :processing_status do |video_upload|
|
707
|
+
@processing_statuses[video_upload.id]
|
708
|
+
end
|
709
|
+
end
|
710
|
+
```
|
711
|
+
|
712
|
+
#### Instance method versions
|
713
|
+
|
714
|
+
In addition to the class method versions of `field`, `eager`,
|
715
|
+
`set_association_packer`, and `precompute`, there are also regular instance method
|
716
|
+
versions which take the exact same arguments. When writing a `trait` block, the
|
717
|
+
block is evaulated in the context of a new Packer instance and actually calls the
|
718
|
+
instance method versions instead.
|
719
|
+
|
720
|
+
### Context
|
721
|
+
|
722
|
+
In addition to the data to be packed, and a set of traits, the `pack` method
|
723
|
+
also accepts arbitrary keyword arguments. This is referred to as `context` is
|
724
|
+
handled opaquely by the Packer. The data passed in here is saved as the
|
725
|
+
`@context` instance variable, which is then accessible from within the blocks
|
726
|
+
passed to `field`, `trait`, and `precompute`, for whatever purpose. It is also
|
727
|
+
automatically passed to any nested subpackers.
|
728
|
+
|
729
|
+
The most common usage for context would be to pass in the current user making
|
730
|
+
a request. It could then be used to pack permission levels about records, for
|
731
|
+
example.
|
732
|
+
|
733
|
+
```ruby
|
734
|
+
class PostPacker < Sequel::Packer
|
735
|
+
model Post
|
736
|
+
|
737
|
+
eager :permissions
|
738
|
+
field :access_level do |post|
|
739
|
+
user_permission = post.permissions.find do |perm|
|
740
|
+
perm.user_id == @context[:user].id
|
741
|
+
end
|
742
|
+
|
743
|
+
user_permission.access_level
|
744
|
+
end
|
745
|
+
end
|
746
|
+
```
|
747
|
+
|
748
|
+
You might notice something inefficient about the above code. Even though we only
|
749
|
+
want to look at the user's permission record, we fetch ALL of the permission
|
750
|
+
records for each Post. Ideally we would filter the `permissions` association
|
751
|
+
dataset when we call `eager`, but we don't have access to `@context` at that
|
752
|
+
point. This leads to the final DSL method available when writing a Packer:
|
753
|
+
|
754
|
+
#### `self.with_context(&block)`
|
755
|
+
|
756
|
+
You can pass a block to `with_context` that will be executed as soon as a Packer
|
757
|
+
instance is constructed. The block can access `@context` and can also call the
|
758
|
+
standard Packer DSL methods, `field`, `eager`, etc.
|
759
|
+
|
760
|
+
The above example could then be made more efficient as follows:
|
761
|
+
|
762
|
+
```ruby
|
763
|
+
class PostPacker < Sequel::Packer
|
764
|
+
model Post
|
765
|
+
|
766
|
+
- eager :permissions
|
767
|
+
+ with_context do
|
768
|
+
+ eager permissions: (proc {|ds| ds.where(user_id: @context[:user].id)})
|
769
|
+
+ end
|
770
|
+
end
|
771
|
+
```
|
772
|
+
|
773
|
+
A very tricky usage of `with_context` (and not recommended...) would be to
|
774
|
+
control the traits used on subpackers:
|
775
|
+
|
776
|
+
```ruby
|
777
|
+
class UserPacker < Sequel::Packer
|
778
|
+
model User
|
779
|
+
|
780
|
+
with_context do
|
781
|
+
field :comments, CommentPacker, *@context[:comment_traits]
|
782
|
+
end
|
783
|
+
end
|
784
|
+
|
785
|
+
UserPacker.pack(User.dataset, comment_traits: [])
|
786
|
+
=> [{comments: [{id: 7}, ...]}]
|
787
|
+
UserPacker.pack(User.dataset, comment_traits: [:author])
|
788
|
+
=> [{comments: [{id: 7, author: {id: 1, ...}}, ...]}]
|
789
|
+
UserPacker.pack(User.dataset, comment_traits: [:num_likes])
|
790
|
+
=> [{comments: [{id: 7, likes: 53}, ...]}]
|
791
|
+
```
|
792
|
+
|
793
|
+
## Potential Future Functionality
|
794
|
+
|
795
|
+
The 1.0.0 version of the Packer library is flexible to support many use cases.
|
796
|
+
That said, of course there are ways to improve it! There are three main
|
797
|
+
improvements I can imagine adding:
|
798
|
+
|
799
|
+
### Automatically Generated Type Declarations
|
800
|
+
|
801
|
+
It would be fairly easy to add generate type definitions by adding arguments to
|
802
|
+
`field`. Packers could produce [TypeScript
|
803
|
+
interface](https://www.typescriptlang.org/docs/handbook/interfaces.html)
|
804
|
+
declarations, and adding a simple build step to a CI pipeline could enforce type
|
805
|
+
safety across the frontend and backend. Or they could produce
|
806
|
+
[OpenAPI](http://spec.openapis.org/oas/v3.0.3) specifications, which could then
|
807
|
+
be used to automatically generate clients using something like
|
808
|
+
[Swagger](https://swagger.io/).
|
809
|
+
|
810
|
+
### Lifecycle Hooks
|
811
|
+
|
812
|
+
It should be fairly easy to extend the Packer library using standard Ruby
|
813
|
+
features like subclassing, mixins, via `include`, or even monkey-patching. It
|
814
|
+
may be beneficial to have explicit hooks for common operations however, like
|
815
|
+
`before_fetch`, or `around_pack`. It's more likely that these hooks are needed
|
816
|
+
for logging and tracing capabilities, than for actual functionality, so I'd
|
817
|
+
like to see some real-world usage before committing to a specific style of
|
818
|
+
integration.
|
819
|
+
|
820
|
+
### Less Data Fetching
|
821
|
+
|
822
|
+
Sequel by default fetches every column in a table, but a Packer knows (roughly)
|
823
|
+
what data is going to be used so it could only select the columns neede for
|
824
|
+
actual serialization, and limit how much data is actually fetched from the
|
825
|
+
database. I haven't done any benchmarking on this, so I'm not sure how much of
|
826
|
+
a benefit could be gained by this, but it would be interesting!
|
827
|
+
|
828
|
+
This could work roughly as follows:
|
829
|
+
|
830
|
+
* Start by fetching all columns that appear in simple `field(:column_name)`
|
831
|
+
declarations
|
832
|
+
* Add any columns need to fetch nested associations, or to re-asscociate fetched
|
833
|
+
records with their "parent" models, using the `left_key` and `right_key`
|
834
|
+
fields of the `AssociationReflections`
|
835
|
+
* Add a `column(*columns)` DSL method to explicitly fetch additional columns
|
836
|
+
|
837
|
+
### Other Enhancements
|
838
|
+
|
839
|
+
Here are some other potential enhancements, though these are less fleshed out.
|
840
|
+
|
841
|
+
* Support not including a key in a hash if the associated value is nil, to
|
842
|
+
reduce size of outputted data.
|
843
|
+
* Support different casing of the outputted hashes, i.e., `snake_case` vs.
|
844
|
+
`camelCase`.
|
845
|
+
* Explicitly support different output formats, rather than just plain Ruby
|
846
|
+
hashes, such as [Protocol
|
847
|
+
Buffers](https://developers.google.com/protocol-buffers) or [Cap'n
|
848
|
+
Proto](https://capnproto.org/).
|
849
|
+
* When using nested `precompute` blocks, the Packer has to flatten the
|
850
|
+
associations of a model, which may be expensive, but has not been benchmarked.
|
851
|
+
These flattened arrays already exist internally in Sequel when the eager
|
852
|
+
loading occurs, but those aren't exposed. The code in Sequel could be
|
853
|
+
re-implemented as part of the library to avoid re-constructing those arrays.
|
518
854
|
|
519
855
|
## Contributing
|
520
856
|
|
521
857
|
Bug reports and pull requests are welcome on GitHub at
|
522
858
|
https://github.com/PaulJuliusMartinez/sequel-packer.
|
523
859
|
|
860
|
+
### Development
|
861
|
+
|
862
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
863
|
+
`rake test` to run the tests. You can also run `bin/console` for an interactive
|
864
|
+
prompt that will allow you to experiment.
|
865
|
+
|
866
|
+
### Releases
|
867
|
+
|
868
|
+
To release a new version, update the version number in
|
869
|
+
`lib/sequel/packer/version.rb`, update the `CHANGELOG.md` with new changes, then
|
870
|
+
run `rake release`, which which will create a git tag for the version, push git
|
871
|
+
commits and tags, and push the `.gem` file to
|
872
|
+
[rubygems.org](https://rubygems.org).
|
873
|
+
|
874
|
+
## Attribution
|
875
|
+
|
876
|
+
[Karthik Viswanathan](https://github.com/karthikv) designed the original API
|
877
|
+
of the Packer library while at [Affinity](https://www.affinity.co/). This
|
878
|
+
library is a ground up rewrite which defines a very similar API, but shares no
|
879
|
+
code with the original implementation.
|
524
880
|
|
525
881
|
## License
|
526
882
|
|
data/lib/sequel/packer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
|
1
3
|
module Sequel
|
2
4
|
class Packer
|
3
5
|
# For invalid arguments provided to the field class method.
|
@@ -7,14 +9,33 @@ module Sequel
|
|
7
9
|
class AssociationDoesNotExistError < StandardError; end
|
8
10
|
class InvalidAssociationPackerError < StandardError; end
|
9
11
|
class UnknownTraitError < StandardError; end
|
12
|
+
class UnnecessaryWithContextError < StandardError; end
|
13
|
+
class NoAssociationSubpackerDefinedError < StandardError; end
|
10
14
|
|
15
|
+
# Think of this method as the "initialize" method for a Packer class.
|
16
|
+
# Every Packer class keeps track of the fields, traits, and other various
|
17
|
+
# operations defined using the DSL internally.
|
11
18
|
def self.inherited(subclass)
|
12
|
-
subclass.instance_variable_set(:@
|
13
|
-
subclass.instance_variable_set(:@
|
14
|
-
subclass.instance_variable_set(:@
|
15
|
-
subclass.instance_variable_set(:@
|
19
|
+
subclass.instance_variable_set(:@model, @model)
|
20
|
+
subclass.instance_variable_set(:@class_fields, @class_fields&.dup || [])
|
21
|
+
subclass.instance_variable_set(:@class_traits, @class_traits&.dup || {})
|
22
|
+
subclass.instance_variable_set(:@class_packers, @class_packers&.dup || {})
|
23
|
+
subclass.instance_variable_set(
|
24
|
+
:@class_eager_hash,
|
25
|
+
EagerHash.deep_dup(@class_eager_hash),
|
26
|
+
)
|
27
|
+
subclass.instance_variable_set(
|
28
|
+
:@class_precomputations,
|
29
|
+
@class_precomputations&.dup || [],
|
30
|
+
)
|
31
|
+
subclass.instance_variable_set(
|
32
|
+
:@class_with_contexts,
|
33
|
+
@class_with_contexts&.dup || [],
|
34
|
+
)
|
16
35
|
end
|
17
36
|
|
37
|
+
# Declare the type of Sequel::Model this Packer will be used for. Used to
|
38
|
+
# validate associations at declaration time.
|
18
39
|
def self.model(klass)
|
19
40
|
if !(klass < Sequel::Model)
|
20
41
|
fail(
|
@@ -32,18 +53,36 @@ module Sequel
|
|
32
53
|
METHOD_FIELD = :method_field
|
33
54
|
# field(:foo, &block)
|
34
55
|
BLOCK_FIELD = :block_field
|
35
|
-
# field(:association,
|
56
|
+
# field(:association, subpacker)
|
36
57
|
ASSOCIATION_FIELD = :association_field
|
37
58
|
# field(&block)
|
38
59
|
ARBITRARY_MODIFICATION_FIELD = :arbitrary_modification_field
|
39
60
|
|
40
|
-
|
61
|
+
# Declare a field to be packed in the output hash. This method can be called
|
62
|
+
# in multiple ways:
|
63
|
+
#
|
64
|
+
# field(:field_name)
|
65
|
+
# - Calls the method :field_name on a model and stores the result under the
|
66
|
+
# key :field_name in the packed hash.
|
67
|
+
#
|
68
|
+
# field(:field_name, &block)
|
69
|
+
# - Yields the model to the block and stores the result under the key
|
70
|
+
# :field_name in the packed hash.
|
71
|
+
#
|
72
|
+
# field(:association, subpacker, *traits)
|
73
|
+
# - Packs model.association using the designated subpacker with the
|
74
|
+
# specified traits.
|
75
|
+
#
|
76
|
+
# field(&block)
|
77
|
+
# - Yields the model and the partially packed hash to the block, allowing
|
78
|
+
# for arbitrary modification of the output hash.
|
79
|
+
def self.field(field_name=nil, subpacker=nil, *traits, &block)
|
41
80
|
Validation.check_field_arguments(
|
42
|
-
@model, field_name,
|
43
|
-
field_type = determine_field_type(field_name,
|
81
|
+
@model, field_name, subpacker, traits, &block)
|
82
|
+
field_type = determine_field_type(field_name, subpacker, block)
|
44
83
|
|
45
84
|
if field_type == ASSOCIATION_FIELD
|
46
|
-
set_association_packer(field_name,
|
85
|
+
set_association_packer(field_name, subpacker, *traits)
|
47
86
|
end
|
48
87
|
|
49
88
|
@class_fields << {
|
@@ -53,15 +92,10 @@ module Sequel
|
|
53
92
|
}
|
54
93
|
end
|
55
94
|
|
56
|
-
|
57
|
-
Validation.check_association_packer(
|
58
|
-
@model, association, packer_class, traits)
|
59
|
-
@class_packers[association] = [packer_class, traits]
|
60
|
-
end
|
61
|
-
|
95
|
+
# Helper for determing a field type from the arguments to field.
|
62
96
|
private_class_method def self.determine_field_type(
|
63
97
|
field_name,
|
64
|
-
|
98
|
+
subpacker,
|
65
99
|
block
|
66
100
|
)
|
67
101
|
if block
|
@@ -71,7 +105,7 @@ module Sequel
|
|
71
105
|
ARBITRARY_MODIFICATION_FIELD
|
72
106
|
end
|
73
107
|
else
|
74
|
-
if
|
108
|
+
if subpacker
|
75
109
|
ASSOCIATION_FIELD
|
76
110
|
else
|
77
111
|
METHOD_FIELD
|
@@ -79,6 +113,17 @@ module Sequel
|
|
79
113
|
end
|
80
114
|
end
|
81
115
|
|
116
|
+
# Register that nested models related to the packed model by association
|
117
|
+
# should be packed using the given subpacker with the specified traits.
|
118
|
+
def self.set_association_packer(association, subpacker, *traits)
|
119
|
+
Validation.check_association_packer(
|
120
|
+
@model, association, subpacker, traits)
|
121
|
+
@class_packers[association] = [subpacker, traits]
|
122
|
+
end
|
123
|
+
|
124
|
+
# Define a trait, a set of optional fields that can be packed in certain
|
125
|
+
# situations. The block can call main Packer DSL methods: field,
|
126
|
+
# set_association_packer, eager, or precompute.
|
82
127
|
def self.trait(name, &block)
|
83
128
|
if @class_traits.key?(name)
|
84
129
|
raise ArgumentError, "Trait :#{name} already defined"
|
@@ -89,6 +134,14 @@ module Sequel
|
|
89
134
|
@class_traits[name] = block
|
90
135
|
end
|
91
136
|
|
137
|
+
# Specify additional eager loading that should take place when fetching data
|
138
|
+
# to be packed. Commonly used to add filters to association datasets via
|
139
|
+
# eager procs.
|
140
|
+
#
|
141
|
+
# Users should not assume when using eager procs that the proc actually gets
|
142
|
+
# executed. If models with their associations already loaded are passed to
|
143
|
+
# pack then the proc will never get processed. Any filtering logic should be
|
144
|
+
# duplicated within a field block.
|
92
145
|
def self.eager(*associations)
|
93
146
|
@class_eager_hash = EagerHash.merge!(
|
94
147
|
@class_eager_hash,
|
@@ -96,23 +149,68 @@ module Sequel
|
|
96
149
|
)
|
97
150
|
end
|
98
151
|
|
99
|
-
|
100
|
-
|
152
|
+
# Declare an arbitrary operation to be performed one all the data has been
|
153
|
+
# fetched. The block will be executed once and be passed all of the models
|
154
|
+
# that will be packed by this Packer, even if this Packer is nested as a
|
155
|
+
# subpacker of other packers. The block can save the result of the
|
156
|
+
# computation in an instance variable which can then be accessed in the
|
157
|
+
# blocks passed to field.
|
158
|
+
def self.precompute(&block)
|
159
|
+
if !block
|
160
|
+
raise ArgumentError, 'Sequel::Packer.precompute must be passed a block'
|
161
|
+
end
|
162
|
+
@class_precomputations << block
|
163
|
+
end
|
164
|
+
|
165
|
+
# Declare a block to be called after a Packer has been initialized with
|
166
|
+
# context. The block can call the common Packer DSL methods. It is most
|
167
|
+
# commonly used to pass eager procs that depend on the Packer context to
|
168
|
+
# eager.
|
169
|
+
def self.with_context(&block)
|
170
|
+
if !block
|
171
|
+
raise ArgumentError, 'Sequel::Packer.with_context must be passed a block'
|
172
|
+
end
|
173
|
+
@class_with_contexts << block
|
174
|
+
end
|
101
175
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
176
|
+
# Pack the given data with the specified traits and additional context.
|
177
|
+
# Context is automatically passed down to any subpackers.
|
178
|
+
#
|
179
|
+
# Data can be provided as a Sequel::Dataset, an array of Sequel::Models, a
|
180
|
+
# single Sequel::Model, or nil. Even when passing models that have already
|
181
|
+
# been materialized, eager loading will be used to efficiently fetch
|
182
|
+
# associations.
|
183
|
+
#
|
184
|
+
# Returns an array of packed hashes, or a single packed hash if a single
|
185
|
+
# model was passed in. Returns nil if nil was passed in.
|
186
|
+
def self.pack(data, *traits, **context)
|
187
|
+
return nil if !data
|
188
|
+
new(*traits, **context).pack(data)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Initialize a Packer instance with the given traits and additional context.
|
192
|
+
# This Packer can then pack multiple datasets or models via the pack method.
|
193
|
+
def initialize(*traits, **context)
|
194
|
+
@context = context
|
195
|
+
|
196
|
+
@subpackers = {}
|
197
|
+
|
198
|
+
# Technically we only need to duplicate these fields if we modify any of
|
199
|
+
# them, but manually implementing some sort of copy-on-write functionality
|
200
|
+
# is messy and error prone.
|
201
|
+
@instance_fields = class_fields.dup
|
202
|
+
@instance_packers = class_packers.dup
|
203
|
+
@instance_eager_hash = EagerHash.deep_dup(class_eager_hash)
|
204
|
+
@instance_precomputations = class_precomputations.dup
|
205
|
+
|
206
|
+
class_with_contexts.each do |with_context_block|
|
207
|
+
self.instance_exec(&with_context_block)
|
111
208
|
end
|
112
209
|
|
113
210
|
# Evaluate trait blocks, which might add new fields to @instance_fields,
|
114
|
-
# new packers to @instance_packers,
|
115
|
-
# @instance_eager_hash
|
211
|
+
# new packers to @instance_packers, new associations to
|
212
|
+
# @instance_eager_hash, and/or new precomputations to
|
213
|
+
# @instance_precomputations.
|
116
214
|
traits.each do |trait|
|
117
215
|
trait_block = class_traits[trait]
|
118
216
|
if !trait_block
|
@@ -123,10 +221,9 @@ module Sequel
|
|
123
221
|
end
|
124
222
|
|
125
223
|
# Create all the subpackers, and merge in their eager hashes.
|
126
|
-
@instance_packers.each do |association, (
|
127
|
-
association_packer =
|
224
|
+
@instance_packers.each do |association, (subpacker, traits)|
|
225
|
+
association_packer = subpacker.new(*traits, **@context)
|
128
226
|
|
129
|
-
@subpackers ||= {}
|
130
227
|
@subpackers[association] = association_packer
|
131
228
|
|
132
229
|
@instance_eager_hash = EagerHash.merge!(
|
@@ -136,12 +233,77 @@ module Sequel
|
|
136
233
|
end
|
137
234
|
end
|
138
235
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
236
|
+
# Pack the given data with the traits and additional context specified when
|
237
|
+
# the Packer instance was created.
|
238
|
+
#
|
239
|
+
# Data can be provided as a Sequel::Dataset, an array of Sequel::Models, a
|
240
|
+
# single Sequel::Model, or nil. Even when passing models that have already
|
241
|
+
# been materialized, eager loading will be used to efficiently fetch
|
242
|
+
# associations.
|
243
|
+
#
|
244
|
+
# Returns an array of packed hashes, or a single packed hash if a single
|
245
|
+
# model was passed in. Returns nil if nil was passed in.
|
246
|
+
def pack(data)
|
247
|
+
case data
|
248
|
+
when Sequel::Dataset
|
249
|
+
data = data.eager(@instance_eager_hash) if @instance_eager_hash
|
250
|
+
models = data.all
|
251
|
+
|
252
|
+
run_precomputations(models)
|
253
|
+
pack_models(models)
|
254
|
+
when Sequel::Model
|
255
|
+
if @instance_eager_hash
|
256
|
+
EagerLoading.eager_load(class_model, [data], @instance_eager_hash)
|
257
|
+
end
|
258
|
+
|
259
|
+
run_precomputations([data])
|
260
|
+
pack_model(data)
|
261
|
+
when Array
|
262
|
+
if @instance_eager_hash
|
263
|
+
EagerLoading.eager_load(class_model, data, @instance_eager_hash)
|
264
|
+
end
|
265
|
+
|
266
|
+
run_precomputations(data)
|
267
|
+
pack_models(data)
|
268
|
+
when NilClass
|
269
|
+
nil
|
270
|
+
end
|
143
271
|
end
|
144
272
|
|
273
|
+
private
|
274
|
+
|
275
|
+
# Run any blocks declared using precompute on the given models, as well as
|
276
|
+
# any precompute blocks declared by subpackers.
|
277
|
+
def run_precomputations(models)
|
278
|
+
@instance_packers.each do |association, _|
|
279
|
+
subpacker = @subpackers[association]
|
280
|
+
next if !subpacker.send(:has_precomputations?)
|
281
|
+
|
282
|
+
reflection = class_model.association_reflection(association)
|
283
|
+
|
284
|
+
if reflection.returns_array?
|
285
|
+
all_associated_records = models.flat_map {|m| m.send(association)}.uniq
|
286
|
+
else
|
287
|
+
all_associated_records = models.map {|m| m.send(association)}.compact
|
288
|
+
end
|
289
|
+
|
290
|
+
subpacker.send(:run_precomputations, all_associated_records)
|
291
|
+
end
|
292
|
+
|
293
|
+
@instance_precomputations.each do |block|
|
294
|
+
instance_exec(models, &block)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# Check if a Packer has any precompute blocks declared, to avoid the
|
299
|
+
# overhead of flattening the child associations.
|
300
|
+
def has_precomputations?
|
301
|
+
return true if @instance_precomputations.any?
|
302
|
+
return false if !@subpackers
|
303
|
+
@subpackers.values.any? {|sp| sp.send(:has_precomputations?)}
|
304
|
+
end
|
305
|
+
|
306
|
+
# Pack a single model by processing all of the Packer's declared fields.
|
145
307
|
def pack_model(model)
|
146
308
|
h = {}
|
147
309
|
|
@@ -164,35 +326,45 @@ module Sequel
|
|
164
326
|
h
|
165
327
|
end
|
166
328
|
|
329
|
+
# Pack an array of models by processing all of the Packer's declared fields.
|
167
330
|
def pack_models(models)
|
168
331
|
models.map {|m| pack_model(m)}
|
169
332
|
end
|
170
333
|
|
334
|
+
# Pack models from an association using the designated subpacker.
|
171
335
|
def pack_association(association, associated_models)
|
172
336
|
return nil if !associated_models
|
173
337
|
|
174
338
|
packer = @subpackers[association]
|
175
339
|
|
340
|
+
if !packer
|
341
|
+
raise(
|
342
|
+
NoAssociationSubpackerDefinedError,
|
343
|
+
"pack_association called for the #{class_model}.#{association} " +
|
344
|
+
'association, but no Packer has been set for that association.',
|
345
|
+
)
|
346
|
+
end
|
347
|
+
|
176
348
|
if associated_models.is_a?(Array)
|
177
|
-
packer.pack_models
|
349
|
+
packer.send(:pack_models, associated_models)
|
178
350
|
else
|
179
|
-
packer.pack_model
|
351
|
+
packer.send(:pack_model, associated_models)
|
180
352
|
end
|
181
353
|
end
|
182
354
|
|
183
|
-
|
184
|
-
|
185
|
-
|
355
|
+
# See the definition of self.field. This method accepts the exact same
|
356
|
+
# arguments. When fields are declared within trait blocks, this method is
|
357
|
+
# called rather than the class method.
|
358
|
+
def field(field_name=nil, subpacker=nil, *traits, &block)
|
186
359
|
klass = self.class
|
187
|
-
model = klass.instance_variable_get(:@model)
|
188
360
|
|
189
361
|
Validation.check_field_arguments(
|
190
|
-
|
362
|
+
class_model, field_name, subpacker, traits, &block)
|
191
363
|
field_type =
|
192
|
-
klass.send(:determine_field_type, field_name,
|
364
|
+
klass.send(:determine_field_type, field_name, subpacker, block)
|
193
365
|
|
194
366
|
if field_type == ASSOCIATION_FIELD
|
195
|
-
set_association_packer(field_name,
|
367
|
+
set_association_packer(field_name, subpacker, *traits)
|
196
368
|
end
|
197
369
|
|
198
370
|
@instance_fields << {
|
@@ -202,18 +374,19 @@ module Sequel
|
|
202
374
|
}
|
203
375
|
end
|
204
376
|
|
205
|
-
|
206
|
-
|
377
|
+
# See the definition of self.set_association_packer. This method accepts the
|
378
|
+
# exact same arguments. When used within a trait block, this method is
|
379
|
+
# called rather than the class method.
|
380
|
+
def set_association_packer(association, subpacker, *traits)
|
207
381
|
Validation.check_association_packer(
|
208
|
-
|
209
|
-
|
210
|
-
@instance_packers[association] = [packer_class, traits]
|
211
|
-
end
|
382
|
+
class_model, association, subpacker, traits)
|
212
383
|
|
213
|
-
|
214
|
-
@instance_eager_hash
|
384
|
+
@instance_packers[association] = [subpacker, traits]
|
215
385
|
end
|
216
386
|
|
387
|
+
# See the definition of self.eager. This method accepts the exact same
|
388
|
+
# arguments. When used within a trait block, this method is called rather
|
389
|
+
# than the class method.
|
217
390
|
def eager(*associations)
|
218
391
|
@instance_eager_hash = EagerHash.merge!(
|
219
392
|
@instance_eager_hash,
|
@@ -221,6 +394,39 @@ module Sequel
|
|
221
394
|
)
|
222
395
|
end
|
223
396
|
|
397
|
+
# See the definition of self.precompute. This method accepts the exact same
|
398
|
+
# arguments. When used within a trait block, this method is called rather
|
399
|
+
# than the class method.
|
400
|
+
def precompute(&block)
|
401
|
+
if !block
|
402
|
+
raise ArgumentError, 'Sequel::Packer.precompute must be passed a block'
|
403
|
+
end
|
404
|
+
@instance_precomputations << block
|
405
|
+
end
|
406
|
+
|
407
|
+
# See the definition of self.with_context. This method accepts the exact
|
408
|
+
# same arguments. When used within a trait block, this method is called
|
409
|
+
# rather than the class method.
|
410
|
+
def with_context(&block)
|
411
|
+
raise(
|
412
|
+
UnnecessaryWithContextError,
|
413
|
+
'There is no need to call with_context from within a trait block; ' +
|
414
|
+
'@context can be accessed directly.',
|
415
|
+
)
|
416
|
+
end
|
417
|
+
|
418
|
+
# Access the internal eager hash.
|
419
|
+
def eager_hash
|
420
|
+
@instance_eager_hash
|
421
|
+
end
|
422
|
+
|
423
|
+
# The following methods expose the class instance variables containing the
|
424
|
+
# core definition of the Packer.
|
425
|
+
|
426
|
+
def class_model
|
427
|
+
self.class.instance_variable_get(:@model)
|
428
|
+
end
|
429
|
+
|
224
430
|
def class_fields
|
225
431
|
self.class.instance_variable_get(:@class_fields)
|
226
432
|
end
|
@@ -236,9 +442,18 @@ module Sequel
|
|
236
442
|
def class_traits
|
237
443
|
self.class.instance_variable_get(:@class_traits)
|
238
444
|
end
|
445
|
+
|
446
|
+
def class_precomputations
|
447
|
+
self.class.instance_variable_get(:@class_precomputations)
|
448
|
+
end
|
449
|
+
|
450
|
+
def class_with_contexts
|
451
|
+
self.class.instance_variable_get(:@class_with_contexts)
|
452
|
+
end
|
239
453
|
end
|
240
454
|
end
|
241
455
|
|
242
456
|
require 'sequel/packer/eager_hash'
|
457
|
+
require 'sequel/packer/eager_loading'
|
243
458
|
require 'sequel/packer/validation'
|
244
459
|
require "sequel/packer/version"
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Packer
|
3
|
+
module EagerLoading
|
4
|
+
# This methods allows eager loading associations _after_ a record, or
|
5
|
+
# multiple records, have been fetched from the database. It is useful when
|
6
|
+
# you know you will be accessing model associations, but models have
|
7
|
+
# already been materialized.
|
8
|
+
#
|
9
|
+
# This method accepts a normalized_eager_hash, as specified by
|
10
|
+
# Sequel::Packer::EagerHash.
|
11
|
+
#
|
12
|
+
# This method will handle procs used to filter association datasets, but
|
13
|
+
# if an association has already been loaded for every model, the dataset
|
14
|
+
# will not be refetched and the proc will not be applied.
|
15
|
+
#
|
16
|
+
# This method borrows a lot from the #eager_load Sequel::Dataset method
|
17
|
+
# defined in Sequels lib/sequel/model/associations.rb.
|
18
|
+
def self.eager_load(model_class, model_or_models, normalized_eager_hash)
|
19
|
+
models = model_or_models.is_a?(Array) ?
|
20
|
+
model_or_models :
|
21
|
+
[model_or_models]
|
22
|
+
|
23
|
+
# Cache to avoid building id maps multiple times.
|
24
|
+
key_hash = {}
|
25
|
+
|
26
|
+
normalized_eager_hash.each do |association, nested_associations|
|
27
|
+
eager_block = nil
|
28
|
+
|
29
|
+
if EagerHash.is_proc_hash?(nested_associations)
|
30
|
+
eager_block, nested_associations = nested_associations.entries[0]
|
31
|
+
end
|
32
|
+
|
33
|
+
reflection = model_class.association_reflections[association]
|
34
|
+
|
35
|
+
# If all of the models have already loaded the association, we'll just
|
36
|
+
# recursively call ::eager_load to load nested associations.
|
37
|
+
if models.all? {|m| m.associations.key?(association)}
|
38
|
+
if nested_associations
|
39
|
+
associated_records = if reflection.returns_array?
|
40
|
+
models.flat_map {|m| m.send(association)}.uniq
|
41
|
+
else
|
42
|
+
models.map {|m| m.send(association)}.compact
|
43
|
+
end
|
44
|
+
|
45
|
+
eager_load(
|
46
|
+
reflection.associated_class,
|
47
|
+
associated_records,
|
48
|
+
nested_associations,
|
49
|
+
)
|
50
|
+
end
|
51
|
+
else
|
52
|
+
key = reflection.eager_loader_key
|
53
|
+
id_map = nil
|
54
|
+
|
55
|
+
if key && !key_hash[key]
|
56
|
+
id_map = Hash.new {|h, k| h[k] = []}
|
57
|
+
|
58
|
+
models.each do |model|
|
59
|
+
case key
|
60
|
+
when Symbol
|
61
|
+
model_id = model.get_column_value(key)
|
62
|
+
id_map[model_id] << model if model_id
|
63
|
+
when Array
|
64
|
+
model_id = key.map {|col| model.get_column_value(col)}
|
65
|
+
id_map[model_id] << model if model_id.all?
|
66
|
+
else
|
67
|
+
raise(
|
68
|
+
Sequel::Error,
|
69
|
+
"unhandled eager_loader_key #{key.inspect} for " +
|
70
|
+
"association #{association}",
|
71
|
+
)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
loader = reflection[:eager_loader]
|
77
|
+
|
78
|
+
loader.call(
|
79
|
+
key_hash: key_hash,
|
80
|
+
rows: models,
|
81
|
+
associations: nested_associations,
|
82
|
+
self: self,
|
83
|
+
eager_block: eager_block,
|
84
|
+
id_map: id_map,
|
85
|
+
)
|
86
|
+
|
87
|
+
if reflection[:after_load]
|
88
|
+
models.each do |model|
|
89
|
+
model.send(
|
90
|
+
:run_association_callbacks,
|
91
|
+
reflection,
|
92
|
+
:after_load,
|
93
|
+
model.associations[association],
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sequel-packer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul Julius Martinez
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -128,6 +128,7 @@ files:
|
|
128
128
|
- bin/setup
|
129
129
|
- lib/sequel/packer.rb
|
130
130
|
- lib/sequel/packer/eager_hash.rb
|
131
|
+
- lib/sequel/packer/eager_loading.rb
|
131
132
|
- lib/sequel/packer/validation.rb
|
132
133
|
- lib/sequel/packer/version.rb
|
133
134
|
- sequel-packer.gemspec
|