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 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