vorpal 0.0.7.rc1 → 0.0.7.rc2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ce7c3ca0efe5dbcd1f9aa75bcbf4465a0ffa9c8b
4
- data.tar.gz: 01a2dad313199961079b71ac522f441d9e68e79b
3
+ metadata.gz: 8b6b4c26a538b1aeac920b566ce3b80e0b04e4fe
4
+ data.tar.gz: 88d0f3a57930e3a8587d89d085293ac9d032fc58
5
5
  SHA512:
6
- metadata.gz: c54780e9bfdaab538b32c2f7a545c1f2182cca2a7ae4f23dce23fb5aeee97947e430961d56d8007c902b7833f38c5057590f4eb78e4613f100d9978602c89f7c
7
- data.tar.gz: 16db095201901d03d70a7c75120195a529e9b1548ce1bccf059d86f2544a9c495b5ed52d76f9d27f2e3755d8f12c45498bae0107a1c67a030609f812d8c2cef8
6
+ metadata.gz: f0190b9231a1ecce12aec8c62a92345e62ae98470f488401f8e0947ddf37bba7573263a9b78b79635958d85614b9249ab952526542cfe48a081c1d6de179ff3d
7
+ data.tar.gz: 32e2963bc4cb79a56de8499fff09b439475f0371d6b4d6f2cfaa8bb87d5b431070965adfcf708e45546db846063418abcabd5e9e64ed8af4054e9c45fbe37be7
data/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ Please see https://github.com/nulogy/vorpal/releases
data/README.md CHANGED
@@ -109,7 +109,7 @@ require 'vorpal'
109
109
  module TreeRepository
110
110
  extend self
111
111
 
112
- @repository = Vorpal.define do
112
+ engine = Vorpal.define do
113
113
  map Tree do
114
114
  attributes :name
115
115
  belongs_to :gardener, owned: false
@@ -123,9 +123,10 @@ module TreeRepository
123
123
  belongs_to :tree
124
124
  end
125
125
  end
126
+ @repository = engine.repository_for(Tree)
126
127
 
127
- def find(id)
128
- @repository.load(id, Tree)
128
+ def find(tree_id)
129
+ @repository.load_one(@repository.db_class.where(id: tree_id).first)
129
130
  end
130
131
 
131
132
  def save(tree)
@@ -137,7 +138,7 @@ module TreeRepository
137
138
  end
138
139
 
139
140
  def destroy_by_id(tree_id)
140
- @repository.destroy_by_id(tree_id, Tree)
141
+ @repository.destroy_by_id(tree_id)
141
142
  end
142
143
  end
