vorpal 0.0.5.1 → 0.0.6.rc1
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 +8 -8
- data/lib/vorpal/aggregate_repository.rb +152 -236
- data/lib/vorpal/aggregate_traversal.rb +50 -0
- data/lib/vorpal/aggregate_utils.rb +37 -0
- data/lib/vorpal/config_builder.rb +2 -2
- data/lib/vorpal/configs.rb +73 -74
- data/lib/vorpal/db_driver.rb +50 -0
- data/lib/vorpal/db_loader.rb +131 -0
- data/lib/vorpal/identity_map.rb +4 -0
- data/lib/vorpal/loaded_objects.rb +53 -0
- data/lib/vorpal/util/array_hash.rb +13 -0
- data/lib/vorpal/{hash_initialization.rb → util/hash_initialization.rb} +0 -0
- data/lib/vorpal/version.rb +1 -1
- data/spec/integration_spec_helper.rb +3 -1
- data/spec/vorpal/aggregate_repository_spec.rb +60 -7
- data/vorpal.gemspec +3 -1
- metadata +41 -8
- data/lib/vorpal/traversal.rb +0 -98
@@ -0,0 +1,50 @@
|
|
1
|
+
module Vorpal
|
2
|
+
# @private
|
3
|
+
class AggregateTraversal
|
4
|
+
def initialize(configs)
|
5
|
+
@configs = configs
|
6
|
+
end
|
7
|
+
|
8
|
+
# Traversal should always begin with an object that is known to be
|
9
|
+
# able to reach all other objects in the aggregate (like the root!)
|
10
|
+
def accept(object, visitor, already_visited=[])
|
11
|
+
return if object.nil?
|
12
|
+
|
13
|
+
config = @configs.config_for(object.class)
|
14
|
+
return if config.nil?
|
15
|
+
|
16
|
+
return if already_visited.include?(object)
|
17
|
+
already_visited << object
|
18
|
+
|
19
|
+
visitor.visit_object(object, config)
|
20
|
+
|
21
|
+
config.belongs_tos.each do |belongs_to_config|
|
22
|
+
child = belongs_to_config.get_child(object)
|
23
|
+
accept(child, visitor, already_visited) if visitor.continue_traversal?(belongs_to_config)
|
24
|
+
end
|
25
|
+
|
26
|
+
config.has_ones.each do |has_one_config|
|
27
|
+
child = has_one_config.get_child(object)
|
28
|
+
accept(child, visitor, already_visited) if visitor.continue_traversal?(has_one_config)
|
29
|
+
end
|
30
|
+
|
31
|
+
config.has_manys.each do |has_many_config|
|
32
|
+
children = has_many_config.get_children(object)
|
33
|
+
children.each do |child|
|
34
|
+
accept(child, visitor, already_visited) if visitor.continue_traversal?(has_many_config)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @private
|
41
|
+
module AggregateVisitorTemplate
|
42
|
+
def visit_object(object, config)
|
43
|
+
# override me!
|
44
|
+
end
|
45
|
+
|
46
|
+
def continue_traversal?(association_config)
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'vorpal/aggregate_traversal'
|
2
|
+
|
3
|
+
module Vorpal
|
4
|
+
module AggregateUtils
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def group_by_type(roots, configs)
|
8
|
+
traversal = AggregateTraversal.new(configs)
|
9
|
+
|
10
|
+
all = roots.flat_map do |root|
|
11
|
+
owned_object_visitor = OwnedObjectVisitor.new
|
12
|
+
traversal.accept(root, owned_object_visitor)
|
13
|
+
owned_object_visitor.owned_objects
|
14
|
+
end
|
15
|
+
|
16
|
+
all.group_by { |obj| configs.config_for(obj.class) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# @private
|
21
|
+
class OwnedObjectVisitor
|
22
|
+
include AggregateVisitorTemplate
|
23
|
+
attr_reader :owned_objects
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@owned_objects = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def visit_object(object, config)
|
30
|
+
@owned_objects << object
|
31
|
+
end
|
32
|
+
|
33
|
+
def continue_traversal?(association_config)
|
34
|
+
association_config.owned
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -135,13 +135,13 @@ class ConfigBuilder
|
|
135
135
|
|
136
136
|
def serializer(attrs)
|
137
137
|
Class.new(SimpleSerializer::Serializer) do
|
138
|
-
|
138
|
+
attributes *attrs
|
139
139
|
end
|
140
140
|
end
|
141
141
|
|
142
142
|
def deserializer(attrs)
|
143
143
|
Class.new(SimpleSerializer::Deserializer) do
|
144
|
-
|
144
|
+
data_attributes *attrs
|
145
145
|
end
|
146
146
|
end
|
147
147
|
end
|
data/lib/vorpal/configs.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
require 'vorpal/hash_initialization'
|
1
|
+
require 'vorpal/util/hash_initialization'
|
2
|
+
require 'equalizer'
|
2
3
|
|
3
4
|
module Vorpal
|
4
5
|
|
@@ -39,6 +40,7 @@ end
|
|
39
40
|
|
40
41
|
# @private
|
41
42
|
class ClassConfig
|
43
|
+
include Equalizer.new(:domain_class, :db_class)
|
42
44
|
attr_reader :serializer, :deserializer, :domain_class, :db_class
|
43
45
|
attr_accessor :has_manys, :belongs_tos, :has_ones
|
44
46
|
|
@@ -52,33 +54,6 @@ class ClassConfig
|
|
52
54
|
end
|
53
55
|
end
|
54
56
|
|
55
|
-
def get_primary_keys(count)
|
56
|
-
result = ActiveRecord::Base.connection.execute("select nextval('#{sequence_name}') from generate_series(1,#{count});")
|
57
|
-
result.column_values(0).map(&:to_i)
|
58
|
-
end
|
59
|
-
|
60
|
-
def find_in_db(object)
|
61
|
-
load_by_id(object.id)
|
62
|
-
end
|
63
|
-
|
64
|
-
def load_by_id(id)
|
65
|
-
db_class.where(id: id).first
|
66
|
-
end
|
67
|
-
|
68
|
-
def load_by_foreign_key(id, foreign_key_info)
|
69
|
-
arel = db_class.where(foreign_key_info.fk_column => id)
|
70
|
-
arel = arel.where(foreign_key_info.fk_type_column => foreign_key_info.fk_type) if foreign_key_info.polymorphic?
|
71
|
-
arel.order(:id).all
|
72
|
-
end
|
73
|
-
|
74
|
-
def destroy(db_object)
|
75
|
-
db_object.destroy
|
76
|
-
end
|
77
|
-
|
78
|
-
def save(db_object)
|
79
|
-
db_object.save!
|
80
|
-
end
|
81
|
-
|
82
57
|
def build_db_object(attributes)
|
83
58
|
db_class.new(attributes)
|
84
59
|
end
|
@@ -112,15 +87,15 @@ class ClassConfig
|
|
112
87
|
db_object.send(field)
|
113
88
|
end
|
114
89
|
|
115
|
-
|
116
|
-
|
117
|
-
def sequence_name
|
118
|
-
"#{db_class.table_name}_id_seq"
|
90
|
+
def table_name
|
91
|
+
db_class.table_name
|
119
92
|
end
|
120
93
|
end
|
121
94
|
|
122
95
|
# @private
|
123
96
|
class ForeignKeyInfo
|
97
|
+
include Equalizer.new(:fk_column, :fk_type_column, :fk_type)
|
98
|
+
|
124
99
|
attr_reader :fk_column, :fk_type_column, :fk_type, :polymorphic
|
125
100
|
|
126
101
|
def initialize(fk_column, fk_type_column, fk_type, polymorphic)
|
@@ -133,6 +108,10 @@ class ForeignKeyInfo
|
|
133
108
|
def polymorphic?
|
134
109
|
@polymorphic
|
135
110
|
end
|
111
|
+
|
112
|
+
def matches_polymorphic_type?(db_object)
|
113
|
+
db_object.send(fk_type_column) == fk_type
|
114
|
+
end
|
136
115
|
end
|
137
116
|
|
138
117
|
# @private
|
@@ -157,33 +136,20 @@ class RelationalAssociation
|
|
157
136
|
local_config.set_field(local_db_model, fk_type, remote_model.class.name) if polymorphic?
|
158
137
|
end
|
159
138
|
|
160
|
-
def
|
161
|
-
|
162
|
-
|
163
|
-
raise "Only supports having one remote configuration when navigating from the remote side to the local side of an association." if remote_configs.size != 1
|
164
|
-
remote_config = remote_configs.first
|
165
|
-
local_config.load_by_foreign_key(id, foreign_key_info(remote_config))
|
166
|
-
end
|
167
|
-
|
168
|
-
def load_remote(local_db_model)
|
169
|
-
remote_config = polymorphic? ? remote_config_for_local_db_object(local_db_model) : remote_configs.first
|
170
|
-
remote_config.load_by_id(get_foreign_key(local_db_model))
|
139
|
+
def remote_config_for_local_db_object(local_db_model)
|
140
|
+
class_name = local_config.get_field(local_db_model, fk_type)
|
141
|
+
remote_configs.detect { |config| config.domain_class.name == class_name }
|
171
142
|
end
|
172
143
|
|
173
|
-
private
|
174
|
-
|
175
144
|
def polymorphic?
|
176
145
|
!fk_type.nil?
|
177
146
|
end
|
178
147
|
|
179
|
-
def foreign_key_info(
|
180
|
-
ForeignKeyInfo.new(fk, fk_type,
|
148
|
+
def foreign_key_info(remote_class_config)
|
149
|
+
ForeignKeyInfo.new(fk, fk_type, remote_class_config.domain_class.name, polymorphic?)
|
181
150
|
end
|
182
151
|
|
183
|
-
|
184
|
-
class_name = local_config.get_field(local_db_model, fk_type)
|
185
|
-
remote_configs.detect { |config| config.domain_class.name == class_name }
|
186
|
-
end
|
152
|
+
private
|
187
153
|
|
188
154
|
def get_foreign_key(local_db_model)
|
189
155
|
local_config.get_field(local_db_model, fk)
|
@@ -196,6 +162,7 @@ class HasManyConfig
|
|
196
162
|
attr_reader :name, :owned, :fk, :fk_type, :child_class
|
197
163
|
|
198
164
|
def init_relational_association(child_config, parent_config)
|
165
|
+
@parent_config = parent_config
|
199
166
|
@relational_association = RelationalAssociation.new(fk: fk, fk_type: fk_type, local_config: child_config, remote_configs: [parent_config])
|
200
167
|
end
|
201
168
|
|
@@ -211,36 +178,57 @@ class HasManyConfig
|
|
211
178
|
@relational_association.set_foreign_key(db_child, parent)
|
212
179
|
end
|
213
180
|
|
214
|
-
def
|
215
|
-
|
181
|
+
def associated?(db_parent, db_child)
|
182
|
+
return false if child_config.db_class != db_child.class
|
183
|
+
db_child.send(fk) == db_parent.id
|
216
184
|
end
|
217
|
-
end
|
218
185
|
|
219
|
-
|
220
|
-
|
221
|
-
include HashInitialization
|
222
|
-
attr_reader :name, :owned, :fk, :fk_type, :child_classes
|
223
|
-
|
224
|
-
def init_relational_association(child_configs, parent_config)
|
225
|
-
@relational_association = RelationalAssociation.new(fk: fk, fk_type: fk_type, local_config: parent_config, remote_configs: child_configs)
|
186
|
+
def child_config
|
187
|
+
@relational_association.local_config
|
226
188
|
end
|
227
189
|
|
228
|
-
def
|
229
|
-
|
190
|
+
def foreign_key_info
|
191
|
+
@relational_association.foreign_key_info(@parent_config)
|
230
192
|
end
|
193
|
+
end
|
194
|
+
# @private
|
195
|
+
class BelongsToConfig
|
196
|
+
include HashInitialization
|
197
|
+
attr_reader :name, :owned, :fk, :fk_type, :child_classes
|
231
198
|
|
232
|
-
|
233
|
-
|
234
|
-
|
199
|
+
def init_relational_association(child_configs, parent_config)
|
200
|
+
@relational_association = RelationalAssociation.new(fk: fk, fk_type: fk_type, local_config: parent_config, remote_configs: child_configs)
|
201
|
+
end
|
235
202
|
|
236
|
-
|
237
|
-
|
238
|
-
|
203
|
+
def get_child(parent)
|
204
|
+
parent.send(name)
|
205
|
+
end
|
206
|
+
|
207
|
+
def set_child(parent, child)
|
208
|
+
parent.send("#{name}=", child)
|
209
|
+
end
|
239
210
|
|
240
|
-
|
241
|
-
|
211
|
+
def set_foreign_key(db_parent, child)
|
212
|
+
@relational_association.set_foreign_key(db_parent, child)
|
213
|
+
end
|
214
|
+
|
215
|
+
def associated?(db_parent, db_child)
|
216
|
+
return false if child_config(db_parent).db_class != db_child.class
|
217
|
+
fk_value(db_parent) == db_child.id
|
218
|
+
end
|
219
|
+
|
220
|
+
def child_config(db_parent)
|
221
|
+
if @relational_association.polymorphic?
|
222
|
+
@relational_association.remote_config_for_local_db_object(db_parent)
|
223
|
+
else
|
224
|
+
@relational_association.remote_configs.first
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def fk_value(db_parent)
|
229
|
+
db_parent.send(fk)
|
230
|
+
end
|
242
231
|
end
|
243
|
-
end
|
244
232
|
|
245
233
|
# @private
|
246
234
|
class HasOneConfig
|
@@ -248,6 +236,7 @@ class HasOneConfig
|
|
248
236
|
attr_reader :name, :owned, :fk, :fk_type, :child_class
|
249
237
|
|
250
238
|
def init_relational_association(child_config, parent_config)
|
239
|
+
@parent_config = parent_config
|
251
240
|
@relational_association = RelationalAssociation.new(fk: fk, fk_type: fk_type, local_config: child_config, remote_configs: [parent_config])
|
252
241
|
end
|
253
242
|
|
@@ -263,8 +252,18 @@ class HasOneConfig
|
|
263
252
|
@relational_association.set_foreign_key(db_child, parent)
|
264
253
|
end
|
265
254
|
|
266
|
-
def
|
267
|
-
|
255
|
+
def associated?(db_parent, db_child)
|
256
|
+
return false if child_config.db_class != db_child.class
|
257
|
+
db_child.send(fk) == db_parent.id
|
258
|
+
end
|
259
|
+
|
260
|
+
def child_config
|
261
|
+
@relational_association.local_config
|
262
|
+
end
|
263
|
+
|
264
|
+
def foreign_key_info
|
265
|
+
@relational_association.foreign_key_info(@parent_config)
|
268
266
|
end
|
269
267
|
end
|
268
|
+
|
270
269
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Vorpal
|
2
|
+
module DbDriver
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def insert(config, db_objects)
|
6
|
+
if defined? ActiveRecord::Import
|
7
|
+
config.db_class.import db_objects
|
8
|
+
else
|
9
|
+
db_objects.each do |db_object|
|
10
|
+
db_object.save!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(config, db_objects)
|
16
|
+
db_objects.each do |db_object|
|
17
|
+
db_object.save!
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def destroy(config, db_objects)
|
22
|
+
config.db_class.delete_all(id: db_objects.map(&:id))
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_by_id(config, ids)
|
26
|
+
config.db_class.where(id: ids)
|
27
|
+
end
|
28
|
+
|
29
|
+
def load_by_foreign_key(config, id, foreign_key_info)
|
30
|
+
arel = config.db_class.where(foreign_key_info.fk_column => id)
|
31
|
+
arel = arel.where(foreign_key_info.fk_type_column => foreign_key_info.fk_type) if foreign_key_info.polymorphic?
|
32
|
+
arel.order(:id).all
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_primary_keys(config, count)
|
36
|
+
result = execute("select nextval('#{sequence_name(config)}') from generate_series(1,#{count});")
|
37
|
+
result.column_values(0).map(&:to_i)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def execute(sql)
|
43
|
+
ActiveRecord::Base.connection.execute(sql)
|
44
|
+
end
|
45
|
+
|
46
|
+
def sequence_name(config)
|
47
|
+
"#{config.table_name}_id_seq"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'vorpal/loaded_objects'
|
2
|
+
require 'vorpal/util/array_hash'
|
3
|
+
require 'vorpal/db_driver'
|
4
|
+
|
5
|
+
module Vorpal
|
6
|
+
|
7
|
+
# @private
|
8
|
+
class DbLoader
|
9
|
+
def initialize(configs, only_owned)
|
10
|
+
@configs = configs
|
11
|
+
@only_owned = only_owned
|
12
|
+
end
|
13
|
+
|
14
|
+
def load_from_db(ids, domain_class)
|
15
|
+
config = @configs.config_for(domain_class)
|
16
|
+
@loaded_objects = LoadedObjects.new
|
17
|
+
@lookup_instructions = LookupInstructions.new
|
18
|
+
@lookup_instructions.lookup_by_id(config, ids)
|
19
|
+
|
20
|
+
until @lookup_instructions.empty?
|
21
|
+
lookup = @lookup_instructions.next_lookup
|
22
|
+
new_objects = lookup.load_all
|
23
|
+
@loaded_objects.add(lookup.config, new_objects)
|
24
|
+
explore_objects(new_objects)
|
25
|
+
end
|
26
|
+
|
27
|
+
@loaded_objects
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def explore_objects(objects_to_explore)
|
33
|
+
objects_to_explore.each do |db_object|
|
34
|
+
config = @configs.config_for_db(db_object.class)
|
35
|
+
config.has_manys.each do |has_many_config|
|
36
|
+
lookup_by_fk(db_object, has_many_config) if explore_association?(has_many_config)
|
37
|
+
end
|
38
|
+
|
39
|
+
config.has_ones.each do |has_one_config|
|
40
|
+
lookup_by_fk(db_object, has_one_config) if explore_association?(has_one_config)
|
41
|
+
end
|
42
|
+
|
43
|
+
config.belongs_tos.each do |belongs_to_config|
|
44
|
+
lookup_by_id(db_object, belongs_to_config) if explore_association?(belongs_to_config)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def explore_association?(association_config)
|
50
|
+
!@only_owned || association_config.owned == true
|
51
|
+
end
|
52
|
+
|
53
|
+
def lookup_by_id(db_object, belongs_to_config)
|
54
|
+
child_config = belongs_to_config.child_config(db_object)
|
55
|
+
id = belongs_to_config.fk_value(db_object)
|
56
|
+
return if @loaded_objects.id_lookup_done?(child_config, id)
|
57
|
+
@lookup_instructions.lookup_by_id(child_config, id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def lookup_by_fk(db_object, has_many_config)
|
61
|
+
child_config = has_many_config.child_config
|
62
|
+
fk_info = has_many_config.foreign_key_info
|
63
|
+
fk_value = db_object.id
|
64
|
+
return if @loaded_objects.fk_lookup_done?(child_config, fk_info, fk_value)
|
65
|
+
@lookup_instructions.lookup_by_fk(child_config, fk_info, fk_value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# @private
|
70
|
+
class LookupInstructions
|
71
|
+
include ArrayHash
|
72
|
+
def initialize
|
73
|
+
@lookup_by_id = {}
|
74
|
+
@lookup_by_fk = {}
|
75
|
+
end
|
76
|
+
|
77
|
+
def lookup_by_id(config, ids)
|
78
|
+
add_to_hash(@lookup_by_id, config, Array(ids))
|
79
|
+
end
|
80
|
+
|
81
|
+
def lookup_by_fk(config, fk_info, fk_value)
|
82
|
+
add_to_hash(@lookup_by_fk, [config, fk_info], fk_value)
|
83
|
+
end
|
84
|
+
|
85
|
+
def next_lookup
|
86
|
+
if @lookup_by_id.empty?
|
87
|
+
config, fk_info = @lookup_by_fk.first.first
|
88
|
+
fk_values = @lookup_by_fk.delete([config, fk_info])
|
89
|
+
LookupByFk.new(config, fk_info, fk_values)
|
90
|
+
else
|
91
|
+
config = @lookup_by_id.first.first
|
92
|
+
ids = @lookup_by_id.delete(config)
|
93
|
+
LookupById.new(config, ids)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def empty?
|
98
|
+
@lookup_by_id.empty? && @lookup_by_fk.empty?
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# @private
|
103
|
+
class LookupById
|
104
|
+
attr_reader :config
|
105
|
+
def initialize(config, ids)
|
106
|
+
@config = config
|
107
|
+
@ids = ids
|
108
|
+
end
|
109
|
+
|
110
|
+
def load_all
|
111
|
+
return [] if @ids.empty?
|
112
|
+
DbDriver.load_by_id(@config, @ids)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# @private
|
117
|
+
class LookupByFk
|
118
|
+
attr_reader :config
|
119
|
+
def initialize(config, fk_info, fk_values)
|
120
|
+
@config = config
|
121
|
+
@fk_info = fk_info
|
122
|
+
@fk_values = fk_values
|
123
|
+
end
|
124
|
+
|
125
|
+
def load_all
|
126
|
+
return [] if @fk_values.empty?
|
127
|
+
DbDriver.load_by_foreign_key(@config, @fk_values, @fk_info)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|