vorpal 0.0.6.rc4 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/README.md +10 -3
- data/lib/vorpal/aggregate_repository.rb +227 -190
- data/lib/vorpal/aggregate_traversal.rb +35 -35
- data/lib/vorpal/aggregate_utils.rb +1 -0
- data/lib/vorpal/config_builder.rb +120 -119
- data/lib/vorpal/configs.rb +22 -8
- data/lib/vorpal/configuration.rb +43 -40
- data/lib/vorpal/db_driver.rb +14 -2
- data/lib/vorpal/db_loader.rb +102 -104
- data/lib/vorpal/identity_map.rb +25 -25
- data/lib/vorpal/loaded_objects.rb +29 -28
- data/lib/vorpal/util/array_hash.rb +12 -14
- data/lib/vorpal/version.rb +1 -1
- data/spec/vorpal/{aggregate_repository_spec.rb → acceptance/aggregate_repository_spec.rb} +133 -2
- data/spec/vorpal/{performance_spec.rb → acceptance/performance_spec.rb} +4 -4
- data/spec/vorpal/{class_config_builder_spec.rb → unit/class_config_builder_spec.rb} +0 -0
- data/spec/vorpal/{configs_spec.rb → unit/configs_spec.rb} +0 -0
- data/spec/vorpal/{identity_map_spec.rb → unit/identity_map_spec.rb} +0 -0
- data/spec/vorpal/unit/loaded_objects_spec.rb +22 -0
- data/vorpal.gemspec +1 -0
- metadata +30 -14
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
MzY2ZTAwNWJiYmFlY2Y5NGZkZjg4ZTUyMGU4M2U4NjNiNzkxNzdkMg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
YTQ0OGZiZGIwOTdmMjcyYmRjZTVmOWRjZGI0ODgzOWRkZTI5OWUxNQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZDBiYjBhZDk0ZjY3MjJmMWRjZDcwMWQ3YzAxMGRhYzNiNjkzZjZlMzk5MmIz
|
10
|
+
ZWVmY2M1YjBiY2NhY2IyZmEyOThkMjA5MGI5YzUzZGJkMTU3Mzg0NWU4NGY5
|
11
|
+
Zjc3OGVhMmE1ZTExMzg1YWI1MjhlMjU2NjI2MTdjNzQzZGY1MzU=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
NTNlMTA1NjRiYzc1ODVhNGU5MGNlODIyYTNmOGY0YTQxNTBiMTYzN2UwMzdm
|
14
|
+
YTk0MTIyNTAxZmNhMzY3MTk5YjEyM2M1ZmIzNTIzMzU2M2FlZWQ2YjM4OGZj
|
15
|
+
MTU4YjU5ZTlkZmIzZTMzMjAzMWMxNWQ2OGY5YTI5MGZiZTQ3Y2Q=
|
data/README.md
CHANGED
@@ -72,7 +72,7 @@ class Tree
|
|
72
72
|
end
|
73
73
|
```
|
74
74
|
|
75
|
-
In this aggregate, the Tree is the root and the Branches are inside the aggregate boundary. The Gardener is not technically part of the aggregate but is required for the aggregate to make sense so we say that it is on the aggregate boundary.
|
75
|
+
In this aggregate, the Tree is the root and the Branches are inside the aggregate boundary. The Gardener is not technically part of the aggregate but is required for the aggregate to make sense so we say that it is on the aggregate boundary. Only objects that are inside the aggregate boundary will be saved, updated, or destroyed by Vorpal.
|
76
76
|
|
77
77
|
POROs must have setters and getters for all fields and associations that are to be persisted. They must also provide a no argument constructor.
|
78
78
|
|
@@ -143,6 +143,10 @@ module TreeRepository
|
|
143
143
|
def destroy(tree)
|
144
144
|
@repository.destroy(tree)
|
145
145
|
end
|
146
|
+
|
147
|
+
def destroy_by_id(tree_id)
|
148
|
+
@repository.destroy_by_id(tree_id, Tree)
|
149
|
+
end
|
146
150
|
end
|
147
151
|
```
|
148
152
|
|
@@ -162,6 +166,9 @@ small_tree = TreeRepository.find(small_tree_id)
|
|
162
166
|
# Destroys the given Tree as well as all Branches referenced by it,
|
163
167
|
# but not Gardeners.
|
164
168
|
TreeRepository.destroy(dead_tree)
|
169
|
+
|
170
|
+
# Or
|
171
|
+
TreeRepository.destroy_by_id(dead_tree_id)
|
165
172
|
```
|
166
173
|
|
167
174
|
## API Documentation
|
@@ -188,9 +195,9 @@ It also does not do some things that you might expect from other ORMs:
|
|
188
195
|
* Support for other ORMs.
|
189
196
|
* Value objects.
|
190
197
|
* Remove dependency on ActiveRecord (optimistic locking? updated_at, created_at support? Data type conversions? TimeZone support?)
|
191
|
-
* More efficient
|
198
|
+
* More efficient updates (use fewer queries.)
|
192
199
|
* Nicer DSL for specifying field that have different names in the domain model than in the DB.
|
193
|
-
|
200
|
+
|
194
201
|
## FAQ
|
195
202
|
|
196
203
|
**Q.** Why do I care about separating my persistence mechanism from my domain models?
|
@@ -4,240 +4,277 @@ require 'vorpal/db_loader'
|
|
4
4
|
require 'vorpal/db_driver'
|
5
5
|
|
6
6
|
module Vorpal
|
7
|
-
class AggregateRepository
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
7
|
+
class AggregateRepository
|
8
|
+
# @private
|
9
|
+
def initialize(db_driver, master_config)
|
10
|
+
@db_driver = db_driver
|
11
|
+
@configs = master_config
|
12
|
+
end
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
14
|
+
# Saves an aggregate to the DB. Inserts objects that are new to the
|
15
|
+
# aggregate, updates existing objects and deletes objects that are no longer
|
16
|
+
# present.
|
17
|
+
#
|
18
|
+
# Objects that are on the boundary of the aggregate (owned: false) will not
|
19
|
+
# be inserted, updated, or deleted. However, the relationships to these
|
20
|
+
# objects (provided they are stored within the aggregate) will be saved.
|
21
|
+
#
|
22
|
+
# @param root [Object] Root of the aggregate to be saved.
|
23
|
+
# @return [Object] Root of the aggregate.
|
24
|
+
def persist(root)
|
25
|
+
persist_all([root]).first
|
26
|
+
end
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
28
|
+
# Like {#persist} but operates on multiple aggregates. Roots must
|
29
|
+
# be of the same type.
|
30
|
+
#
|
31
|
+
# @param roots [[Object]] array of aggregate roots to be saved.
|
32
|
+
# @return [[Object]] array of aggregate roots.
|
33
|
+
def persist_all(roots)
|
34
|
+
return roots if roots.empty?
|
35
|
+
raise InvalidAggregateRoot, 'Nil aggregate roots are not allowed.' if roots.any?(&:nil?)
|
36
|
+
|
37
|
+
all_owned_objects = all_owned_objects(roots)
|
38
|
+
mapping = {}
|
39
|
+
loaded_db_objects = load_owned_from_db(roots.map(&:id).compact, roots.first.class)
|
40
|
+
|
41
|
+
serialize(all_owned_objects, mapping, loaded_db_objects)
|
42
|
+
new_objects = get_unsaved_objects(mapping.keys)
|
43
|
+
begin
|
44
|
+
set_primary_keys(all_owned_objects, mapping)
|
45
|
+
set_foreign_keys(all_owned_objects, mapping)
|
46
|
+
remove_orphans(mapping, loaded_db_objects)
|
47
|
+
save(all_owned_objects, new_objects, mapping)
|
48
|
+
|
49
|
+
return roots
|
50
|
+
rescue Exception
|
51
|
+
nil_out_object_ids(new_objects)
|
52
|
+
raise
|
53
|
+
end
|
52
54
|
end
|
53
|
-
end
|
54
55
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
56
|
+
# Loads an aggregate from the DB. Will eagerly load all objects in the
|
57
|
+
# aggregate and on the boundary (owned: false).
|
58
|
+
#
|
59
|
+
# @param id [Integer] Primary key value of the root of the aggregate to be
|
60
|
+
# loaded.
|
61
|
+
# @param domain_class [Class] Type of the root of the aggregate to
|
62
|
+
# be loaded.
|
63
|
+
# @param identity_map [Vorpal::IdentityMap] Provide your own IdentityMap instance
|
64
|
+
# if you want entity id - unique object mapping for a greater scope than one
|
65
|
+
# operation.
|
66
|
+
# @return [Object] Entity with the given primary key value and type.
|
67
|
+
def load(id, domain_class, identity_map=IdentityMap.new)
|
68
|
+
load_all([id], domain_class, identity_map).first
|
69
|
+
end
|
69
70
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
objects = deserialize(loaded_db_objects, identity_map)
|
82
|
-
set_associations(loaded_db_objects, identity_map)
|
83
|
-
|
84
|
-
objects.select { |obj| obj.class == domain_class }
|
85
|
-
end
|
71
|
+
# Like {#load} but operates on multiple ids.
|
72
|
+
#
|
73
|
+
# @param ids [[Integer]] Array of primary key values of the roots of the
|
74
|
+
# aggregates to be loaded.
|
75
|
+
# @param domain_class [Class] Type of the roots of the aggregate to be loaded.
|
76
|
+
# @param identity_map [Vorpal::IdentityMap] Provide your own IdentityMap instance
|
77
|
+
# if you want entity id - unique object mapping for a greater scope than one
|
78
|
+
# operation.
|
79
|
+
# @return [[Object]] Entities with the given primary key values and type.
|
80
|
+
def load_all(ids, domain_class, identity_map=IdentityMap.new)
|
81
|
+
raise InvalidPrimaryKeyValue, 'Nil primary key values are not allowed.' if ids.any?(&:nil?)
|
86
82
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
# @param root [Object] Root of the aggregate to be destroyed.
|
91
|
-
# @return [Object] Root that was passed in.
|
92
|
-
def destroy(root)
|
93
|
-
destroy_all(Array(root)).first
|
94
|
-
end
|
83
|
+
loaded_db_objects = load_from_db(ids, domain_class)
|
84
|
+
objects = deserialize(loaded_db_objects, identity_map)
|
85
|
+
set_associations(loaded_db_objects, identity_map)
|
95
86
|
|
96
|
-
|
97
|
-
|
98
|
-
#
|
99
|
-
# @param roots [[Object]] Array of roots of the aggregates to be destroyed.
|
100
|
-
# @return [[Object]] Roots that were passed in.
|
101
|
-
def destroy_all(roots)
|
102
|
-
return roots if roots.empty?
|
103
|
-
loaded_db_objects = load_owned_from_db(roots.map(&:id), roots.first.class)
|
104
|
-
loaded_db_objects.each do |config, db_objects|
|
105
|
-
@db_driver.destroy(config, db_objects)
|
106
|
-
end
|
107
|
-
roots
|
108
|
-
end
|
87
|
+
sorted_roots(ids, objects, domain_class)
|
88
|
+
end
|
109
89
|
|
110
|
-
|
90
|
+
# Removes an aggregate from the DB. Even if the aggregate contains unsaved
|
91
|
+
# changes this method will correctly remove everything.
|
92
|
+
#
|
93
|
+
# @param root [Object] Root of the aggregate to be destroyed.
|
94
|
+
# @return [Object] Root that was passed in.
|
95
|
+
def destroy(root)
|
96
|
+
destroy_all([root]).first
|
97
|
+
end
|
111
98
|
|
112
|
-
|
113
|
-
|
114
|
-
|
99
|
+
# Like {#destroy} but operates on multiple aggregates. Roots must
|
100
|
+
# be of the same type.
|
101
|
+
#
|
102
|
+
# @param roots [[Object]] Array of roots of the aggregates to be destroyed.
|
103
|
+
# @return [[Object]] Roots that were passed in.
|
104
|
+
def destroy_all(roots)
|
105
|
+
return roots if roots.empty?
|
106
|
+
raise InvalidAggregateRoot, 'Nil aggregate roots are not allowed.' if roots.any?(&:nil?)
|
115
107
|
|
116
|
-
|
117
|
-
|
118
|
-
|
108
|
+
destroy_all_by_id(roots.map(&:id), roots.first.class)
|
109
|
+
roots
|
110
|
+
end
|
119
111
|
|
120
|
-
|
121
|
-
|
122
|
-
|
112
|
+
# Removes an aggregate from the DB given its primary key.
|
113
|
+
#
|
114
|
+
# @param id [Integer] Id of root of the aggregate to be destroyed.
|
115
|
+
# @param domain_class [Class] Type of the root of the aggregate to
|
116
|
+
# be destroyed.
|
117
|
+
def destroy_by_id(id, domain_class)
|
118
|
+
destroy_all_by_id([id], domain_class)
|
119
|
+
end
|
123
120
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
121
|
+
# Like {#destroy_by_id} but operates on multiple ids. Roots must
|
122
|
+
# be of the same type.
|
123
|
+
#
|
124
|
+
# @param ids [[Integer]] Ids of roots of the aggregates to be destroyed.
|
125
|
+
# @param domain_class [Class] Type of the roots of the aggregates to
|
126
|
+
# be destroyed.
|
127
|
+
def destroy_all_by_id(ids, domain_class)
|
128
|
+
raise InvalidPrimaryKeyValue, 'Nil primary key values are not allowed.' if ids.any?(&:nil?)
|
129
|
+
|
130
|
+
loaded_db_objects = load_owned_from_db(ids, domain_class)
|
131
|
+
loaded_db_objects.each do |config, db_objects|
|
132
|
+
@db_driver.destroy(config, db_objects.map(&:id))
|
129
133
|
end
|
134
|
+
ids
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def all_owned_objects(roots)
|
140
|
+
AggregateUtils.group_by_type(roots, @configs)
|
141
|
+
end
|
142
|
+
|
143
|
+
def load_from_db(ids, domain_class, only_owned=false)
|
144
|
+
DbLoader.new(only_owned, @db_driver).load_from_db(ids, @configs.config_for(domain_class))
|
145
|
+
end
|
146
|
+
|
147
|
+
def load_owned_from_db(ids, domain_class)
|
148
|
+
load_from_db(ids, domain_class, true)
|
130
149
|
end
|
131
|
-
end
|
132
150
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
association_config.remote_class_config(db_object),
|
139
|
-
association_config.fk_value(db_object)
|
140
|
-
)
|
141
|
-
association_config.associate(identity_map.get(db_object), identity_map.get(db_remote))
|
151
|
+
def deserialize(loaded_db_objects, identity_map)
|
152
|
+
loaded_db_objects.flat_map do |config, db_objects|
|
153
|
+
db_objects.map do |db_object|
|
154
|
+
# TODO: There is a bug here when you have something in the IdentityMap that is stale and needs to be updated.
|
155
|
+
identity_map.get_and_set(db_object) { config.deserialize(db_object) }
|
142
156
|
end
|
143
157
|
end
|
144
158
|
end
|
145
|
-
end
|
146
159
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
160
|
+
def set_associations(loaded_db_objects, identity_map)
|
161
|
+
loaded_db_objects.each do |config, db_objects|
|
162
|
+
db_objects.each do |db_object|
|
163
|
+
config.local_association_configs.each do |association_config|
|
164
|
+
db_remote = loaded_db_objects.find_by_id(
|
165
|
+
association_config.remote_class_config(db_object),
|
166
|
+
association_config.fk_value(db_object)
|
167
|
+
)
|
168
|
+
association_config.associate(identity_map.get(db_object), identity_map.get(db_remote))
|
169
|
+
end
|
170
|
+
end
|
152
171
|
end
|
153
172
|
end
|
154
|
-
end
|
155
173
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
174
|
+
def sorted_roots(ids, objects, domain_class)
|
175
|
+
roots = objects.select { |obj| obj.class == domain_class }
|
176
|
+
roots_by_id = roots.reduce({}) { |h, root| h[root.id] = root; h }
|
177
|
+
ids.map { |id| roots_by_id[id] }.compact
|
178
|
+
end
|
179
|
+
|
180
|
+
def serialize(owned_objects, mapping, loaded_db_objects)
|
181
|
+
owned_objects.each do |config, objects|
|
182
|
+
objects.each do |object|
|
183
|
+
db_object = serialize_object(object, config, loaded_db_objects)
|
184
|
+
mapping[object] = db_object
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def serialize_object(object, config, loaded_db_objects)
|
190
|
+
if config.serialization_required?
|
191
|
+
attributes = config.serialize(object)
|
192
|
+
if object.id.nil?
|
193
|
+
config.build_db_object(attributes)
|
194
|
+
else
|
195
|
+
db_object = loaded_db_objects.find_by_id(config, object.id)
|
196
|
+
config.set_db_object_attributes(db_object, attributes)
|
197
|
+
db_object
|
198
|
+
end
|
161
199
|
else
|
162
|
-
|
163
|
-
config.set_db_object_attributes(db_object, attributes)
|
164
|
-
db_object
|
200
|
+
object
|
165
201
|
end
|
166
|
-
else
|
167
|
-
object
|
168
202
|
end
|
169
|
-
end
|
170
203
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
204
|
+
def set_primary_keys(owned_objects, mapping)
|
205
|
+
owned_objects.each do |config, objects|
|
206
|
+
in_need_of_primary_keys = objects.find_all { |obj| obj.id.nil? }
|
207
|
+
primary_keys = @db_driver.get_primary_keys(config, in_need_of_primary_keys.length)
|
208
|
+
in_need_of_primary_keys.zip(primary_keys).each do |object, primary_key|
|
209
|
+
mapping[object].id = primary_key
|
210
|
+
object.id = primary_key
|
211
|
+
end
|
178
212
|
end
|
213
|
+
mapping.rehash # needs to happen because setting the id on an AR::Base model changes its hash value
|
179
214
|
end
|
180
|
-
mapping.rehash # needs to happen because setting the id on an AR::Base model changes its hash value
|
181
|
-
end
|
182
215
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
216
|
+
def set_foreign_keys(owned_objects, mapping)
|
217
|
+
owned_objects.each do |config, objects|
|
218
|
+
objects.each do |object|
|
219
|
+
config.has_manys.each do |has_many_config|
|
220
|
+
if has_many_config.owned
|
221
|
+
children = has_many_config.get_children(object)
|
222
|
+
children.each do |child|
|
223
|
+
has_many_config.set_foreign_key(mapping[child], object)
|
224
|
+
end
|
191
225
|
end
|
192
226
|
end
|
193
|
-
end
|
194
227
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
228
|
+
config.has_ones.each do |has_one_config|
|
229
|
+
if has_one_config.owned
|
230
|
+
child = has_one_config.get_child(object)
|
231
|
+
has_one_config.set_foreign_key(mapping[child], object)
|
232
|
+
end
|
199
233
|
end
|
200
|
-
end
|
201
234
|
|
202
|
-
|
203
|
-
|
204
|
-
|
235
|
+
config.belongs_tos.each do |belongs_to_config|
|
236
|
+
child = belongs_to_config.get_child(object)
|
237
|
+
belongs_to_config.set_foreign_key(mapping[object], child)
|
238
|
+
end
|
205
239
|
end
|
206
240
|
end
|
207
241
|
end
|
208
|
-
end
|
209
242
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
243
|
+
def save(owned_objects, new_objects, mapping)
|
244
|
+
grouped_new_objects = new_objects.group_by { |obj| @configs.config_for(obj.class) }
|
245
|
+
owned_objects.each do |config, objects|
|
246
|
+
objects_to_insert = grouped_new_objects[config] || []
|
247
|
+
db_objects_to_insert = objects_to_insert.map { |obj| mapping[obj] }
|
248
|
+
@db_driver.insert(config, db_objects_to_insert)
|
216
249
|
|
217
|
-
|
218
|
-
|
219
|
-
|
250
|
+
objects_to_update = objects - objects_to_insert
|
251
|
+
db_objects_to_update = objects_to_update.map { |obj| mapping[obj] }
|
252
|
+
@db_driver.update(config, db_objects_to_update)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def remove_orphans(mapping, loaded_db_objects)
|
257
|
+
db_objects_in_aggregate = mapping.values
|
258
|
+
db_objects_in_db = loaded_db_objects.all_objects
|
259
|
+
all_orphans = db_objects_in_db - db_objects_in_aggregate
|
260
|
+
grouped_orphans = all_orphans.group_by { |o| @configs.config_for_db_object(o) }
|
261
|
+
grouped_orphans.each do |config, orphans|
|
262
|
+
@db_driver.destroy(config, orphans)
|
263
|
+
end
|
220
264
|
end
|
221
|
-
end
|
222
265
|
|
223
|
-
|
224
|
-
|
225
|
-
db_objects_in_db = loaded_db_objects.all_objects
|
226
|
-
all_orphans = db_objects_in_db - db_objects_in_aggregate
|
227
|
-
grouped_orphans = all_orphans.group_by { |o| @configs.config_for_db_object(o) }
|
228
|
-
grouped_orphans.each do |config, orphans|
|
229
|
-
@db_driver.destroy(config, orphans)
|
266
|
+
def get_unsaved_objects(objects)
|
267
|
+
objects.find_all { |object| object.id.nil? }
|
230
268
|
end
|
231
|
-
end
|
232
269
|
|
233
|
-
|
234
|
-
|
270
|
+
def nil_out_object_ids(objects)
|
271
|
+
objects ||= []
|
272
|
+
objects.each { |object| object.id = nil }
|
273
|
+
end
|
235
274
|
end
|
236
275
|
|
237
|
-
|
238
|
-
|
239
|
-
|
276
|
+
class InvalidPrimaryKeyValue < StandardError
|
277
|
+
end
|
278
|
+
class InvalidAggregateRoot < StandardError
|
240
279
|
end
|
241
|
-
end
|
242
|
-
|
243
280
|
end
|