autobots 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +84 -83
- data/docs/flow.png +0 -0
- data/lib/autobots.rb +1 -0
- data/lib/autobots/active_record_assembler.rb +5 -0
- data/lib/autobots/helpers/active_record_preloading.rb +6 -3
- data/lib/autobots/version.rb +1 -1
- data/test/test_models.rb +1 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cced0fc73b998d0e2c7ec8d62db5006e4e3191fd
|
4
|
+
data.tar.gz: df8529087829518b6982070e58c443c779588fc9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 253704d48566fe2a4be8c11d2c3274b84045e739592baef54d80982539b73096fa270f1706918efa77f4c755ff95a6e855ba9ef884a5950c240d0a511041c92a
|
7
|
+
data.tar.gz: 91cf503c5dc1136547588320aba92696320476b252a48796e3ff85bb7f603f67695839650a60576ab442eff784695e772eb688c069043dfcd3c1f74f9c5b1757
|
data/README.md
CHANGED
@@ -4,134 +4,135 @@ Loading and serializing models in bulk with caching.
|
|
4
4
|
|
5
5
|
Separate the loading/serialization of resources from their protocol (http/json, avro, xml).
|
6
6
|
|
7
|
-
## Motivation
|
8
|
-
|
9
7
|
We want to improve api response time by loading the minimum amount of data needed to check cache keys. If we miss, we want to optimally load our data, serialize it, cache it and then return it.
|
10
8
|
|
11
|
-
##
|
12
|
-
|
13
|
-
Add this line to your application's Gemfile:
|
9
|
+
## Problem
|
14
10
|
|
15
|
-
|
16
|
-
gem 'autobots'
|
17
|
-
```
|
11
|
+
Say we have a simple api action:
|
18
12
|
|
19
|
-
|
13
|
+
def index
|
14
|
+
# find our records
|
15
|
+
projects = Project.includes(issues: :comments).first(10)
|
16
|
+
render json: projects, each_serializer: ProjectSerializer
|
17
|
+
end
|
20
18
|
|
21
|
-
|
19
|
+
How do we cache this?
|
22
20
|
|
23
|
-
|
21
|
+
class ProjectSerializer < ActiveModel::Serializer
|
22
|
+
cached
|
23
|
+
end
|
24
24
|
|
25
|
-
|
25
|
+
def index
|
26
|
+
projects = Project.includes(issues: :comments).first(10)
|
27
|
+
render json: projects, each_serializer: ProjectSerializer
|
28
|
+
end
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
When trying to render a resource for an API, we have 3 parts:
|
30
|
-
|
31
|
-
1. Fetching data
|
32
|
-
2. Figuring out the data to return
|
33
|
-
3. Format it for the protocol
|
30
|
+
There are 2 problems with this approach:
|
34
31
|
|
35
|
-
|
32
|
+
1. We're making 3 sql calls every time regardless of what keys are required for checking the cache
|
33
|
+
2. We're making 10 cache fetch requests
|
36
34
|
|
37
|
-
|
35
|
+
We can fix this by fetching the bare minimum amount of data needed to check the cache, checking the cache in bulk, then loading the data needed in an optimized fashion for cache misses only.
|
38
36
|
|
39
|
-
|
40
|
-
|
41
|
-
class ProjectAssembler < Autobots::Assembler
|
42
|
-
|
43
|
-
# assembles the base objects needed for cache key generation
|
44
|
-
def assemble(identifiers)
|
45
|
-
Project.where(id: identifiers).to_a
|
46
|
-
end
|
47
|
-
|
48
|
-
end
|
37
|
+
This gem's goal is to abstract all that logic into a testable, declarative model that can improve your api's response time. We also want to return serializable data so that we decouple the definition of a resource from its protocols (not locked to json or xml)
|
49
38
|
|
39
|
+
## Usage
|
50
40
|
|
51
|
-
|
52
|
-
assembler = ProjectAssembler.new(project_ids)
|
41
|
+
An `Autobots::Assembler` is the core of the `autobots` gem. The input for an autobot is that you provide an array of objects needed to build your cache keys. The output is an array of serializable data that corresponds to the input set (retaining order).
|
53
42
|
|
54
|
-
|
55
|
-
resources = assembler.resources
|
43
|
+
![flow](docs/flow.png)
|
56
44
|
|
57
|
-
|
45
|
+
### Lifecycle of an Autobot
|
58
46
|
|
59
|
-
|
47
|
+
When trying to render a resource for an API, we have 3 parts:
|
60
48
|
|
61
|
-
|
49
|
+
1. Fetching data
|
50
|
+
2. Figuring out the data to return
|
51
|
+
3. Format it for the protocol
|
62
52
|
|
63
|
-
|
53
|
+
Each of these methods corresponds to a single lifecycle method of an autobot:
|
64
54
|
|
65
|
-
|
66
|
-
|
67
|
-
|
55
|
+
1. `assemble`
|
56
|
+
2. `transform`
|
57
|
+
3. `roll_out`
|
68
58
|
|
69
|
-
```
|
70
59
|
|
71
|
-
|
60
|
+
#### Fetching data (assemble)
|
72
61
|
|
73
|
-
`
|
62
|
+
An `assembler` can optionally load whatever data is required to build it's cache keys. The assembler's job is to fetch all data needed for serialization as optimally as possible. The identifiers should be the minimum amount of data needed to fetch data and determine caching.
|
74
63
|
|
75
|
-
|
64
|
+
class ProjectAssembler < Autobots::Assembler
|
65
|
+
|
66
|
+
# assembles the base objects needed for cache key generation
|
67
|
+
def assemble(identifiers)
|
68
|
+
Project.where(id: identifiers).to_a
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
76
72
|
|
77
|
-
|
73
|
+
project_ids = [1,2,3]
|
74
|
+
assembler = ProjectAssembler.new(project_ids)
|
75
|
+
|
76
|
+
# returns preloaded objects for serialization
|
77
|
+
objects = assembler.objects
|
78
78
|
|
79
|
-
|
79
|
+
#### Figuring out the data to return (transform)
|
80
80
|
|
81
|
-
|
82
|
-
cache = Rails.cache
|
83
|
-
assembler = ProjectAssembler.new(project_ids, cache: cache)
|
81
|
+
The transform lifecycle event is called with every object that needs refreshing. It allows us to to optimize our loading of nested resources by using bulk loading. If caching is enabled, we only need to run this on resources that missed the cache. We may not even need to run it at all!
|
84
82
|
|
85
|
-
|
83
|
+
An example of this would be using `ActiveRecord::Associations::Preloader` to fetch included records if necessary:
|
86
84
|
|
87
|
-
|
85
|
+
class ProjectAssembler < Autobots::Assembler
|
86
|
+
def transform(resources)
|
87
|
+
ActiveRecord::Associations::Preloader.new().preload(resources, {issues: :comments})
|
88
|
+
end
|
89
|
+
end
|
88
90
|
|
89
|
-
|
91
|
+
Since this is a fairly common pattern, I created the `Autobots::ActiveRecordAssembler` that has this default behavior:
|
90
92
|
|
91
|
-
|
93
|
+
class ProjectAssembler < Autobots::ActiveRecordAssembler
|
94
|
+
self.preloads = {issues: :comments}
|
95
|
+
end
|
92
96
|
|
93
|
-
|
94
|
-
cache: cache,
|
95
|
-
cache_key: -> (obj, assembler) {
|
96
|
-
"#{obj.cache_key}-foo"
|
97
|
-
}
|
98
|
-
})
|
97
|
+
The strength of this model lies in minimizing data fetch requests when building our data to serialize. We do this by delaying the loading of nested models and only load them if we have to.
|
99
98
|
|
100
|
-
|
99
|
+
If a resources's cache is up to date, we shouldn't have to fetch it's dependencies from the database. However, if we have a cache miss, we want to optimally load the data needed.
|
101
100
|
|
102
|
-
|
101
|
+
#### Formatting the response (roll_out)
|
103
102
|
|
104
|
-
|
103
|
+
We use the `active_model_serializers` gem to accomplish serialization. An assembler declares the type of serializer used to specify the data returned
|
105
104
|
|
106
|
-
If a resources's cache is up to date, we shouldn't have to fetch it's dependencies from the database. However, if we have a cache miss, we want to optimally load the data needed.
|
107
105
|
|
108
|
-
|
106
|
+
class ProjectAssembler < Autobots::Assembler
|
107
|
+
self.serializer = ProjectSerializer # an ActiveModel::Serializer
|
108
|
+
end
|
109
|
+
|
110
|
+
# returns an array of data for serialization
|
111
|
+
data = assembler.data
|
109
112
|
|
110
|
-
|
113
|
+
The default behavior of an assembler is to wrap each object with the declared serializer and returning its serializable_hash.
|
111
114
|
|
112
|
-
|
113
|
-
def transform(resources)
|
114
|
-
ActiveRecord::Associations::Preloader.new.preload(resources, {issues: :comments})
|
115
|
-
end
|
115
|
+
`autobots` gem only handles the loading and representation of the data.
|
116
116
|
|
117
|
-
|
117
|
+
### Caching
|
118
118
|
|
119
|
-
|
119
|
+
We can get large performance boosts by caching our serializable data. Caching is straight forward:
|
120
120
|
|
121
|
-
|
121
|
+
# some sort of ActiveSupport::CacheStore
|
122
|
+
cache = Rails.cache
|
123
|
+
assembler = ProjectAssembler.new(project_ids, cache: cache)
|
122
124
|
|
123
|
-
|
125
|
+
The cache store must implement `read_multi` as we use [`bulk_cache_fetcher` gem](https://github.com/justinweiss/bulk_cache_fetcher/)
|
124
126
|
|
125
|
-
|
126
|
-
include Autobots::Helpers::ActiveRecordPreloading
|
127
|
+
By default, we use each resource's `cache_key` implementation. You can provide your own cache key generator by passing in a cache_key proc:
|
127
128
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
129
|
+
assembler = ProjectAssembler.new(project_ids, {
|
130
|
+
cache: cache,
|
131
|
+
cache_key: -> (obj, assembler) {
|
132
|
+
"#{obj.cache_key}-foo"
|
133
|
+
}
|
134
|
+
})
|
133
135
|
|
134
|
-
```
|
135
136
|
|
136
137
|
## Contributing
|
137
138
|
|
data/docs/flow.png
ADDED
Binary file
|
data/lib/autobots.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
module Autobots
|
2
2
|
module Helpers
|
3
3
|
module ActiveRecordPreloading
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_attribute :preloads
|
8
|
+
self.preloads = []
|
9
|
+
end
|
4
10
|
|
5
11
|
def transform(objects)
|
6
12
|
ActiveRecord::Associations::Preloader.new(objects, preloads).run
|
@@ -10,9 +16,6 @@ module Autobots
|
|
10
16
|
objects
|
11
17
|
end
|
12
18
|
|
13
|
-
def preloads
|
14
|
-
[]
|
15
|
-
end
|
16
19
|
end
|
17
20
|
end
|
18
21
|
end
|
data/lib/autobots/version.rb
CHANGED
data/test/test_models.rb
CHANGED
@@ -31,10 +31,8 @@ class ProjectPreloadAssembler < Autobots::Assembler
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
class ProjectPreloadIncludedAssembler < Autobots::
|
34
|
+
class ProjectPreloadIncludedAssembler < Autobots::ActiveRecordAssembler
|
35
35
|
self.serializer = ProjectSerializer
|
36
|
-
include Autobots::Helpers::ActiveRecordPreloading
|
37
|
-
|
38
36
|
def preloads
|
39
37
|
{issues: :comments}
|
40
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: autobots
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeff Ching
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-11-
|
11
|
+
date: 2014-11-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bulk_cache_fetcher
|
@@ -108,7 +108,9 @@ files:
|
|
108
108
|
- README.md
|
109
109
|
- Rakefile
|
110
110
|
- autobots.gemspec
|
111
|
+
- docs/flow.png
|
111
112
|
- lib/autobots.rb
|
113
|
+
- lib/autobots/active_record_assembler.rb
|
112
114
|
- lib/autobots/assembler.rb
|
113
115
|
- lib/autobots/helpers.rb
|
114
116
|
- lib/autobots/helpers/active_record_preloading.rb
|