143
144
  ```
@@ -1,286 +1,92 @@
1
1
  require 'vorpal/identity_map'
2
- require 'vorpal/aggregate_utils'
3
- require 'vorpal/db_loader'
4
- require 'vorpal/db_driver'
5
2
 
6
3
  module Vorpal
7
4
  class AggregateRepository
8
5
  # @private
9
- def initialize(db_driver, master_config)
10
- @db_driver = db_driver
11
- @configs = master_config
6
+ def initialize(domain_class, engine)
7
+ @domain_class = domain_class
8
+ @engine = engine
12
9
  end
13
10
 
14
- # Saves an aggregate to the DB. Inserts objects that are new to the
11
+ # Saves a collection of aggregates to the DB. Inserts objects that are new to an
15
12
  # aggregate, updates existing objects and deletes objects that are no longer
16
13
  # present.
17
14
  #
18
- # Objects that are on the boundary of the aggregate (owned: false) will not
15
+ # Objects that are on the boundary of an aggregate (owned: false) will not
19
16
  # be inserted, updated, or deleted. However, the relationships to these
20
- # objects (provided they are stored within the aggregate) will be saved.
17
+ # objects (provided they are stored within an aggregate) will be saved.
21
18
  #
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
-
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.
19
+ # @param roots [[Object]] array of aggregate roots to be saved. Will also accept a
20
+ # single aggregate.
32
21
  # @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
22
+ # @raise [InvalidAggregateRoot] When any of the roots are nil.
23
+ def persist(roots)
24
+ @engine.persist(roots)
54
25
  end
55
26
 
56
27
  # Loads an aggregate from the DB. Will eagerly load all objects in the
57
28
  # aggregate and on the boundary (owned: false).
58
29
  #
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
30
+ # @param db_root [Object] DB representation of the root of the aggregate to be
31
+ # loaded. This can be nil.
32
+ # @param identity_map [IdentityMap] Provide your own IdentityMap instance
33
+ # if you want entity id -> unique object mapping for a greater scope than one
65
34
  # 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
35
+ # @return [Object] Aggregate root corresponding to the given DB representation.
36
+ def load_one(db_root, identity_map=IdentityMap.new)
37
+ @engine.load_one(db_root, @domain_class, identity_map)
69
38
  end
70
39
 
71
- # Like {#load} but operates on multiple ids.
40
+ # Like {#load_one} but operates on multiple aggregate roots.
72
41
  #
73
- # @param ids [[Integer]] Array of primary key values of the roots of the
42
+ # @param db_roots [[Integer]] Array of primary key values of the roots of the
74
43
  # 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
44
+ # @param identity_map [IdentityMap] Provide your own IdentityMap instance
45
+ # if you want entity id -> unique object mapping for a greater scope than one
78
46
  # 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?)
82
-
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)
86
-
87
- sorted_roots(ids, objects, domain_class)
88
- end
89
-
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
47
+ # @return [[Object]] Aggregate roots corresponding to the given DB representations.
48
+ # @raise [InvalidAggregateRoot] When any of the db_roots are nil.
49
+ def load_many(db_roots, identity_map=IdentityMap.new)
50
+ @engine.load_many(db_roots, @domain_class, identity_map)
97
51
  end
98
52
 
99
- # Like {#destroy} but operates on multiple aggregates. Roots must
100
- # be of the same type.
53
+ # Removes a collection of aggregates from the DB. Even if an aggregate
54
+ # contains unsaved changes this method will correctly remove everything.
101
55
  #
102
- # @param roots [[Object]] Array of roots of the aggregates to be destroyed.
56
+ # @param roots [[Object]] Roots of the aggregates to be destroyed. Also accepts a
57
+ # single root.
103
58
  # @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?)
107
-
108
- destroy_all_by_id(roots.map(&:id), roots.first.class)
109
- roots
59
+ # @raise [InvalidAggregateRoot] When any of the roots are nil.
60
+ def destroy(roots)
61
+ @engine.destroy(roots)
110
62
  end
111
63
 
112
- # Removes an aggregate from the DB given its primary key.
64
+ # Removes a collection of aggregates from the DB given their primary keys.
113
65
  #
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
120
-
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))
133
- end
134
- ids
66
+ # @param ids [[Integer]] Ids of roots of the aggregates to be destroyed. Also
67
+ # accepts a single id.
68
+ # @raise [InvalidPrimaryKeyValue] When any of the ids are nil.
69
+ def destroy_by_id(ids)
70
+ @engine.destroy_by_id(ids, @domain_class)
135
71
  end
136
72
 
137
73
  # Returns the DB Class (e.g. ActiveRecord::Base class) that is responsible
138
74
  # for accessing the associated data in the DB.
139
- def db_class(domain_class)
140
- @configs.config_for(domain_class).db_class
141
- end
142
-
143
- private
144
-
145
- def all_owned_objects(roots)
146
- AggregateUtils.group_by_type(roots, @configs)
147
- end
148
-
149
- def load_from_db(ids, domain_class, only_owned=false)
150
- DbLoader.new(only_owned, @db_driver).load_from_db(ids, @configs.config_for(domain_class))
151
- end
152
-
153
- def load_owned_from_db(ids, domain_class)
154
- load_from_db(ids, domain_class, true)
155
- end
156
-
157
- def deserialize(loaded_db_objects, identity_map)
158
- loaded_db_objects.flat_map do |config, db_objects|
159
- db_objects.map do |db_object|
160
- # TODO: There is a bug here when you have something in the IdentityMap that is stale and needs to be updated.
161
- identity_map.get_and_set(db_object) { config.deserialize(db_object) }
162
- end
163
- end
164
- end
165
-
166
- def set_associations(loaded_db_objects, identity_map)
167
- loaded_db_objects.each do |config, db_objects|
168
- db_objects.each do |db_object|
169
- config.local_association_configs.each do |association_config|
170
- db_remote = loaded_db_objects.find_by_id(
171
- association_config.remote_class_config(db_object),
172
- association_config.fk_value(db_object)
173
- )
174
- association_config.associate(identity_map.get(db_object), identity_map.get(db_remote))
175
- end
176
- end
177
- end
178
- end
179
-
180
- def sorted_roots(ids, objects, domain_class)
181
- roots = objects.select { |obj| obj.class == domain_class }
182
- roots_by_id = roots.reduce({}) { |h, root| h[root.id] = root; h }
183
- ids.map { |id| roots_by_id[id] }.compact
184
- end
185
-
186
- def serialize(owned_objects, mapping, loaded_db_objects)
187
- owned_objects.each do |config, objects|
188
- objects.each do |object|
189
- db_object = serialize_object(object, config, loaded_db_objects)
190
- mapping[object] = db_object
191
- end
192
- end
193
- end
194
-
195
- def serialize_object(object, config, loaded_db_objects)
196
- if config.serialization_required?
197
- attributes = config.serialize(object)
198
- if object.id.nil?
199
- config.build_db_object(attributes)
200
- else
201
- db_object = loaded_db_objects.find_by_id(config, object.id)
202
- config.set_db_object_attributes(db_object, attributes)
203
- db_object
204
- end
205
- else
206
- object
207
- end
208
- end
209
-
210
- def set_primary_keys(owned_objects, mapping)
211
- owned_objects.each do |config, objects|
212
- in_need_of_primary_keys = objects.find_all { |obj| obj.id.nil? }
213
- primary_keys = @db_driver.get_primary_keys(config, in_need_of_primary_keys.length)
214
- in_need_of_primary_keys.zip(primary_keys).each do |object, primary_key|
215
- mapping[object].id = primary_key
216
- object.id = primary_key
217
- end
218
- end
219
- mapping.rehash # needs to happen because setting the id on an AR::Base model changes its hash value
220
- end
221
-
222
- def set_foreign_keys(owned_objects, mapping)
223
- owned_objects.each do |config, objects|
224
- objects.each do |object|
225
- config.has_manys.each do |has_many_config|
226
- if has_many_config.owned
227
- children = has_many_config.get_children(object)
228
- children.each do |child|
229
- has_many_config.set_foreign_key(mapping[child], object)
230
- end
231
- end
232
- end
233
-
234
- config.has_ones.each do |has_one_config|
235
- if has_one_config.owned
236
- child = has_one_config.get_child(object)
237
- has_one_config.set_foreign_key(mapping[child], object)
238
- end
239
- end
240
-
241
- config.belongs_tos.each do |belongs_to_config|
242
- child = belongs_to_config.get_child(object)
243
- belongs_to_config.set_foreign_key(mapping[object], child)
244
- end
245
- end
246
- end
247
- end
248
-
249
- def save(owned_objects, new_objects, mapping)
250
- grouped_new_objects = new_objects.group_by { |obj| @configs.config_for(obj.class) }
251
- owned_objects.each do |config, objects|
252
- objects_to_insert = grouped_new_objects[config] || []
253
- db_objects_to_insert = objects_to_insert.map { |obj| mapping[obj] }
254
- @db_driver.insert(config, db_objects_to_insert)
255
-
256
- objects_to_update = objects - objects_to_insert
257
- db_objects_to_update = objects_to_update.map { |obj| mapping[obj] }
258
- @db_driver.update(config, db_objects_to_update)
259
- end
75
+ def db_class
76
+ @engine.db_class(@domain_class)
260
77
  end
261
78
 
262
- def remove_orphans(mapping, loaded_db_objects)
263
- db_objects_in_aggregate = mapping.values
264
- db_objects_in_db = loaded_db_objects.all_objects
265
- all_orphans = db_objects_in_db - db_objects_in_aggregate
266
- grouped_orphans = all_orphans.group_by { |o| @configs.config_for_db_object(o) }
267
- grouped_orphans.each do |config, orphans|
268
- @db_driver.destroy(config, orphans)
269
- end
270
- end
271
-
272
- def get_unsaved_objects(objects)
273
- objects.find_all { |object| object.id.nil? }
79
+ # Access to the underlying mapping {Engine}. Provided in case access to another aggregate
80
+ # or another db_class is required.
81
+ #
82
+ # @return [Engine] Mapping interface not specific to a particular aggregate root.
83
+ def engine
84
+ @engine
274
85
  end
275
86
 
276
- def nil_out_object_ids(objects)
277
- objects ||= []
278
- objects.each { |object| object.id = nil }
87
+ def query
88
+ # db_class.unscoped.extending(ArelQueryMethods.new(self))
89
+ @engine.query(@domain_class)
279
90
  end
280
91
  end
281
-
282
- class InvalidPrimaryKeyValue < StandardError
283
- end
284
- class InvalidAggregateRoot < StandardError
285
- end
286
92
  end
@@ -1,20 +1,20 @@
1
- require 'vorpal/aggregate_repository'
1
+ require 'vorpal/engine'
2
2
  require 'vorpal/config_builder'
3
3
 
4
4
  module Vorpal
5
5
  module Configuration
6
6
 
7
- # Configures and creates a {Vorpal::AggregateRepository} instance.
7
+ # Configures and creates a {Engine} instance.
8
8
  #
9
9
  # @param options [Hash] Global configuration options for the repository instance.
10
10
  # @option options [Object] :db_driver (Object that will be used to interact with the DB.)
11
- # Must be duck-type compatible with {Vorpal::DbDriver}.
11
+ # Must be duck-type compatible with {DbDriver}.
12
12
  #
13
- # @return [Vorpal::AggregateRepository] Repository instance.
13
+ # @return [Engine] Instance of the mapping engine.
14
14
  def define(options={}, &block)
15
15
  master_config = build_config(&block)
16
16
  db_driver = options.fetch(:db_driver, DbDriver.new)
17
- AggregateRepository.new(db_driver, master_config)
17
+ Engine.new(db_driver, master_config)
18
18
  end
19
19
 
20
20
  # Maps a domain class to a relational table.
@@ -1,7 +1,7 @@
1
1
  module Vorpal
2
- # Interfaces between the database and Vorpal
2
+ # Interface between the database and Vorpal
3
3
  #
4
- # Currently only works for PostgreSQL.
4
+ # Currently only works for PostgreSQL via ActiveRecord.
5
5
  class DbDriver
6
6
  def initialize
7
7
  @sequence_names = {}
@@ -29,13 +29,16 @@ module Vorpal
29
29
 
30
30
  # Loads instances of the given class by primary key.
31
31
  #
32
+ # @param class_config [ClassConfig]
32
33
  # @return [[Object]] An array of entities.
33
34
  def load_by_id(class_config, ids)
34
- class_config.db_class.where(id: ids)
35
+ class_config.db_class.where(id: ids).all
35
36
  end
36
37
 
37
38
  # Loads instances of the given class whose foreign key has the given value.
38
39
  #
40
+ # @param class_config [ClassConfig]
41
+ # @param foreign_key_info [ForeignKeyInfo]
39
42
  # @return [[Object]] An array of entities.
40
43
  def load_by_foreign_key(class_config, id, foreign_key_info)
41
44
  arel = class_config.db_class.where(foreign_key_info.fk_column => id)
@@ -45,6 +48,7 @@ module Vorpal
45
48
 
46
49
  # Fetches primary key values to be used for new entities.
47
50
  #
51
+ # @param class_config [ClassConfig] Config of the entity whose primary keys are being fetched.
48
52
  # @return [[Integer]] An array of unused primary keys.
49
53
  def get_primary_keys(class_config, count)
50
54
  result = execute("select nextval($1) from generate_series(1,$2);", [sequence_name(class_config), count])
@@ -53,6 +57,7 @@ module Vorpal
53
57
 
54
58
  # Builds an ORM Class for accessing data in the given DB table.
55
59
  #
60
+ # @param table_name [String] Name of the DB table the DB class should interface with.
56
61
  # @return [Class] ActiveRecord::Base Class
57
62
  def build_db_class(table_name)
58
63
  db_class = Class.new(ActiveRecord::Base)
@@ -60,6 +65,13 @@ module Vorpal
60
65
  db_class
61
66
  end
62
67
 
68
+ # Builds a composable query object (e.g. ActiveRecord::Relation) with Vorpal methods mixed in.
69
+ #
70
+ # @param class_config [ClassConfig] Config of the entity whose db representations should be returned.
71
+ def query(class_config)
72
+ class_config.db_class.unscoped.extending(ArelQueryMethods.new(self))
73
+ end
74
+
63
75
  private
64
76
 
65
77
  def sequence_name(class_config)
@@ -74,4 +86,33 @@ module Vorpal
74
86
  ActiveRecord::Base.connection.exec_query(sql, 'SQL', binds)
75
87
  end
76
88
  end
89
+
90
+ class ArelQueryMethods < Module
91
+ def initialize(repository)
92
+ @repository = repository
93
+ end
94
+
95
+ def extended(descendant)
96
+ super
97
+ descendant.extend(Methods)
98
+ descendant.vorpal_aggregate_repository = @repository
99
+ end
100
+
101
+ # Methods in this module will appear on any composable
102
+ module Methods
103
+ attr_writer :vorpal_aggregate_repository
104
+
105
+ # See {AggregateRepository#load_many}.
106
+ def load_many
107
+ db_roots = self.all
108
+ @vorpal_aggregate_repository.load_many(db_roots)
109
+ end
110
+
111
+ # See {AggregateRepository#load_one}.
112
+ def load_one
113
+ db_root = self.first
114
+ @vorpal_aggregate_repository.load_one(db_root)
115
+ end
116
+ end
117
+ end
77
118
  end
@@ -13,9 +13,15 @@ module Vorpal
13
13
  end
14
14
 
15
15
  def load_from_db(ids, config)
16
+ db_roots = @db_driver.load_by_id(config, ids)
17
+ load_from_db_objects(db_roots, config)
18
+ end
19
+
20
+ def load_from_db_objects(db_roots, config)
16
21
  @loaded_objects = LoadedObjects.new
22
+ @loaded_objects.add(config, db_roots)
17
23
  @lookup_instructions = LookupInstructions.new
18
- @lookup_instructions.lookup_by_id(config, ids)
24
+ explore_objects(config, db_roots)
19
25
 
20
26
  until @lookup_instructions.empty?
21
27
  lookup = @lookup_instructions.next_lookup