vorpal 1.0.0 → 1.2.0

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
  SHA256:
3
- metadata.gz: 2fdca5b34f5a1ac2b9b228849c53b8322e542ab9bc75a5c15a0c8003f1f1fd94
4
- data.tar.gz: aebdb05efdfd853644afa49bf101d845082a7d627e47c50bf609830be46901f0
3
+ metadata.gz: cf920ae279d48b7b9db84a79c77eeb521602a885c8fe88e8f2121e47a5a46e41
4
+ data.tar.gz: 9e97596434cf65e598279005a0bb325cadde5d7a6d3f2180ea5a828432f89760
5
5
  SHA512:
6
- metadata.gz: 1416691727efd279769865a4725949ff41965d120d6431a3737880936838e6760d857c3a5546b5b73774a1ea24cb1ef742d6f7cce6dfa0497de34739ba67b22a
7
- data.tar.gz: 117da60ac019fa9b373d7f45ad6893b24de5611533d348034ef4f4d978adc81468d3228d55adf8107482084b219c47509cb8fef69516ae0ccdea31f73e3a0390
6
+ metadata.gz: d7c07c613b24c7d342be0361e33bda6db6cff0270222db1b7ca8018cf141fb623b2583e49a0d39db51d53389225e527d6a5fd74f133edc6f7653309e86644be4
7
+ data.tar.gz: 492d62d23506de0e26ffdc8e525169dcb78d6c1060dc9309c9828e9865f2341e8f01cc84fe95a059d01d89462c62292a67ebcfc4148ce4ace770884a56e5e04c
data/README.md CHANGED
@@ -1,16 +1,13 @@
1
- # Vorpal [![Build Status](https://travis-ci.org/nulogy/vorpal.svg?branch=master)](https://travis-ci.org/nulogy/vorpal) [![Code Climate](https://codeclimate.com/github/nulogy/vorpal/badges/gpa.svg)](https://codeclimate.com/github/nulogy/vorpal)
1
+ # Vorpal [![Build Status](https://travis-ci.com/nulogy/vorpal.svg?branch=main)](https://travis-ci.com/nulogy/vorpal) [![Code Climate](https://codeclimate.com/github/nulogy/vorpal/badges/gpa.svg)](https://codeclimate.com/github/nulogy/vorpal) [![Code Coverage](https://codecov.io/gh/nulogy/vorpal/branch/main/graph/badge.svg)](https://codecov.io/gh/nulogy/vorpal/branch/main)
2
2
 
3
3
  Separate your domain model from your persistence mechanism. Some problems call for a really sharp tool.
4
4
 
5
-
6
- > One, two! One, two! and through and through
7
-
8
- > The vorpal blade went snicker-snack!
9
-
10
- > He left it dead, and with its head
11
-
5
+ > One, two! One, two! and through and through<br/>
6
+ > The vorpal blade went snicker-snack!<br/>
7
+ > He left it dead, and with its head<br/>
12
8
  > He went galumphing back.
13
9
 
