vorpal 0.0.7.rc1 → 0.0.7.rc2
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/CHANGELOG.md +1 -0
- data/README.md +5 -4
- data/lib/vorpal/aggregate_repository.rb +50 -244
- data/lib/vorpal/configuration.rb +5 -5
- data/lib/vorpal/db_driver.rb +44 -3
- data/lib/vorpal/db_loader.rb +7 -1
- data/lib/vorpal/engine.rb +241 -0
- data/lib/vorpal/version.rb +1 -1
- data/lib/vorpal.rb +4 -3
- data/spec/vorpal/acceptance/aggregate_repository_spec.rb +103 -65
- data/spec/vorpal/performance/performance_spec.rb +13 -12
- data/spec/vorpal/unit/config_builder_spec.rb +1 -0
- data/vorpal.gemspec +1 -1
- metadata +6 -4
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'vorpal/identity_map'
|
2
|
+
require 'vorpal/aggregate_utils'
|
3
|
+
require 'vorpal/db_loader'
|
4
|
+
require 'vorpal/db_driver'
|
5
|
+
require 'vorpal/aggregate_repository'
|
6
|
+
|
7
|
+
module Vorpal
|
8
|
+
class Engine
|
9
|
+
# @private
|
10
|
+
def initialize(db_driver, master_config)
|
11
|
+
@db_driver = db_driver
|
12
|
+
@configs = master_config
|
13
|
+
end
|
14
|
+
|
15
|
+
# Creates a repository for saving/updating/loading/destroying an aggregate to/from
|
16
|
+
# the DB. It is possible to use the methods directly on the {Engine}.
|
17
|
+
#
|
18
|
+
# @param domain_class [Class] Class of the root of the aggregate.
|
19
|
+
# @return [AggregateRepository] Repository suitable for mapping a single aggregate.
|
20
|
+
def repository_for(domain_class)
|
21
|
+
AggregateRepository.new(domain_class, self)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Try to use {AggregateRepository#persist} instead.
|
25
|
+
def persist(roots)
|
26
|
+
roots = wrap(roots)
|
27
|
+
return roots if roots.empty?
|
28
|
+
raise InvalidAggregateRoot, 'Nil aggregate roots are not allowed.' if roots.any?(&:nil?)
|
29
|
+
|
30
|
+
all_owned_objects = all_owned_objects(roots)
|
31
|
+
mapping = {}
|
32
|
+
loaded_db_objects = load_owned_from_db(roots.map(&:id).compact, roots.first.class)
|
33
|
+
|
34
|
+
serialize(all_owned_objects, mapping, loaded_db_objects)
|
35
|
+
new_objects = get_unsaved_objects(mapping.keys)
|
36
|
+
begin
|
37
|
+
set_primary_keys(all_owned_objects, mapping)
|
38
|
+
set_foreign_keys(all_owned_objects, mapping)
|
39
|
+
remove_orphans(mapping, loaded_db_objects)
|
40
|
+
save(all_owned_objects, new_objects, mapping)
|
41
|
+
|
42
|
+
return roots
|
43
|
+
rescue Exception
|
44
|
+
nil_out_object_ids(new_objects)
|
45
|
+
raise
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Try to use {AggregateRepository#load_one} instead.
|
50
|
+
def load_one(db_root, domain_class, identity_map)
|
51
|
+
load_many(Array(db_root), domain_class, identity_map).first
|
52
|
+
end
|
53
|
+
|
54
|
+
# Try to use {AggregateRepository#load_many} instead.
|
55
|
+
def load_many(db_roots, domain_class, identity_map)
|
56
|
+
raise InvalidAggregateRoot, 'Nil aggregate roots are not allowed.' if db_roots.any?(&:nil?)
|
57
|
+
|
58
|
+
loaded_db_objects = DbLoader.new(false, @db_driver).load_from_db_objects(db_roots, @configs.config_for(domain_class))
|
59
|
+
deserialize(loaded_db_objects, identity_map)
|
60
|
+
set_associations(loaded_db_objects, identity_map)
|
61
|
+
|
62
|
+
db_roots.map { |db_object| identity_map.get(db_object) }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Try to use {AggregateRepository#destroy} instead.
|
66
|
+
def destroy(roots)
|
67
|
+
roots = wrap(roots)
|
68
|
+
return roots if roots.empty?
|
69
|
+
raise InvalidAggregateRoot, 'Nil aggregate roots are not allowed.' if roots.any?(&:nil?)
|
70
|
+
|
71
|
+
destroy_by_id(roots.map(&:id), roots.first.class)
|
72
|
+
roots
|
73
|
+
end
|
74
|
+
|
75
|
+
# Try to use {AggregateRepository#destroy_by_id} instead.
|
76
|
+
def destroy_by_id(ids, domain_class)
|
77
|
+
ids = wrap(ids)
|
78
|
+
raise InvalidPrimaryKeyValue, 'Nil primary key values are not allowed.' if ids.any?(&:nil?)
|
79
|
+
|
80
|
+
loaded_db_objects = load_owned_from_db(ids, domain_class)
|
81
|
+
loaded_db_objects.each do |config, db_objects|
|
82
|
+
@db_driver.destroy(config, db_objects.map(&:id))
|
83
|
+
end
|
84
|
+
ids
|
85
|
+
end
|
86
|
+
|
87
|
+
# Try to use {AggregateRepository#db_class} instead.
|
88
|
+
def db_class(domain_class)
|
89
|
+
@configs.config_for(domain_class).db_class
|
90
|
+
end
|
91
|
+
|
92
|
+
def query(domain_class)
|
93
|
+
@db_driver.query(@configs.config_for(domain_class))
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def wrap(collection_or_not)
|
99
|
+
if collection_or_not.is_a?(Array)
|
100
|
+
collection_or_not
|
101
|
+
else
|
102
|
+
[collection_or_not]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def all_owned_objects(roots)
|
107
|
+
AggregateUtils.group_by_type(roots, @configs)
|
108
|
+
end
|
109
|
+
|
110
|
+
def load_from_db(ids, domain_class, only_owned=false)
|
111
|
+
DbLoader.new(only_owned, @db_driver).load_from_db(ids, @configs.config_for(domain_class))
|
112
|
+
end
|
113
|
+
|
114
|
+
def load_owned_from_db(ids, domain_class)
|
115
|
+
load_from_db(ids, domain_class, true)
|
116
|
+
end
|
117
|
+
|
118
|
+
def deserialize(loaded_db_objects, identity_map)
|
119
|
+
loaded_db_objects.flat_map do |config, db_objects|
|
120
|
+
db_objects.map do |db_object|
|
121
|
+
# TODO: There is a bug here when you have something in the IdentityMap that is stale and needs to be updated.
|
122
|
+
identity_map.get_and_set(db_object) { config.deserialize(db_object) }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def set_associations(loaded_db_objects, identity_map)
|
128
|
+
loaded_db_objects.each do |config, db_objects|
|
129
|
+
db_objects.each do |db_object|
|
130
|
+
config.local_association_configs.each do |association_config|
|
131
|
+
db_remote = loaded_db_objects.find_by_id(
|
132
|
+
association_config.remote_class_config(db_object),
|
133
|
+
association_config.fk_value(db_object)
|
134
|
+
)
|
135
|
+
association_config.associate(identity_map.get(db_object), identity_map.get(db_remote))
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def serialize(owned_objects, mapping, loaded_db_objects)
|
142
|
+
owned_objects.each do |config, objects|
|
143
|
+
objects.each do |object|
|
144
|
+
db_object = serialize_object(object, config, loaded_db_objects)
|
145
|
+
mapping[object] = db_object
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def serialize_object(object, config, loaded_db_objects)
|
151
|
+
if config.serialization_required?
|
152
|
+
attributes = config.serialize(object)
|
153
|
+
if object.id.nil?
|
154
|
+
config.build_db_object(attributes)
|
155
|
+
else
|
156
|
+
db_object = loaded_db_objects.find_by_id(config, object.id)
|
157
|
+
config.set_db_object_attributes(db_object, attributes)
|
158
|
+
db_object
|
159
|
+
end
|
160
|
+
else
|
161
|
+
object
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def set_primary_keys(owned_objects, mapping)
|
166
|
+
owned_objects.each do |config, objects|
|
167
|
+
in_need_of_primary_keys = objects.find_all { |obj| obj.id.nil? }
|
168
|
+
primary_keys = @db_driver.get_primary_keys(config, in_need_of_primary_keys.length)
|
169
|
+
in_need_of_primary_keys.zip(primary_keys).each do |object, primary_key|
|
170
|
+
mapping[object].id = primary_key
|
171
|
+
object.id = primary_key
|
172
|
+
end
|
173
|
+
end
|
174
|
+
mapping.rehash # needs to happen because setting the id on an AR::Base model changes its hash value
|
175
|
+
end
|
176
|
+
|
177
|
+
def set_foreign_keys(owned_objects, mapping)
|
178
|
+
owned_objects.each do |config, objects|
|
179
|
+
objects.each do |object|
|
180
|
+
config.has_manys.each do |has_many_config|
|
181
|
+
if has_many_config.owned
|
182
|
+
children = has_many_config.get_children(object)
|
183
|
+
children.each do |child|
|
184
|
+
has_many_config.set_foreign_key(mapping[child], object)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
config.has_ones.each do |has_one_config|
|
190
|
+
if has_one_config.owned
|
191
|
+
child = has_one_config.get_child(object)
|
192
|
+
has_one_config.set_foreign_key(mapping[child], object)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
config.belongs_tos.each do |belongs_to_config|
|
197
|
+
child = belongs_to_config.get_child(object)
|
198
|
+
belongs_to_config.set_foreign_key(mapping[object], child)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def save(owned_objects, new_objects, mapping)
|
205
|
+
grouped_new_objects = new_objects.group_by { |obj| @configs.config_for(obj.class) }
|
206
|
+
owned_objects.each do |config, objects|
|
207
|
+
objects_to_insert = grouped_new_objects[config] || []
|
208
|
+
db_objects_to_insert = objects_to_insert.map { |obj| mapping[obj] }
|
209
|
+
@db_driver.insert(config, db_objects_to_insert)
|
210
|
+
|
211
|
+
objects_to_update = objects - objects_to_insert
|
212
|
+
db_objects_to_update = objects_to_update.map { |obj| mapping[obj] }
|
213
|
+
@db_driver.update(config, db_objects_to_update)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def remove_orphans(mapping, loaded_db_objects)
|
218
|
+
db_objects_in_aggregate = mapping.values
|
219
|
+
db_objects_in_db = loaded_db_objects.all_objects
|
220
|
+
all_orphans = db_objects_in_db - db_objects_in_aggregate
|
221
|
+
grouped_orphans = all_orphans.group_by { |o| @configs.config_for_db_object(o) }
|
222
|
+
grouped_orphans.each do |config, orphans|
|
223
|
+
@db_driver.destroy(config, orphans)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def get_unsaved_objects(objects)
|
228
|
+
objects.find_all { |object| object.id.nil? }
|
229
|
+
end
|
230
|
+
|
231
|
+
def nil_out_object_ids(objects)
|
232
|
+
objects ||= []
|
233
|
+
objects.each { |object| object.id = nil }
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
class InvalidPrimaryKeyValue < StandardError
|
238
|
+
end
|
239
|
+
class InvalidAggregateRoot < StandardError
|
240
|
+
end
|
241
|
+
end
|
data/lib/vorpal/version.rb
CHANGED
data/lib/vorpal.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
require "vorpal/version"
|
2
2
|
require "vorpal/configuration"
|
3
3
|
|
4
|
-
# Allows easy creation of {Vorpal::
|
5
|
-
# instances.
|
4
|
+
# Allows easy creation of {Vorpal::Engine} instances.
|
6
5
|
#
|
7
6
|
# ```ruby
|
8
|
-
#
|
7
|
+
# engine = Vorpal.define do
|
9
8
|
# map Tree do
|
10
9
|
# attributes :name
|
11
10
|
# belongs_to :trunk
|
@@ -22,6 +21,8 @@ require "vorpal/configuration"
|
|
22
21
|
# belongs_to :tree
|
23
22
|
# end
|
24
23
|
# end
|
24
|
+
#
|
25
|
+
# repository = engine.repository_for(Tree)
|
25
26
|
# ```
|
26
27
|
module Vorpal
|
27
28
|
extend Vorpal::Configuration
|