capsula 0.0.3 → 0.0.4
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/Gemfile.lock +1 -1
- data/README.md +80 -24
- data/capsula.gemspec +2 -2
- data/lib/capsula/encapsulator.rb +15 -3
- data/spec/capsula_spec.rb +28 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c079ef240a2cfdd558fdf3edffa7987476f278ade0831c55947011fac809ba9b
|
4
|
+
data.tar.gz: 8d99d06ef13427f10e8bab16a2ba813a7621c65e1348deae30dd3e2d8e253591
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e9519cd42c384e75c56ec4e342d3506da9ee0ffa83a231a36eb82d87b324bcf637bc0112c594778fe1a1c22723acefe4eb2122d824a69ca639e25a3300bd9e9
|
7
|
+
data.tar.gz: e5edde7d165a6eb1f075222d6be927c98c4c39b86af12765eb8abebc4ea03eb8fdf9db8fb5308b18d22aad49a09ed143e96f34ff37346db4027cc3aa8aeef466
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,42 +1,97 @@
|
|
1
1
|
# Capsula
|
2
|
-
The tool for
|
2
|
+
The tool for preloading related objects into **any** object to prevent N+1 queries.
|
3
3
|
|
4
|
-
##
|
5
|
-
|
6
|
-
Add to Gemfile:
|
4
|
+
## INSTALL
|
7
5
|
|
8
6
|
```
|
9
7
|
gem 'capsula'
|
10
8
|
```
|
11
9
|
|
12
|
-
|
10
|
+
## USE
|
13
11
|
|
14
12
|
```ruby
|
15
|
-
|
13
|
+
require 'capsula'
|
14
|
+
|
15
|
+
# our objects
|
16
|
+
$cars = [ {name: 'A', brand_id: 3}, {name: 'B', brand_id: 1}, {name: 'C', brand_id: 1} ]
|
17
|
+
$brands = [ {id: 1, name: 'Ferrari'}, {id: 2, name: 'Lamborghini'}, {id: 3, name: 'Rolls-Royce'} ]
|
18
|
+
|
19
|
+
# let's preload cars with brands
|
20
|
+
|
21
|
+
# our capsula-preloader
|
22
|
+
class CarPreloader < Capsula::Base
|
23
|
+
plan_for :brand,
|
24
|
+
src_key: -> (car) { car[:brand_id] },
|
25
|
+
dst_key: -> (brand) { brand[:id] },
|
26
|
+
dst_loader: -> (ids, opt) { $brands.select { |b| ids.include?(b[:id]) } }
|
27
|
+
end
|
28
|
+
|
29
|
+
# ok, preload!
|
30
|
+
$cars = CarPreloader.new($cars).plans(:brand).encapsulate
|
16
31
|
|
17
|
-
|
18
|
-
|
32
|
+
$cars
|
33
|
+
# => [{:name=>"A", :brand_id=>3}, {:name=>"B", :brand_id=>1}, {:name=>"C", :brand_id=>1}]
|
34
|
+
|
35
|
+
# hm... where is brands?
|
36
|
+
|
37
|
+
$cars.first.brand
|
38
|
+
# => {:id=>3, :name=>"Rolls-Royce"}
|
39
|
+
|
40
|
+
$cars[0].brand
|
41
|
+
# => {:id=>3, :name=>"Rolls-Royce"}
|
42
|
+
$cars[1].brand
|
43
|
+
# => {:id=>1, :name=>"Ferrari"}
|
44
|
+
$cars[2].brand
|
45
|
+
# => {:id=>1, :name=>"Ferrari"}
|
46
|
+
|
47
|
+
# so, source objects leave untouched, but was wrapped for methods interception
|
48
|
+
```
|
49
|
+
|
50
|
+
### ActiveRecord case:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
cars = Cars.where(id: [1,2,3]).to_a
|
54
|
+
|
55
|
+
cars = CarPreloader.new(cars).plans(
|
56
|
+
:fuel, :food, { driver: [:car_keys, :sunglasses] }
|
19
57
|
).encapsulate
|
20
58
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
# no actual action was triggered,
|
59
|
+
cars.first.driver.car_keys
|
60
|
+
=> <car_keys>
|
61
|
+
# no actual action (any SQL query) was triggered,
|
25
62
|
# immediate result was received from Capsula's wrapper
|
26
63
|
```
|
27
64
|
|
28
|
-
Let's see how `
|
65
|
+
Let's see how `CarPreloader` looks:
|
29
66
|
|
30
67
|
```ruby
|
31
68
|
class StarshipsEncapsulator < Capsula::Base
|
32
|
-
plan_for :
|
33
|
-
src_key: :
|
34
|
-
dst_key: :id,
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
69
|
+
plan_for :driver,
|
70
|
+
src_key: :driver_id, # it's default value, so can be skipped
|
71
|
+
dst_key: :id, # it's default value, so can be skipped
|
72
|
+
# Example loader for ActiveRecord model Driver:
|
73
|
+
dst_loader: -> (ids, opt) {
|
74
|
+
Driver.where(id: ids).includes(opt[:plans]).to_a
|
75
|
+
}
|
76
|
+
|
77
|
+
# # Plans for other relations:
|
78
|
+
# plan_for :fuel, ...
|
79
|
+
# plan_for :food, ...
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
### has_many example
|
84
|
+
|
85
|
+
You can use custom encapsulator, but stantard encapsulator understand you if dst_key be placed inside array:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class Sea < Capsula::Base
|
89
|
+
plan_for :crabs,
|
90
|
+
src_key: :name,
|
91
|
+
dst_key: [:sea_name], # key inside array signals about has_many relation
|
92
|
+
dst_loader: -> (sea_names, opt) {
|
93
|
+
Crab.where(sea_name: sea_names).includes(opt[:plans]).to_a
|
94
|
+
}
|
40
95
|
end
|
41
96
|
```
|
42
97
|
|
@@ -44,7 +99,7 @@ end
|
|
44
99
|
|
45
100
|
**default values:**
|
46
101
|
|
47
|
-
Definition for `src_key` and `dst_key` can be skipped if they values are `:
|
102
|
+
Definition for `src_key` and `dst_key` can be skipped if they values are `:driver_id` (`<key_name>_id`) and `:id`
|
48
103
|
|
49
104
|
**lambdas:**
|
50
105
|
|
@@ -94,12 +149,13 @@ class CustomLoader
|
|
94
149
|
# This method is triggered by Capsula
|
95
150
|
def collect_ids_and_load_relations
|
96
151
|
ids = @items.map{ |i| i.fuel_id }
|
97
|
-
|
152
|
+
preloads = Fuel.where(id: ids).to_a
|
153
|
+
@store = preloads.index_by(&:id)
|
98
154
|
end
|
99
155
|
|
100
156
|
# This method is calling by Capsula during encapsulation
|
101
157
|
def get_preloads_for_object starship
|
102
|
-
@store
|
158
|
+
@store[starship.fuel_id]
|
103
159
|
end
|
104
160
|
end
|
105
161
|
|
data/capsula.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'capsula'
|
3
|
-
s.version = '0.0.
|
4
|
-
s.date = '2019-
|
3
|
+
s.version = '0.0.4'
|
4
|
+
s.date = '2019-05-29'
|
5
5
|
s.summary = 'Encapsulating tool'
|
6
6
|
s.description = 'The tool for encapsulating (preloading, including) related objects'
|
7
7
|
s.authors = ['Rodion V']
|
data/lib/capsula/encapsulator.rb
CHANGED
@@ -108,8 +108,14 @@ module Capsula
|
|
108
108
|
col =
|
109
109
|
if ids.any?
|
110
110
|
preloads = dec[:dst_loader].call(ids,opt)
|
111
|
-
|
112
|
-
|
111
|
+
dst_key = dec[:dst_key]
|
112
|
+
if dst_key.is_a?(Array)
|
113
|
+
# build hash: {id1 => [Obj1, ...], idN => [ObjN] }
|
114
|
+
preloads.group_by { |o| get_value(o, dst_key.first) }
|
115
|
+
else
|
116
|
+
# build hash: {id1 => Obj1, ..., idN => ObjN }
|
117
|
+
index_array_by(preloads) { |el| get_value(el, dst_key) }
|
118
|
+
end
|
113
119
|
else
|
114
120
|
{}
|
115
121
|
end
|
@@ -130,12 +136,16 @@ module Capsula
|
|
130
136
|
def native_encapsulation plan_key, preloaded_collection, declaration
|
131
137
|
col = preloaded_collection
|
132
138
|
dec = declaration
|
139
|
+
is_dst_key_array = dec[:dst_key].is_a?(Array)
|
133
140
|
|
134
141
|
@items.each do |i|
|
135
142
|
src_id = get_value(i, dec[:src_key])
|
136
143
|
|
137
144
|
val =
|
138
|
-
if
|
145
|
+
if is_dst_key_array
|
146
|
+
# src has many dst
|
147
|
+
col[src_id] || []
|
148
|
+
elsif src_id.is_a?(Array)
|
139
149
|
# if object has many links to related objects (Array)
|
140
150
|
col.values_at(*src_id)
|
141
151
|
else
|
@@ -151,6 +161,8 @@ module Capsula
|
|
151
161
|
case key_or_lambda
|
152
162
|
when ::Symbol, ::String
|
153
163
|
_object_.send(key_or_lambda)
|
164
|
+
when ::Array
|
165
|
+
_object_.send(key_or_lambda[0])
|
154
166
|
when ::Proc
|
155
167
|
key_or_lambda.call(_object_)
|
156
168
|
else
|
data/spec/capsula_spec.rb
CHANGED
@@ -2,10 +2,11 @@ require 'capsula'
|
|
2
2
|
|
3
3
|
RSpec.describe Capsula do
|
4
4
|
let(:klass){ Class.new(Capsula::Base) }
|
5
|
-
let(:a) { Struct.new(:b_id) }
|
6
|
-
let(:b) { Struct.new(:id) }
|
7
5
|
|
8
6
|
describe 'User can' do
|
7
|
+
let(:a) { Struct.new(:b_id) }
|
8
|
+
let(:b) { Struct.new(:id) }
|
9
|
+
|
9
10
|
it 'declare new encapsulation plan' do
|
10
11
|
klass.plan_for(:b, src_key: :b_id, dst_key: :id, dst_loader: ->(i,o){})
|
11
12
|
plans = klass.instance_variable_get('@plans_declarations')
|
@@ -82,7 +83,32 @@ RSpec.describe Capsula do
|
|
82
83
|
expect(objects.first).to be_a(Capsula::Wrapper)
|
83
84
|
expect(objects.first.b.id).to be == objects.first.b_id
|
84
85
|
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'has_many plan' do
|
89
|
+
let(:a) { Struct.new(:id) }
|
90
|
+
let(:b) { Struct.new(:a_id) }
|
91
|
+
|
92
|
+
it 'working' do
|
93
|
+
b_store = [1,2,2,3].map { |a_id| b.new(a_id) }
|
94
|
+
klass.plan_for(
|
95
|
+
:bs,
|
96
|
+
src_key: :id,
|
97
|
+
dst_key: [:a_id],
|
98
|
+
dst_loader: ->(ids,opt){ b_store.select { |b| ids.include?(b.a_id) } }
|
99
|
+
)
|
85
100
|
|
101
|
+
as = [2,3,4].map { |id| a.new(id) }
|
102
|
+
as = klass.new(as).plans(:bs).encapsulate
|
103
|
+
|
104
|
+
expect(as[0]).to respond_to(:bs)
|
105
|
+
expect(as[0]).to be_a(Capsula::Wrapper)
|
106
|
+
expect(as[0].bs.count).to eq(2)
|
107
|
+
expect(as[1].bs.count).to eq(1)
|
108
|
+
expect(as[2].bs.count).to eq(0)
|
109
|
+
expect(as[0].bs[0].a_id).to eq(as[0].id)
|
110
|
+
expect(as[1].bs[0].a_id).to eq(as[1].id)
|
111
|
+
end
|
86
112
|
end
|
87
113
|
|
88
114
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: capsula
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rodion V
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-05-29 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: The tool for encapsulating (preloading, including) related objects
|
14
14
|
email: rodion.v@devaer.com
|