10
+ \- [Jabberwocky](https://www.poetryfoundation.org/poems/42916/jabberwocky) by Lewis Carroll
14
11
 
15
12
  ## Overview
16
13
  Vorpal is a [Data Mapper](http://martinfowler.com/eaaCatalog/dataMapper.html)-style ORM (object relational mapper) framelet that persists POROs (plain old Ruby objects) to a relational DB. It has been heavily influenced by concepts from [Domain Driven Design](http://www.infoq.com/minibooks/domain-driven-design-quickly).
@@ -48,27 +45,21 @@ Or install it yourself as:
48
45
  Start with a domain model of POROs and AR::Base objects that form an aggregate:
49
46
 
50
47
  ```ruby
51
- class Tree; end
52
-
53
48
  class Branch
54
- include Virtus.model
55
-
56
- attribute :id, Integer
57
- attribute :length, Decimal
58
- attribute :diameter, Decimal
59
- attribute :tree, Tree
49
+ attr_accessor :id
50
+ attr_accessor :length
51
+ attr_accessor :diameter
52
+ attr_accessor :tree
60
53
  end
61
54
 
62
- class Gardener < ActiveRecord::Base
55
+ class Gardener
63
56
  end
64
57
 
65
58
  class Tree
66
- include Virtus.model
67
-
68
- attribute :id, Integer
69
- attribute :name, String
70
- attribute :gardener, Gardener
71
- attribute :branches, Array[Branch]
59
+ attr_accessor :id
60
+ attr_accessor :name
61
+ attr_accessor :gardener
62
+ attr_accessor :branches
72
63
  end
73
64
  ```
74
65
 
@@ -143,7 +134,8 @@ module TreeRepository
143
134
  end
144
135
  ```
145
136
 
146
- Here we've used the `owned` flag on the `belongs_to` from the Tree to the Gardener to show that the Gardener is on the aggregate boundary.
137
+ Here we've used the `owned: false` flag on the `belongs_to` from the Tree to the Gardener to show
138
+ that the Gardener is on the aggregate boundary.
147
139
 
148
140
  And use it:
149
141
 
@@ -164,9 +156,35 @@ TreeRepository.destroy(dead_tree)
164
156
  TreeRepository.destroy_by_id(dead_tree_id)
165
157
  ```
166
158
 
159
+ ### Ids
160
+
161
+ Vorpal by default will use auto-incrementing Integers from a DB sequence for ids. However, UUID v4 ids are also
162
+ supported:
163
+
164
+ ```ruby
165
+ Vorpal.define do
166
+ # UUID v4 id!
167
+ map Tree, primary_key_type: :uuid do
168
+ # ..
169
+ end
170
+
171
+ # Also a UUID v4 id, the Rails Way!
172
+ map Trunk, id: :uuid do
173
+ # ..
174
+ end
175
+
176
+ # If you feel the need to specify an auto-incrementing integer id.
177
+ map Branch, primary_key_type: :serial do
178
+ # ..
179
+ end
180
+ end
181
+ ```
182
+
183
+ CAVEAT: Vorpal currently does NOT SUPPORT anyone but Vorpal setting the id of an entity!
184
+
167
185
  ## API Documentation
168
186
 
169
- http://rubydoc.info/github/nulogy/vorpal/master/frames
187
+ http://rubydoc.info/github/nulogy/vorpal/main/frames
170
188
 
171
189
  ## Caveats
172
190
  It also does not do some things that you might expect from other ORMs:
@@ -174,7 +192,7 @@ It also does not do some things that you might expect from other ORMs:
174
192
  1. No lazy loading of associations. This might sound like a big deal, but with [correctly designed aggregates](http://dddcommunity.org/library/vernon_2011/) it turns out not to be.
175
193
  1. No managing of transactions. It is the strong opinion of the authors that managing transactions is an application-level concern.
176
194
  1. No support for validations. Validations are not a persistence concern.
177
- 1. No AR-style callbacks. Use Infrastructure, Application, or Domain [services](http://martinfowler.com/bliki/EvansClassification.html) instead.
195
+ 1. No AR-style callbacks. Use [Infrastructure, Application, or Domain services](http://martinfowler.com/bliki/EvansClassification.html) instead.
178
196
  1. No has-many-through associations. Use two has-many associations to a join entity instead.
179
197
  1. The `id` attribute is reserved for database primary keys. If you have a natural key/id on your domain model, name it something that makes sense for your domain. It is the strong opinion of the authors that using natural keys as foreign keys is a bad idea. This mixes domain and persistence concerns.
180
198
 
@@ -183,13 +201,10 @@ It also does not do some things that you might expect from other ORMs:
183
201
  1. Only supports PostgreSQL.
184
202
 
185
203
  ## Future Enhancements
186
- * Aggregate updated_at.
187
- * Support for other DBMSs (no MySQL support until ids can be generated without inserting into a table!)
188
- * Support for other ORMs.
189
- * Value objects.
190
- * Remove dependency on ActiveRecord (optimistic locking? updated_at, created_at support? Data type conversions? TimeZone support?)
191
- * More efficient updates (use fewer queries.)
204
+ * Support for clients to set UUID-based ids.
192
205
  * Nicer DSL for specifying attributes that have different names in the domain model than in the DB.
206
+ * Aggregate updated_at.
207
+ * Better support for value objects.
193
208
 
194
209
  ## FAQ
195
210
 
@@ -205,11 +220,12 @@ It also does not do some things that you might expect from other ORMs:
205
220
 
206
221
  **A.** Create a method on a [Repository](http://martinfowler.com/eaaCatalog/repository.html)! They have full access to the DB/ORM so you can use [Arel](https://github.com/rails/arel) and go [crazy](http://asciicasts.com/episodes/239-activerecord-relation-walkthrough) or use direct SQL if you want.
207
222
 
208
- For example:
223
+ For example, use the [#query](https://rubydoc.info/github/nulogy/vorpal/main/Vorpal/AggregateMapper#query-instance_method) method on the [AggregateMapper](https://rubydoc.info/github/nulogy/vorpal/main/Vorpal/AggregateMapper) to access the underyling [ActiveRecordRelation](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html):
209
224
 
210
225
  ```ruby
211
- def find_all
212
- @mapper.query.load_all # use the mapper to load all the aggregates
226
+ def find_special_ones
227
+ # use `load_all` or `load_one` to convert from ActiveRecord objects to domain POROs.
228
+ @mapper.query.where(special: true).load_all
213
229
  end
214
230
  ```
215
231
 
@@ -231,19 +247,9 @@ For example:
231
247
 
232
248
  **A.** You can use [ActiveModel::Serialization](http://api.rubyonrails.org/classes/ActiveModel/Serialization.html) or [ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers) but they are not heartily recommended. The former is too coupled to the model and the latter is too coupled to Rails controllers. Vorpal uses [SimpleSerializer](https://github.com/nulogy/simple_serializer) for this purpose.
233
249
 
234
- ## Running Tests
250
+ **Q.** Are `updated_at` and `created_at` supported?
235
251
 
236
- 1. Start a PostgreSQL server.
237
- 2. Either:
238
- * Create a DB user called `vorpal` with password `pass`. OR:
239
- * Modify `spec/helpers/db_helpers.rb`.
240
- 3. Run `rake` from the terminal.
241
-
242
- ## Contributors
243
-
244
- * [Sean Kirby](https://github.com/sskirby)
245
- * [Paul Sobocinski](https://github.com/psobocinski)
246
- * [Jason Cheong-Kee-You](https://github.com/jchunky)
252
+ **A.** Yes. If they exist on your database tables, they will behave exactly as if you were using vanilla ActiveRecord.
247
253
 
248
254
  ## Contributing
249
255
 
@@ -252,3 +258,35 @@ For example:
252
258
  3. Commit your changes (`git commit -am 'Add some feature'`)
253
259
  4. Push to the branch (`git push origin my-new-feature`)
254
260
  5. Create a new Pull Request
261
+
262
+ ## OSX Environment setup
263
+
264
+ 1. Install [Homebrew](https://brew.sh/)
265
+ 2. Install [rbenv](https://github.com/rbenv/rbenv#installation) ([RVM](https://rvm.io/) can work too)
266
+ 3. Install [DirEnv](https://direnv.net/docs/installation.html) (`brew install direnv`)
267
+ 4. Install Docker Desktop Community Edition (`brew cask install docker`)
268
+ 5. Start Docker Desktop Community Edition (`CMD+space docker ENTER`)
269
+ 6. Install Ruby (`rbenv install 2.7.0`)
270
+ 7. Install PostgreSQL (`brew install postgresql`)
271
+ 8. Clone the repo (`git clone git@github.com:nulogy/vorpal.git`) and `cd` to the project root.
272
+ 8. Copy the contents of `gemfiles/rails_<version>.gemfile.lock` into a `Gemfile.lock` file
273
+ at the root of the project. (`cp gemfiles/rails_6_0.gemfile.lock gemfile.lock`)
274
+ 9. `bundle`
275
+
276
+ ### Running Tests
277
+
278
+ 1. Start a PostgreSQL server using `docker-compose up`
279
+ 3. Run `rake` from the terminal to run all specs or `rspec <path to spec file>` to
280
+ run a single spec.
281
+
282
+ ### Running Tests for a specific version of Rails
283
+
284
+ 1. Start a PostgreSQL server using `docker-compose up`
285
+ 2. Run `appraisal rails-5-2 rake` from the terminal to run all specs or
286
+ `appraisal rails-5-2 rspec <path to spec file>` to run a single spec.
287
+
288
+ Please see the [Appraisal gem docs](https://github.com/thoughtbot/appraisal) for more information.
289
+
290
+ ## Contributors
291
+
292
+ See who's [contributed](https://github.com/nulogy/vorpal/graphs/contributors)!
@@ -84,6 +84,17 @@ module Vorpal
84
84
  @engine
85
85
  end
86
86
 
87
+ # Returns a 'Vorpal-aware' [ActiveRecord::Relation](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html)
88
+ # for the ActiveRecord object underlying the domain entity mapped by this mapper.
89
+ #
90
+ # This method allows you to easily access the power of ActiveRecord::Relation to do more complex
91
+ # queries in your repositories.
92
+ #
93
+ # The ActiveRecord::Relation is 'Vorpal-aware' because it has the {#load_one} and {#load_many} methods
94
+ # mixed in so that you can get the POROs from your domain model instead of the ActiveRecord
95
+ # objects normally returned by ActiveRecord::Relation.
96
+ #
97
+ # @return [ActiveRecord::Relation]
87
98
  def query
88
99
  @engine.query(@domain_class)
89
100
  end
@@ -0,0 +1,71 @@
1
+ require 'equalizer'
2
+
3
+ module Vorpal
4
+ module Config
5
+ # @private
6
+ class ClassConfig
7
+ include Equalizer.new(:domain_class, :db_class)
8
+ attr_reader :serializer, :deserializer, :domain_class, :db_class, :primary_key_type, :local_association_configs
9
+ attr_accessor :has_manys, :belongs_tos, :has_ones
10
+
11
+ ALLOWED_PRIMARY_KEY_TYPE_OPTIONS = [:serial, :uuid]
12
+
13
+ def initialize(attrs)
14
+ @has_manys = []
15
+ @belongs_tos = []
16
+ @has_ones = []
17
+ @local_association_configs = []
18
+
19
+ @serializer = attrs[:serializer]
20
+ @deserializer = attrs[:deserializer]
21
+ @domain_class = attrs[:domain_class]
22
+ @db_class = attrs[:db_class]
23
+ @primary_key_type = attrs[:primary_key_type]
24
+ raise "Invalid primary_key_type: '#{@primary_key_type}'" unless ALLOWED_PRIMARY_KEY_TYPE_OPTIONS.include?(@primary_key_type)
25
+ end
26
+
27
+ def build_db_object(attributes)
28
+ db_class.new(attributes)
29
+ end
30
+
31
+ def set_db_object_attributes(db_object, attributes)
32
+ db_object.attributes = attributes
33
+ end
34
+
35
+ def get_db_object_attributes(db_object)
36
+ symbolize_keys(db_object.attributes)
37
+ end
38
+
39
+ def serialization_required?
40
+ domain_class.superclass.name != 'ActiveRecord::Base'
41
+ end
42
+
43
+ def serialize(object)
44
+ serializer.serialize(object)
45
+ end
46
+
47
+ def deserialize(db_object)
48
+ attributes = get_db_object_attributes(db_object)
49
+ serialization_required? ? deserializer.deserialize(domain_class.new, attributes) : db_object
50
+ end
51
+
52
+ def set_attribute(db_object, attribute, value)
53
+ db_object.send("#{attribute}=", value)
54
+ end
55
+
56
+ def get_attribute(db_object, attribute)
57
+ db_object.send(attribute)
58
+ end
59
+
60
+ private
61
+
62
+ def symbolize_keys(hash)
63
+ result = {}
64
+ hash.each_key do |key|
65
+ result[key.to_sym] = hash[key]
66
+ end
67
+ result
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,13 +1,13 @@
1
1
  require 'vorpal/util/hash_initialization'
2
2
  require 'vorpal/exceptions'
3
+ require 'vorpal/config/class_config'
3
4
  require 'equalizer'
4
5
 
5
6
  module Vorpal
6
7
  # @private
7
- class MasterConfig
8
- def initialize(class_configs)
9
- @class_configs = class_configs
10
- initialize_association_configs
8
+ class MainConfig
9
+ def initialize
10
+ @class_configs = []
11
11
  end
12
12
 
13
13
  def config_for(clazz)
@@ -20,7 +20,9 @@ module Vorpal
20
20
  @class_configs.detect { |conf| conf.db_class == db_object.class }
21
21
  end
22
22
 
23
- private
23
+ def add_class_config(class_config)
24
+ @class_configs << class_config
25
+ end
24
26
 
25
27
  def initialize_association_configs
26
28
  association_configs = {}
@@ -50,6 +52,8 @@ module Vorpal
50
52
  end
51
53
  end
52
54
 
55
+ private
56
+
53
57
  def build_association_config(association_configs, local_config, fk, fk_type)
54
58
  association_config = AssociationConfig.new(local_config, fk, fk_type)
55
59
  if association_configs[association_config]
@@ -126,67 +130,6 @@ module Vorpal
126
130
  end
127
131
  end
128
132
 
129
- # @private
130
- class ClassConfig
131
- include Equalizer.new(:domain_class, :db_class)
132
- attr_reader :serializer, :deserializer, :domain_class, :db_class, :local_association_configs
133
- attr_accessor :has_manys, :belongs_tos, :has_ones
134
-
135
- def initialize(attrs)
136
- @has_manys = []
137
- @belongs_tos = []
138
- @has_ones = []
139
- @local_association_configs = []
140
-
141
- attrs.each do |k,v|
142
- instance_variable_set("@#{k}", v)
143
- end
144
- end
145
-
146
- def build_db_object(attributes)
147
- db_class.new(attributes)
148
- end
149
-
150
- def set_db_object_attributes(db_object, attributes)
151
- db_object.attributes = attributes
152
- end
153
-
154
- def get_db_object_attributes(db_object)
155
- symbolize_keys(db_object.attributes)
156
- end
157
-
158
- def serialization_required?
159
- domain_class.superclass.name != 'ActiveRecord::Base'
160
- end
161
-
162
- def serialize(object)
163
- serializer.serialize(object)
164
- end
165
-
166
- def deserialize(db_object)
167
- attributes = get_db_object_attributes(db_object)
168
- serialization_required? ? deserializer.deserialize(domain_class.new, attributes) : db_object
169
- end
170
-
171
- def set_attribute(db_object, attribute, value)
172
- db_object.send("#{attribute}=", value)
173
- end
174
-
175
- def get_attribute(db_object, attribute)
176
- db_object.send(attribute)
177
- end
178
-
179
- private
180
-
181
- def symbolize_keys(hash)
182
- result = {}
183
- hash.each_key do |key|
184
- result[key.to_sym] = hash[key]
185
- end
186
- result
187
- end
188
- end
189
-
190
133
  # @private
191
134
  class ForeignKeyInfo
192
135
  include Equalizer.new(:fk_column, :fk_type_column, :fk_type)
@@ -9,7 +9,12 @@ module Vorpal
9
9
  end
10
10
 
11
11
  def insert(db_class, db_objects)
12
- if defined? ActiveRecord::Import
12
+ if ActiveRecord::VERSION::MAJOR >= 6
13
+ return if db_objects.empty?
14
+
15
+ update_timestamps_on_create(db_class, db_objects)
16
+ db_class.insert_all!(db_objects.map(&:attributes))
17
+ elsif defined? ActiveRecord::Import
13
18
  db_class.import(db_objects, validate: false)
14
19
  else
15
20
  db_objects.each do |db_object|
@@ -19,13 +24,20 @@ module Vorpal
19
24
  end
20
25
 
21
26
  def update(db_class, db_objects)
22
- db_objects.each do |db_object|
23
- db_object.save!(validate: false)
27
+ if ActiveRecord::VERSION::MAJOR >= 6
28
+ return if db_objects.empty?
29
+
30
+ update_timestamps_on_update(db_class, db_objects)
31
+ db_class.upsert_all(db_objects.map(&:attributes))
32
+ else
33
+ db_objects.each do |db_object|
34
+ db_object.save!(validate: false)
35
+ end
24
36
  end
25
37
  end
26
38
 
27
39
  def destroy(db_class, ids)
28
- db_class.delete_all(id: ids)
40
+ db_class.where(id: ids).delete_all
29
41
  end
30
42
 
31
43
  # Loads instances of the given class by primary key.
@@ -100,6 +112,30 @@ module Vorpal
100
112
 
101
113
  private
102
114
 
115
+ # Adapted from https://github.com/rails/rails/blob/614580270d7789e5275defc3da020ce27b3b2302/activerecord/lib/active_record/timestamp.rb#L99
116
+ def update_timestamps_on_create(db_class, db_objects)
117
+ return unless db_class.record_timestamps
118
+
119
+ current_time = db_class.current_time_from_proper_timezone
120
+ db_objects.each do |db_object|
121
+ db_class.all_timestamp_attributes_in_model.each do |column|
122
+ db_object.write_attribute(column, current_time) unless db_object.read_attribute(column)
123
+ end
124
+ end
125
+ end
126
+
127
+ #Adapted from https://github.com/rails/rails/blob/614580270d7789e5275defc3da020ce27b3b2302/activerecord/lib/active_record/timestamp.rb#L111
128
+ def update_timestamps_on_update(db_class, db_objects)
129
+ return unless db_class.record_timestamps
130
+
131
+ current_time = db_class.current_time_from_proper_timezone
132
+ db_objects.each do |db_object|
133
+ db_class.timestamp_attributes_for_update_in_model.each do |column|
134
+ db_object.write_attribute(column, current_time)
135
+ end
136
+ end
137
+ end
138
+
103
139
  def sequence_name(db_class)
104
140
  @sequence_names[db_class] ||= execute(
105
141
  "SELECT substring(column_default from '''(.*)''') FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = $1 AND column_name = 'id' LIMIT 1",