vorpal 0.0.5.1 → 0.0.6.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|