vorpal 1.0.3 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -26
  3. data/lib/vorpal/aggregate_mapper.rb +13 -2
  4. data/lib/vorpal/aggregate_traversal.rb +9 -8
  5. data/lib/vorpal/config/association_config.rb +84 -0
  6. data/lib/vorpal/config/belongs_to_config.rb +35 -0
  7. data/lib/vorpal/config/class_config.rb +71 -0
  8. data/lib/vorpal/config/configs.rb +54 -0
  9. data/lib/vorpal/config/foreign_key_info.rb +23 -0
  10. data/lib/vorpal/config/has_many_config.rb +38 -0
  11. data/lib/vorpal/config/has_one_config.rb +35 -0
  12. data/lib/vorpal/config/main_config.rb +68 -0
  13. data/lib/vorpal/db_loader.rb +25 -22
  14. data/lib/vorpal/driver/postgresql.rb +42 -6
  15. data/lib/vorpal/dsl/config_builder.rb +26 -73
  16. data/lib/vorpal/dsl/configuration.rb +139 -42
  17. data/lib/vorpal/dsl/defaults_generator.rb +1 -1
  18. data/lib/vorpal/engine.rb +27 -13
  19. data/lib/vorpal/exceptions.rb +4 -0
  20. data/lib/vorpal/loaded_objects.rb +57 -14
  21. data/lib/vorpal/util/array_hash.rb +22 -8
  22. data/lib/vorpal/util/hash_initialization.rb +1 -1
  23. data/lib/vorpal/version.rb +1 -1
  24. data/vorpal.gemspec +4 -4
  25. metadata +17 -74
  26. data/.editorconfig +0 -13
  27. data/.envrc +0 -4
  28. data/.gitignore +0 -16
  29. data/.rspec +0 -1
  30. data/.ruby-version +0 -1
  31. data/.travis.yml +0 -18
  32. data/.yardopts +0 -1
  33. data/Appraisals +0 -18
  34. data/Gemfile +0 -4
  35. data/Rakefile +0 -39
  36. data/bin/appraisal +0 -29
  37. data/bin/rake +0 -29
  38. data/bin/rspec +0 -29
  39. data/docker-compose.yml +0 -19
  40. data/gemfiles/rails_5_1.gemfile +0 -11
  41. data/gemfiles/rails_5_1.gemfile.lock +0 -101
  42. data/gemfiles/rails_5_2.gemfile +0 -11
  43. data/gemfiles/rails_5_2.gemfile.lock +0 -101
  44. data/gemfiles/rails_6_0.gemfile +0 -9
  45. data/gemfiles/rails_6_0.gemfile.lock +0 -101
  46. data/lib/vorpal/configs.rb +0 -296
  47. data/spec/acceptance/vorpal/aggregate_mapper_spec.rb +0 -910
  48. data/spec/helpers/codecov_helper.rb +0 -7
  49. data/spec/helpers/db_helpers.rb +0 -69
  50. data/spec/helpers/profile_helpers.rb +0 -26
  51. data/spec/integration/vorpal/driver/postgresql_spec.rb +0 -42
  52. data/spec/integration_spec_helper.rb +0 -29
  53. data/spec/performance/vorpal/performance_spec.rb +0 -305
  54. data/spec/unit/vorpal/configs_spec.rb +0 -117
  55. data/spec/unit/vorpal/db_loader_spec.rb +0 -103
  56. data/spec/unit/vorpal/dsl/config_builder_spec.rb +0 -18
  57. data/spec/unit/vorpal/dsl/defaults_generator_spec.rb +0 -75
  58. data/spec/unit/vorpal/identity_map_spec.rb +0 -62
  59. data/spec/unit/vorpal/loaded_objects_spec.rb +0 -22
  60. data/spec/unit/vorpal/util/string_utils_spec.rb +0 -25
  61. data/spec/unit_spec_helper.rb +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9833373892cb8a97f3b37ba1ec0fe9d06fe0b39dc6caaf9f7b333b28f20c2a47
4
- data.tar.gz: 47df18148ce8412236a13032055da0ca6d3c1c21c9839e99c329a2df88982a55
3
+ metadata.gz: a6010044c0c80fa3020a1e7a0cd362977b363df6e03bbcc8508028b9bf4e3a2f
4
+ data.tar.gz: '07708e14f8dc2af434718350a18935ead37232b3a9907338cade26cfd9080b41'
5
5
  SHA512:
6
- metadata.gz: 24c5cd71c9d56d8b882bb6691808c22749317d81ca1b104c90ddcf846c35ee0d8b36809cf1558f5a973f3696a48dc2aa9e80a6c0d6f7ea80dac275ef7b1d8597
7
- data.tar.gz: c1012c585122a6259137e9de766247222b7c1a20606b27aca7fb219d947914a860016c1ec1c7ba8387d223d9bbde53c416065f52b837680b0bcff2fc6d177d40
6
+ metadata.gz: dc45b50d694ea28489e2125e8ecae7065a24ba91773771536e01dcb5cbfc4bad93c8e9fabc1be635213b072ca6c9068b0851c37763814ff3cbcebe5037ba8686
7
+ data.tar.gz: 4b8dc8c8ce5a567a7f563df9d3c478d24f61afd66ab110852cb3fab028089ec6f307d74d9ff5157c478f5f60a349930cce6a8f837f282f56b5d1502af30710cd
data/README.md CHANGED
@@ -1,4 +1,4 @@
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) [![Code Coverage](https://codecov.io/gh/nulogy/vorpal/branch/master/graph/badge.svg)](https://codecov.io/gh/nulogy/vorpal/branch/master)
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
 
@@ -45,27 +45,21 @@ Or install it yourself as:
45
45
  Start with a domain model of POROs and AR::Base objects that form an aggregate:
46
46
 
47
47
  ```ruby
48
- class Tree; end
49
-
50
48
  class Branch
51
- include Virtus.model
52
-
53
- attribute :id, Integer
54
- attribute :length, Decimal
55
- attribute :diameter, Decimal
56
- attribute :tree, Tree
49
+ attr_accessor :id
50
+ attr_accessor :length
51
+ attr_accessor :diameter
52
+ attr_accessor :tree
57
53
  end
58
54
 
59
- class Gardener < ActiveRecord::Base
55
+ class Gardener
60
56
  end
61
57
 
62
58
  class Tree
63
- include Virtus.model
64
-
65
- attribute :id, Integer
66
- attribute :name, String
67
- attribute :gardener, Gardener
68
- attribute :branches, Array[Branch]
59
+ attr_accessor :id
60
+ attr_accessor :name
61
+ attr_accessor :gardener
62
+ attr_accessor :branches
69
63
  end
70
64
  ```
71
65
 
@@ -162,9 +156,33 @@ TreeRepository.destroy(dead_tree)
162
156
  TreeRepository.destroy_by_id(dead_tree_id)
163
157
  ```
164
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
+
165
183
  ## API Documentation
166
184
 
167
- http://rubydoc.info/github/nulogy/vorpal/master/frames
185
+ http://rubydoc.info/github/nulogy/vorpal
168
186
 
169
187
  ## Caveats
170
188
  It also does not do some things that you might expect from other ORMs:
@@ -181,13 +199,11 @@ It also does not do some things that you might expect from other ORMs:
181
199
  1. Only supports PostgreSQL.
182
200
 
183
201
  ## Future Enhancements
184
- * Aggregate updated_at.
185
- * Support for other DBMSs (no MySQL support until ids can be generated without inserting into a table!)
186
- * Support for other ORMs.
187
- * Value objects.
188
- * Remove dependency on ActiveRecord (optimistic locking? updated_at, created_at support? Data type conversions? TimeZone support?)
189
- * More efficient updates (use fewer queries.)
202
+ * Support for having foreign keys point to columns other than primary keys.
203
+ * Support for storing entity ids in a column called something other than "id".
190
204
  * Nicer DSL for specifying attributes that have different names in the domain model than in the DB.
205
+ * Aggregate updated_at.
206
+ * Better support for value objects.
191
207
 
192
208
  ## FAQ
193
209
 
@@ -203,11 +219,12 @@ It also does not do some things that you might expect from other ORMs:
203
219
 
204
220
  **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.
205
221
 
206
- For example:
222
+ 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):
207
223
 
208
224
  ```ruby
209
- def find_all
210
- @mapper.query.load_all # use the mapper to load all the aggregates
225
+ def find_special_ones
226
+ # use `load_all` or `load_one` to convert from ActiveRecord objects to domain POROs.
227
+ @mapper.query.where(special: true).load_all
211
228
  end
212
229
  ```
213
230
 
@@ -229,6 +246,28 @@ For example:
229
246
 
230
247
  **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.
231
248
 
249
+ **Q.** Are `updated_at` and `created_at` supported?
250
+
251
+ **A.** Yes. If they exist on your database tables, they will behave exactly as if you were using vanilla ActiveRecord.
252
+
253
+ **Q.** How do I tell ActiveRecord to ignore certain columns so that I can rename/remove them using zero-downtime deploys?
254
+
255
+ **A.** You will want to use ActiveRecord's [`ignored_columns=`](https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-ignored_columns) method like this:
256
+
257
+ ```ruby
258
+ engine = Vorpal.define do
259
+ map Product do
260
+ attributes(
261
+ :name,
262
+ :description,
263
+ # :column_to_remove
264
+ )
265
+ end
266
+ end
267
+ product_ar_class = engine.mapper_for(Product).db_class
268
+ product_ar_class.ignored_columns = [:column_to_remove]
269
+ ```
270
+
232
271
  ## Contributing
233
272
 
234
273
  1. Fork it ( https://github.com/nulogy/vorpal/fork )
@@ -265,6 +304,14 @@ For example:
265
304
 
266
305
  Please see the [Appraisal gem docs](https://github.com/thoughtbot/appraisal) for more information.
267
306
 
307
+ ### Releasing
308
+
309
+ 1. Update the version number in `lib/vorpal/version.rb`
310
+ 2. Update the version of Vorpal in the Appraisal gemfiles (otherwise Travis CI will fail): `appraisal install`
311
+ 3. Commit the above changes with the message: `Bump version to <X.Y.Z>`
312
+ 4. Release the new version to Rubygems: `rake release`
313
+ 5. Profit!
314
+
268
315
  ## Contributors
269
316
 
270
317
  See who's [contributed](https://github.com/nulogy/vorpal/graphs/contributors)!
@@ -27,12 +27,12 @@ module Vorpal
27
27
  # Loads an aggregate from the DB. Will eagerly load all objects in the
28
28
  # aggregate and on the boundary (owned: false).
29
29
  #
30
- # @param db_root [Object] DB representation of the root of the aggregate to be
30
+ # @param db_root [Object, nil] DB representation of the root of the aggregate to be
31
31
  # loaded. This can be nil.
32
32
  # @param identity_map [IdentityMap] Provide your own IdentityMap instance
33
33
  # if you want entity id -> unique object mapping for a greater scope than one
34
34
  # operation.
35
- # @return [Object] Aggregate root corresponding to the given DB representation.
35
+ # @return [Object, nil] Aggregate root corresponding to the given DB representation.
36
36
  def load_one(db_root, identity_map=IdentityMap.new)
37
37
  @engine.load_one(db_root, @domain_class, identity_map)
38
38
  end
@@ -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
@@ -19,19 +19,20 @@ module Vorpal
19
19
  visitor.visit_object(object, config)
20
20
 
21
21
  config.belongs_tos.each do |belongs_to_config|
22
- child = belongs_to_config.get_child(object)
23
- accept(child, visitor, already_visited) if visitor.continue_traversal?(belongs_to_config)
22
+ associate = belongs_to_config.get_associated(object)
23
+ accept(associate, visitor, already_visited) if visitor.continue_traversal?(belongs_to_config)
24
24
  end
25
25
 
26
26
  config.has_ones.each do |has_one_config|
27
- child = has_one_config.get_child(object)
28
- accept(child, visitor, already_visited) if visitor.continue_traversal?(has_one_config)
27
+ associate = has_one_config.get_associated(object)
28
+ accept(associate, visitor, already_visited) if visitor.continue_traversal?(has_one_config)
29
29
  end
30
30
 
31
31
  config.has_manys.each do |has_many_config|
32
- children = has_many_config.get_children(object)
33
- children.each do |child|
34
- accept(child, visitor, already_visited) if visitor.continue_traversal?(has_many_config)
32
+ associates = has_many_config.get_associated(object)
33
+ raise InvariantViolated.new("#{has_many_config.pretty_name} was set to nil. Use an empty array instead.") if associates.nil?
34
+ associates.each do |associate|
35
+ accept(associate, visitor, already_visited) if visitor.continue_traversal?(has_many_config)
35
36
  end
36
37
  end
37
38
  end
@@ -47,4 +48,4 @@ module Vorpal
47
48
  true
48
49
  end
49
50
  end
50
- end
51
+ end
@@ -0,0 +1,84 @@
1
+ require 'vorpal/config/foreign_key_info'
2
+ require 'equalizer'
3
+
4
+ module Vorpal
5
+ module Config
6
+ # @private
7
+ # Object association terminology:
8
+ # - All object associations are uni-directional
9
+ # - The end that holds the association is the 'Owner' and the end that
10
+ # is referred to is the 'Associate' or 'Associates'
11
+ #
12
+ # Relational association terminology:
13
+ # - Local end: has FK
14
+ # - Remote end: has no FK
15
+ #
16
+ class AssociationConfig
17
+ include Equalizer.new(:local_class_config, :fk)
18
+
19
+ attr_reader :local_class_config, :remote_class_configs, :fk
20
+
21
+ # Only one of these two attributes needs to be specified
22
+ # If one is specified, then the association is uni-directional.
23
+ # If both are specified, then the association is bi-directional.
24
+ attr_accessor :local_end_config, :remote_end_config
25
+
26
+ def initialize(local_class_config, fk, fk_type)
27
+ @local_class_config = local_class_config
28
+ @remote_class_configs = {}
29
+ @fk = fk
30
+ @fk_type = fk_type
31
+ end
32
+
33
+ def fk_value(local_db_object)
34
+ local_db_object.send(fk)
35
+ end
36
+
37
+ def associate(local_object, remote_object)
38
+ local_end_config.associate(local_object, remote_object) if local_end_config && local_object
39
+ remote_end_config.associate(remote_object, local_object) if remote_end_config && remote_object
40
+ end
41
+
42
+ def add_remote_class_config(remote_class_configs)
43
+ Array(remote_class_configs).each do |remote_class_config|
44
+ @remote_class_configs[remote_class_config.domain_class.name] = remote_class_config
45
+ end
46
+ end
47
+
48
+ def remote_class_config(local_db_object)
49
+ if polymorphic?
50
+ fk_type_value = local_db_object.send(@fk_type)
51
+ @remote_class_configs[fk_type_value]
52
+ else
53
+ @remote_class_configs.values.first
54
+ end
55
+ end
56
+
57
+ def polymorphic?
58
+ !@fk_type.nil?
59
+ end
60
+
61
+ def set_foreign_key(local_db_object, remote_object)
62
+ local_class_config.set_attribute(local_db_object, @fk, remote_object&.send(unique_key_name))
63
+ local_class_config.set_attribute(local_db_object, @fk_type, remote_object.class.name) if polymorphic?
64
+ end
65
+
66
+ # @return ForeignKeyInfo
67
+ def foreign_key_info(remote_class_config)
68
+ ForeignKeyInfo.new(@fk, @fk_type, remote_class_config.domain_class.name, polymorphic?)
69
+ end
70
+
71
+ def unique_key_name
72
+ (@local_end_config || @remote_end_config).unique_key_name
73
+ end
74
+
75
+ def validate
76
+ if @local_end_config && @remote_end_config
77
+ if @local_end_config.unique_key_name != @remote_end_config.unique_key_name
78
+ raise ConfigurationError.new("#{@local_end_config.pretty_name} and #{@remote_end_config.pretty_name} must have the same unique_key_name/primary_key")
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,35 @@
1
+ require 'vorpal/config/configs'
2
+
3
+ module Vorpal
4
+ module Config
5
+ # @private
6
+ # Object association terminology:
7
+ # - All object associations are uni-directional
8
+ # - The end that holds the association is the 'Owner' and the end that
9
+ # is referred to is the 'Associate' or 'Associates'
10
+ #
11
+ # Relational association terminology:
12
+ # - Local end: has FK
13
+ # - Remote end: has no FK
14
+ #
15
+ class BelongsToConfig
16
+ include Util::HashInitialization
17
+ include LocalEndConfig
18
+
19
+ attr_reader :name, :owned, :fk, :fk_type, :associated_classes, :unique_key_name
20
+ attr_accessor :association_config
21
+
22
+ def get_associated(owner)
23
+ owner.send(name)
24
+ end
25
+
26
+ def associate(owner, associate)
27
+ owner.send("#{name}=", associate)
28
+ end
29
+
30
+ def pretty_name
31
+ "#{association_config.local_class_config.domain_class.name} belongs_to :#{name}"
32
+ end
33
+ end
34
+ end
35
+ 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
@@ -0,0 +1,54 @@
1
+ require 'vorpal/util/hash_initialization'
2
+ require 'vorpal/exceptions'
3
+ require 'equalizer'
4
+
5
+ module Vorpal
6
+ module Config
7
+ # @private
8
+ # Object association terminology:
9
+ # - All object associations are uni-directional
10
+ # - The end that holds the association is the 'Owner' and the end that
11
+ # is referred to is the 'Associate' or 'Associates'
12
+ #
13
+ # Relational association terminology:
14
+ # - Local end: has FK
15
+ # - Remote end: has no FK
16
+ #
17
+ module RemoteEndConfig
18
+ def associated_class_config
19
+ association_config.local_class_config
20
+ end
21
+
22
+ def set_foreign_key(db_associate, owner)
23
+ association_config.set_foreign_key(db_associate, owner)
24
+ end
25
+
26
+ def set_class_config(class_config)
27
+ @class_config = class_config
28
+ end
29
+
30
+ def foreign_key_info
31
+ association_config.foreign_key_info(@class_config)
32
+ end
33
+
34
+ def get_unique_key_value(db_owner)
35
+ db_owner.send(unique_key_name)
36
+ end
37
+ end
38
+
39
+ # @private
40
+ module LocalEndConfig
41
+ def associated_class_config(db_owner)
42
+ association_config.remote_class_config(db_owner)
43
+ end
44
+
45
+ def set_foreign_key(db_owner, associate)
46
+ association_config.set_foreign_key(db_owner, associate)
47
+ end
48
+
49
+ def fk_value(db_owner)
50
+ db_owner.send(fk)
51
+ end
52
+ end
53
+ end
54
+ end