caruby-core 1.4.2 → 1.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/History.txt +10 -0
  2. data/lib/caruby/cli/command.rb +10 -8
  3. data/lib/caruby/database/fetched_matcher.rb +28 -39
  4. data/lib/caruby/database/lazy_loader.rb +101 -0
  5. data/lib/caruby/database/persistable.rb +190 -167
  6. data/lib/caruby/database/persistence_service.rb +21 -7
  7. data/lib/caruby/database/persistifier.rb +185 -0
  8. data/lib/caruby/database/reader.rb +106 -176
  9. data/lib/caruby/database/saved_matcher.rb +56 -0
  10. data/lib/caruby/database/search_template_builder.rb +1 -1
  11. data/lib/caruby/database/sql_executor.rb +8 -7
  12. data/lib/caruby/database/store_template_builder.rb +134 -61
  13. data/lib/caruby/database/writer.rb +252 -52
  14. data/lib/caruby/database.rb +88 -67
  15. data/lib/caruby/domain/attribute_initializer.rb +16 -0
  16. data/lib/caruby/domain/attribute_metadata.rb +161 -72
  17. data/lib/caruby/domain/id_alias.rb +22 -0
  18. data/lib/caruby/domain/inversible.rb +91 -0
  19. data/lib/caruby/domain/merge.rb +116 -35
  20. data/lib/caruby/domain/properties.rb +1 -1
  21. data/lib/caruby/domain/reference_visitor.rb +207 -71
  22. data/lib/caruby/domain/resource_attributes.rb +93 -80
  23. data/lib/caruby/domain/resource_dependency.rb +22 -97
  24. data/lib/caruby/domain/resource_introspection.rb +21 -28
  25. data/lib/caruby/domain/resource_inverse.rb +134 -0
  26. data/lib/caruby/domain/resource_metadata.rb +41 -19
  27. data/lib/caruby/domain/resource_module.rb +42 -33
  28. data/lib/caruby/import/java.rb +8 -9
  29. data/lib/caruby/migration/migrator.rb +20 -7
  30. data/lib/caruby/migration/resource_module.rb +0 -2
  31. data/lib/caruby/resource.rb +132 -351
  32. data/lib/caruby/util/cache.rb +4 -1
  33. data/lib/caruby/util/class.rb +48 -1
  34. data/lib/caruby/util/collection.rb +54 -18
  35. data/lib/caruby/util/inflector.rb +7 -0
  36. data/lib/caruby/util/options.rb +35 -31
  37. data/lib/caruby/util/partial_order.rb +1 -1
  38. data/lib/caruby/util/properties.rb +2 -2
  39. data/lib/caruby/util/stopwatch.rb +16 -8
  40. data/lib/caruby/util/transitive_closure.rb +1 -1
  41. data/lib/caruby/util/visitor.rb +342 -328
  42. data/lib/caruby/version.rb +1 -1
  43. data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
  44. data/lib/caruby.rb +2 -0
  45. metadata +10 -9
  46. data/lib/caruby/database/saved_merger.rb +0 -131
  47. data/lib/caruby/domain/annotatable.rb +0 -25
  48. data/lib/caruby/domain/annotation.rb +0 -23
  49. data/lib/caruby/import/annotatable_class.rb +0 -28
  50. data/lib/caruby/import/annotation_class.rb +0 -27
  51. data/lib/caruby/import/annotation_module.rb +0 -67
  52. data/lib/caruby/migration/resource.rb +0 -8
data/History.txt CHANGED
@@ -6,3 +6,13 @@
6
6
 
7
7
  * Minor Migrator fixes
8
8
 
9
+
10
+ === 1.4.3 / 2011-02-18
11
+
12
+ * Refactor Persistifier
13
+
14
+ * Add attribute filters
15
+
16
+
17
+
18
+
@@ -13,22 +13,22 @@ module CaRuby
13
13
  # specifications as follows:
14
14
  #
15
15
  # Each option specification is an array in the form:
16
- # [:option, short, long, class, description]
16
+ # [option, short, long, class, description]
17
17
  # where:
18
- # * :option is the option symbol, e.g. +:output+
18
+ # * option is the option symbol, e.g. +:output+
19
19
  # * short is the short option form, e.g. "-o"
20
20
  # * long is the long option form, e.g. "--output FILE"
21
21
  # * class is the option value class, e.g. Integer
22
22
  # * description is the option usage, e.g. "Output file"
23
- # The :option, long and description items are required; the short and class items can
23
+ # The option, long and description items are required; the short and class items can
24
24
  # be omitted.
25
25
  #
26
26
  # Each command line argument specification is an array in the form:
27
- # [:arg, text]
27
+ # [arg, text]
28
28
  # where:
29
- # * :arg is the argument symbol, e.g. +:input+
29
+ # * arg is the argument symbol, e.g. +:input+
30
30
  # * text is the usage message text, e.g. 'input', '[input]' or 'input ...'
31
- # Both items are required.
31
+ # The arg and description items are required.
32
32
  #
33
33
  # Built-in options include the following:
34
34
  # * --help : print the help message and exit
@@ -36,6 +36,7 @@ module CaRuby
36
36
  # * --log FILE : log file
37
37
  # * --debug : print debug messages to the log
38
38
  # * --file FILE: configuration file containing other options
39
+ # * --quiet: suppress printing messages to stdout
39
40
  # This class processes these built-in options, with the exception of +--version+,
40
41
  # which is a subclass responsibility. Subclasses are responsible for
41
42
  # processing any remaining options.
@@ -70,10 +71,11 @@ module CaRuby
70
71
  private
71
72
 
72
73
  DEF_OPTS = [
73
- [:help, "--help", "Displays this help message"],
74
+ [:help, "-h", "--help", "Display this help message"],
74
75
  [:file, "--file FILE", "Configuration file containing other options"],
75
76
  [:log, "--log FILE", "Log file"],
76
- [:debug, "--debug", "Displays debug log messages"],
77
+ [:debug, "--debug", "Display debug log messages"],
78
+ [:quiet, "-q", "--quiet", "Suppress printing messages to stdout"]
77
79
  ]
78
80
 
79
81
  def call_executor(opts)
@@ -1,32 +1,45 @@
1
1
  require 'caruby/util/options'
2
2
  require 'caruby/util/collection'
3
- require 'caruby/util/cache'
4
- require 'caruby/util/pretty_print'
5
- require 'caruby/domain/reference_visitor'
6
- require 'caruby/database/search_template_builder'
7
3
 
8
4
  module CaRuby
9
5
  class Database
10
6
  # Proc that matches fetched sources to targets.
11
7
  class FetchedMatcher < Proc
12
8
  # Initializes a new FetchedMatcher.
13
- #
14
- # @param [{Symbol => Object}, Symbol, nil] opts the match options
15
- # @option [Boolean] opts :relaxed flag indicating whether a {Resource#minimal_match?} is
16
- # used in the match on the fetched content
17
- def initialize(opts=nil)
18
- super() { |srcs, tgts| match_fetched(srcs, tgts) }
19
- @relaxed = Options.get(:relaxed, opts)
9
+ def initialize
10
+ super { |srcs, tgts| match_fetched(srcs, tgts) }
20
11
  end
21
12
 
22
13
  private
23
14
 
24
- # Returns a target => source match hash for the given targets and sources.
15
+ # Returns a target => source match hash for the given targets and sources using
16
+ # {Resource#match_in_owner_scope}.
17
+ #
18
+ # @param [<Resource>] sources the domain objects to match with targets
19
+ # @param [<Resource>] targets the domain objects to match with targets
20
+ # @return [{Resource => Resource}] the source => target matches
25
21
  def match_fetched(sources, targets)
26
22
  return Hash::EMPTY_HASH if sources.empty? or targets.empty?
27
- logger.debug { "Matching database content #{sources.qp} to #{targets.qp}..." }
28
-
29
- # match source => target based on the key
23
+ # the domain class
24
+ klass = sources.first.class
25
+ # the non-owner secondary key domain attributes
26
+ attrs = klass.secondary_key_attributes.select do |attr|
27
+ attr_md = klass.attribute_metadata(attr)
28
+ attr_md.domain? and not attr_md.owner?
29
+ end
30
+ # fetch the non-owner secondary key domain attributes as necessary
31
+ unless attrs.empty? then
32
+ sources.each do |src|
33
+ attrs.each do |attr|
34
+ next if src.send(attr)
35
+ logger.debug { "Fetching #{src.qp} #{attr} in order to match on the secondary key..." }
36
+ ref = src.query(attr).first || next
37
+ src.set_attribute(attr, ref)
38
+ logger.debug { "Set fetched #{src.qp} secondary key attribute #{attr} to fetched #{ref}." }
39
+ end
40
+ end
41
+ end
42
+ # match source => target based on the secondary key
30
43
  unmatched = Set === sources ? sources.dup : sources.to_set
31
44
  matches = {}
32
45
  targets.each do |tgt|
@@ -35,30 +48,6 @@ module CaRuby
35
48
  matches[src] = tgt
36
49
  unmatched.delete(src)
37
50
  end
38
-
39
- # match residual targets, if any, on a relaxed criterion
40
- if @relaxed and matches.size != targets.size then
41
- unmtchd_tgts = targets.to_set - matches.keys.delete_if { |tgt| tgt.identifier }
42
- unmtchd_srcs = sources.to_set - matches.values
43
- min_mtchs = match_minimal(unmtchd_srcs, unmtchd_tgts)
44
- matches.merge!(min_mtchs)
45
- end
46
-
47
- logger.debug { "Matched database sources to targets #{matches.qp}." } unless matches.empty?
48
- matches
49
- end
50
-
51
- #@param [<Resource>] sources the source objects to match
52
- #@param [<Resource>] targets the potential match target objects
53
- # @return (see #match_saved)
54
- def match_minimal(sources, targets)
55
- matches = {}
56
- unmatched = Set === sources ? sources.to_set : sources.dup
57
- targets.each do |tgt|
58
- src = unmatched.detect { |src| tgt.minimal_match?(src) } || next
59
- matches[src] = tgt
60
- unmatched.delete(src)
61
- end
62
51
  matches
63
52
  end
64
53
  end
@@ -0,0 +1,101 @@
1
+ require 'caruby/database/fetched_matcher'
2
+
3
+ module CaRuby
4
+ class Database
5
+ # A LazyLoader fetches an association from the database on demand.
6
+ class LazyLoader < Proc
7
+ # Creates a new LazyLoader which calls the loader block on the subject.
8
+ #
9
+ # @yield [subject, attribute] fetches the given subject attribute value from the database
10
+ # @yieldparam [Resource] subject the domain object whose attribute is to be loaded
11
+ # @yieldparam [Symbol] attribute the domain attribute to load
12
+ def initialize(&loader)
13
+ super { |sbj, attr| load(sbj, attr, &loader) }
14
+ # the fetch result matcher
15
+ @matcher = FetchedMatcher.new
16
+ @enabled = true
17
+ end
18
+
19
+ # Disables this lazy loader. If the loader is already disabled, then this method is a no-op.
20
+ # Otherwise, if a block is given, then the lazy loader is reenabled after the block is executed.
21
+ #
22
+ # @yield the block to call while the loader is disabled
23
+ # @return the result of calling the block if a block is given, nil otherwise
24
+ def disable
25
+ reenable = set_disabled
26
+ return unless block_given?
27
+ begin
28
+ yield
29
+ ensure
30
+ set_enabled if reenable
31
+ end
32
+ end
33
+
34
+ alias :suspend :disable
35
+
36
+ # Enables this lazy loader. If the loader is already enabled, then this method is a no-op.
37
+ # Otherwise, if a block is given, then the lazy loader is redisabled after the block is executed.
38
+ #
39
+ # @yield the block to call while the loader is enabled
40
+ # @return the result of calling the block if a block is given, nil otherwise
41
+ def enable
42
+ redisable = set_enabled
43
+ return unless block_given?
44
+ begin
45
+ yield
46
+ ensure
47
+ set_disabled if redisable
48
+ end
49
+ end
50
+
51
+ alias :resume :enable
52
+
53
+ # @return [Boolean] whether this loader is enabled
54
+ def enabled?
55
+ @enabled
56
+ end
57
+
58
+ # @return [Boolean] whether this loader is disabled
59
+ def disabled?
60
+ not @enabled
61
+ end
62
+
63
+ private
64
+
65
+ # Disables this loader.
66
+ #
67
+ # @return [Boolean] true if this loader was previously enabled, false otherwise
68
+ def set_disabled
69
+ enabled? and (@enabled = false; true)
70
+ end
71
+
72
+ # Enables this loader.
73
+ #
74
+ # @return [Boolean] true if this loader was previously disabled, false otherwise
75
+ def set_enabled
76
+ disabled? and (@enabled = true)
77
+ end
78
+
79
+ # @param [Resource] subject the domain object whose attribute is to be loaded
80
+ # @param [Symbol] the domain attribute to load
81
+ # @yield (see #initialize)
82
+ # @yieldparam (see #initialize)
83
+ # @return the attribute value loaded from the database
84
+ # @raise [RuntimeError] if this loader is disabled
85
+ def load(subject, attribute)
86
+ if disabled? then raise RuntimeError.new("#{subject.qp} lazy load called on disabled loader") end
87
+ logger.debug { "Lazy-loading #{subject.qp} #{attribute}..." }
88
+ # the current value
89
+ oldval = subject.send(attribute)
90
+ # load the fetched value
91
+ fetched = yield(subject, attribute)
92
+ # nothing to merge if nothing fetched
93
+ return oldval if fetched.nil_or_empty?
94
+ # merge the fetched into the attribute
95
+ logger.debug { "Merging #{subject.qp} fetched #{attribute} value #{fetched.qp}#{' into ' + oldval.qp if oldval}..." }
96
+ matches = @matcher.call(fetched.to_enum, oldval.to_enum)
97
+ subject.merge_attribute(attribute, fetched, matches)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -5,20 +5,33 @@ require 'caruby/util/collection'
5
5
  require 'caruby/util/validation'
6
6
 
7
7
  module CaRuby
8
- # The Persistable mixin adds persistance capability.
8
+ # The Persistable mixin adds persistance capability. Every instance which includes Persistable
9
+ # must respond to an overrided {#database} method.
9
10
  module Persistable
10
11
  include Validation
11
12
 
12
- # @return [LazyLoader] the loader which fetches references on demand
13
- attr_reader :lazy_loader
14
-
15
13
  # @return [{Symbol => Object}] the content value hash at the point of the last
16
14
  # take_snapshot call
17
15
  attr_reader :snapshot
18
-
19
- # @return [#query, #find, #store, #create, #update, #delete] the data access mediator
20
- # for this Persistable
21
- # @raise [NotImplementedError] if the subclass does not define this method
16
+
17
+ # @param [Resource, <Resource>, nil] obj the object(s) to check
18
+ # @return [Boolean] whether the given object(s) have an identifier, or the object is nil or empty
19
+ def self.saved?(obj)
20
+ if obj.collection? then
21
+ obj.all? { |ref| saved?(ref) }
22
+ else
23
+ obj.nil? or obj.identifier
24
+ end
25
+ end
26
+
27
+ # @param [Resource, <Resource>, nil] obj the object(s) to check
28
+ # @return [Boolean] whether at least one of the given object(s) does not have an identifier
29
+ def self.unsaved?(obj)
30
+ not saved?(obj)
31
+ end
32
+
33
+ # @return [Database] the data access mediator for this Persistable
34
+ # @raise [NotImplementedError] if the Persistable subclass does not define this method
22
35
  def database
23
36
  raise NotImplementedError.new("Database operations are not available for #{self}")
24
37
  end
@@ -91,7 +104,7 @@ module CaRuby
91
104
  alias :== :equal?
92
105
 
93
106
  alias :eql? :==
94
-
107
+
95
108
  # Captures the Persistable's updatable attribute base values.
96
109
  # The snapshot is subsequently accessible using the {#snapshot} method.
97
110
  #
@@ -104,7 +117,27 @@ module CaRuby
104
117
  def snapshot_taken?
105
118
  not @snapshot.nil?
106
119
  end
107
-
120
+
121
+ # Merges the other domain object non-domain attribute values into this domain object's snapshot,
122
+ # An existing snapshot value is replaced by the corresponding other attribute value.
123
+ #
124
+ # @param [Resource] other the source domain object
125
+ # @raise [ValidationError] if this domain object does not have a snapshot
126
+ def merge_into_snapshot(other)
127
+ unless snapshot_taken? then
128
+ raise ValidationError.new("Cannot merge #{other.qp} content into #{qp} snapshot, since #{qp} does not have a snapshot.")
129
+ end
130
+ # the non-domain attribute => [target value, other value] difference hash
131
+ delta = diff(other)
132
+ # the difference attribute => other value hash, excluding nil other values
133
+ dvh = delta.transform { |d| d.last }
134
+ return if dvh.empty?
135
+ logger.debug { "#{qp} differs from database content #{other.qp} as follows: #{delta.filter_on_key { |attr| dvh.has_key?(attr) }.qp}" }
136
+ logger.debug { "Setting #{qp} snapshot values from other #{other.qp} values to reflect the database state: #{dvh.qp}..." }
137
+ # update the snapshot from the other value to reflect the database state
138
+ @snapshot.merge!(dvh)
139
+ end
140
+
108
141
  # Returns whether this Persistable either doesn't have a snapshot or has changed since the last snapshot.
109
142
  # This is a conservative condition test that returns false if there is no snaphsot for this Persistable
110
143
  # and therefore no basis to determine whether the content changed.
@@ -118,7 +151,8 @@ module CaRuby
118
151
  def changed_attributes
119
152
  if @snapshot then
120
153
  ovh = value_hash(self.class.updatable_attributes)
121
- diff = @snapshot.diff(ovh) { |attr, v, ov| Resource.value_equal?(v, ov) }
154
+ diff = @snapshot.diff(ovh) { |attr, v, ov|
155
+ Resource.value_equal?(v, ov) }
122
156
  diff.keys
123
157
  else
124
158
  self.class.updatable_attributes
@@ -132,84 +166,41 @@ module CaRuby
132
166
  # Each of the attributes which does not already hold a non-nil or non-empty value
133
167
  # will be loaded from the database on demand.
134
168
  # This method injects attribute value initialization into each loadable attribute reader.
135
- # The initializer is given by either the loader Proc argument or the block provided
136
- # to this method. The loader takes two arguments, the target object and the attribute to load.
169
+ # The initializer is given by either the loader Proc argument.
170
+ # The loader takes two arguments, the target object and the attribute to load.
137
171
  # If this Persistable already has a lazy loader, then this method is a no-op.
138
172
  #
139
173
  # Lazy loading is disabled on an attribute after it is invoked on that attribute or when the
140
174
  # attribute setter method is called.
141
175
  #
142
176
  # @param loader [LazyLoader] the lazy loader to add
143
- # @yield [sources, targets] source => target matcher
144
- # @yieldparam [<Resource>] sources the fetched domain object match candidates
145
- # @yieldparam [<Resource>] targets the search target domain objects to match
146
- # @raise [ValidationError] if this domain object does not have an identifier
147
- def add_lazy_loader(loader, &matcher)
177
+ def add_lazy_loader(loader, attributes=nil)
148
178
  # guard against invalid call
149
- raise ValidationError.new("Cannot add lazy loader to an unfetched domain object: #{self}") if identifier.nil?
150
- # no-op if there is already a loader
151
- return if @lazy_loader
179
+ if identifier.nil? then raise ValidationError.new("Cannot add lazy loader to an unfetched domain object: #{self}") end
152
180
 
153
181
  # the attributes to lazy-load
154
- attrs = self.class.loadable_attributes
155
- return if attrs.empty?
156
-
157
- # make the lazy loader
158
- @lazy_loader = LazyLoader.new(self, loader, &matcher)
182
+ attributes ||= loadable_attributes
183
+ return if attributes.empty?
159
184
  # define the reader and writer method overrides for the missing attributes
160
- loaded = attrs.select { |attr| persistable__add_loader(attr) }
185
+ loaded = attributes.select { |attr| inject_lazy_loader(attr) }
161
186
  logger.debug { "Lazy loader added to #{qp} attributes #{loaded.to_series}." } unless loaded.empty?
162
187
  end
163
188
 
164
- # Returns the attributes to load on demand. The base attribute list is given by
165
- # {ResourceAttributes#loadable_attributes}. In additon, if this Persistable has
166
- # more than one {ResourceDependency#owner_attributes} and one is non-nil, then
167
- # none of the owner attributes are loaded on demand, since there can be at most
168
- # one owner and ownership cannot change.
189
+ # Returns the attributes to load on demand. The base attribute list is given by the
190
+ # {ResourceAttributes#loadable_attributes} whose value is nil or empty.
191
+ # In addition, if this Persistable has more than one {ResourceDependency#owner_attributes}
192
+ # and one is non-nil, then none of the owner attributes are loaded on demand,
193
+ # since there can be at most one owner and ownership cannot change.
169
194
  #
170
195
  # @return [<Symbol>] the attributes to load on demand
171
196
  def loadable_attributes
197
+ attrs = self.class.loadable_attributes.select { |attr| send(attr).nil_or_empty? }
172
198
  ownr_attrs = self.class.owner_attributes
173
- if ownr_attrs.size == 2 and ownr_attrs.detect { |attr| send(ownr_attr) } then
174
- self.class.loadable_attributes - ownr_attrs
199
+ # If there is an owner, then variant owners are not loaded.
200
+ if ownr_attrs.size > 1 and ownr_attrs.any? { |attr| not send(attr).nil_or_empty? } then
201
+ attrs - ownr_attrs
175
202
  else
176
- self.class.loadable_attributes
177
- end
178
- end
179
-
180
- # Disables this Persistable's lazy loader, if one exists. If a block is given to this
181
- # method, then the loader is only disabled while the block is executed.
182
- #
183
- # @yield the block to call while the loader is suspended
184
- # @return the result of calling the block, or self if no block is given
185
- def suspend_lazy_loader
186
- unless @lazy_loader and @lazy_loader.enabled? then
187
- return block_given? ? yield : self
188
- end
189
- @lazy_loader.disable
190
- return self unless block_given?
191
- begin
192
- yield
193
- ensure
194
- @lazy_loader.enable
195
- end
196
- end
197
-
198
- # Enables this Persistable's lazy loader, if one exists. If a block is given to this
199
- # method, then the loader is only enabled while the block is executed.
200
- #
201
- # @yield the block to call while the loader is enabled
202
- # @return the result of calling the block, or self if no block is given
203
- def resume_lazy_loader
204
- unless @lazy_loader and @lazy_loader.disabled? then
205
- return block_given? ? yield : self
206
- end
207
- @lazy_loader.enable
208
- return self unless block_given?
209
- begin
210
- yield
211
- ensure
212
- @lazy_loader.disable
203
+ attrs
213
204
  end
214
205
  end
215
206
 
@@ -219,13 +210,9 @@ module CaRuby
219
210
  #
220
211
  # @param [Symbol] the attribute to remove from the load list, or nil if to remove all attributes
221
212
  def remove_lazy_loader(attribute=nil)
222
- return if @lazy_loader.nil?
223
213
  if attribute.nil? then
224
- self.class.domain_attributes.each { |attr| remove_lazy_loader(attr) }
225
- @lazy_loader = nil
226
- return
214
+ return self.class.domain_attributes.each { |attr| remove_lazy_loader(attr) }
227
215
  end
228
-
229
216
  # the modified accessor method
230
217
  reader, writer = self.class.attribute_metadata(attribute).accessors
231
218
  # remove the reader override
@@ -235,23 +222,108 @@ module CaRuby
235
222
  end
236
223
 
237
224
  # Returns whether this domain object must be fetched to reflect the database state.
238
- # This default implementation returns whether there are any autogenerated attributes.
239
- # Subclasses can override with more restrictive conditions.
225
+ # This default implementation returns whether this domain object was created and
226
+ # there are any autogenerated attributes. Subclasses can override to relax or restrict
227
+ # the condition.
240
228
  #
241
- # caBIG alert - the auto-generated criterion is a sufficient but not necessary condition
242
- # to determine whether a save caCORE result does not necessarily accurately reflect the
243
- # database state. Examples:
244
- # * caTissue SCG name is auto-generated on SCG create but not SCG update.
229
+ # caCORE alert - the auto-generated criterion is a necessary but not sufficient condition
230
+ # to determine whether a save caCORE result reflects the database state. Example:
245
231
  # * caTissue SCG event parameters are not auto-generated on SCG create if the SCG collection
246
232
  # status is Pending, but are auto-generated on SCG update if the SCG status is changed
247
- # to Complete.
233
+ # to Complete. By contrast, the SCG specimens are auto-generated on SCG create, even if
234
+ # the status is +Pending+.
248
235
  # The caBIG application can override this method in a Database subclass to fine-tune the
249
236
  # fetch criteria. Adding a more restrictive {#fetch_saved?} condition will will improve
250
- # performance but not change functionality.
237
+ # performance but not change functionality.
238
+ #
239
+ # caCORE alert - a saved attribute which is cascaded but not fetched must be fetched in
240
+ # order to reflect the database identifier in the saved object.
251
241
  #
252
242
  # @return [Boolean] whether this domain object must be fetched to reflect the database state
253
243
  def fetch_saved?
254
- not self.class.autogenerated_attributes.empty?
244
+ # only fetch a create, not an update (note that subclasses can override this condition)
245
+ return false if identifier
246
+ # Check for an attribute with a value that might need to be changed in order to
247
+ # reflect the auto-generated database content.
248
+ ag_attrs = self.class.autogenerated_attributes
249
+ return false if ag_attrs.empty?
250
+ ag_attrs.any? { |attr| not send(attr).nil_or_empty? }
251
+ end
252
+
253
+ # Returns this domain object's attributes which must be fetched to reflect the database state.
254
+ # This default implementation returns the {ResourceAttributes#autogenerated_logical_dependent_attributes}
255
+ # if this domain object does not have an identifier, or an empty array otherwise.
256
+ # Subclasses can override to relax or restrict the condition.
257
+ #
258
+ # caCORE alert - the auto-generated criterion is a necessary but not sufficient condition
259
+ # to determine whether a save caCORE result reflects the database state. Example:
260
+ # * caTissue SCG event parameters are not auto-generated on SCG create if the SCG collection
261
+ # status is Pending, but are auto-generated on SCG update if the SCG status is changed
262
+ # to Complete. By contrast, the SCG specimens are auto-generated on SCG create, even if
263
+ # the status is +Pending+.
264
+ # The caBIG application can override this method in a Database subclass to fine-tune the
265
+ # fetch criteria. Adding a more restrictive {#fetch_saved?} condition will will improve
266
+ # performance but not change functionality.
267
+ #
268
+ # caCORE alert - a saved attribute which is cascaded but not fetched must be fetched in
269
+ # order to reflect the database identifier in the saved object.
270
+ #
271
+ # @param [Database::Operation] the save operation
272
+ # @return [<Symbol>] whether this domain object must be fetched to reflect the database state
273
+ def saved_fetch_attributes(operation)
274
+ # only fetch a create, not an update (note that subclasses can override this condition)
275
+ if operation.type == :create or operation.autogenerated? then
276
+ # Filter the class saved fetch attributes for content.
277
+ self.class.saved_fetch_attributes.select { |attr| not send(attr).nil_or_empty? }
278
+ else
279
+ Array::EMPTY_ARRAY
280
+ end
281
+ end
282
+
283
+ # Relaxes the {CaRuby::Persistable#saved_fetch_attributes} condition for a SCG as follows:
284
+ # * If the SCG status was updated from +Pending+ to +Collected+, then fetch the saved SCG event parameters.
285
+ #
286
+ # @param (see CaRuby::Persistable#saved_fetch_attributes)
287
+ # @return (see CaRuby::Persistable#saved_fetch_attributes)
288
+ def autogenerated?(operation)
289
+ operation == :update && status_changed_to_complete? ? EVENT_PARAM_ATTRS : super
290
+ end
291
+
292
+ def fetch_autogenerated?(operation)
293
+ # only fetch a create, not an update (note that subclasses can override this condition)
294
+ operation == :update
295
+ # Check for an attribute with a value that might need to be changed in order to
296
+ # reflect the auto-generated database content.
297
+ self.class.autogenerated_logical_dependent_attributes.select { |attr| not send(attr).nil_or_empty? }
298
+ end
299
+
300
+ # Returns whether this domain object must be fetched to reflect the database state.
301
+ # This default implementation returns whether this domain object was created and
302
+ # there are any autogenerated attributes. Subclasses can override to relax or restrict
303
+ # the condition.
304
+ #
305
+ # caCORE alert - the auto-generated criterion is a necessary but not sufficient condition
306
+ # to determine whether a save caCORE result reflects the database state. Example:
307
+ # * caTissue SCG event parameters are not auto-generated on SCG create if the SCG collection
308
+ # status is Pending, but are auto-generated on SCG update if the SCG status is changed
309
+ # to Complete. By contrast, the SCG specimens are auto-generated on SCG create, even if
310
+ # the status is +Pending+.
311
+ # The caBIG application can override this method in a Database subclass to fine-tune the
312
+ # fetch criteria. Adding a more restrictive {#fetch_saved?} condition will will improve
313
+ # performance but not change functionality.
314
+ #
315
+ # caCORE alert - a saved attribute which is cascaded but not fetched must be fetched in
316
+ # order to reflect the database identifier in the saved object.
317
+ #
318
+ # @return [Boolean] whether this domain object must be fetched to reflect the database state
319
+ def fetch_saved?
320
+ # only fetch a create, not an update (note that subclasses can override this condition)
321
+ return false if identifier
322
+ # Check for an attribute with a value that might need to be changed in order to
323
+ # reflect the auto-generated database content.
324
+ ag_attrs = self.class.autogenerated_attributes
325
+ return false if ag_attrs.empty?
326
+ ag_attrs.any? { |attr| not send(attr).nil_or_empty? }
255
327
  end
256
328
 
257
329
  # Sets the {ResourceAttributes#volatile_nondomain_attributes} to the other fetched value,
@@ -259,7 +331,10 @@ module CaRuby
259
331
  #
260
332
  # @param [Resource] other the fetched domain object reflecting the database state
261
333
  def copy_volatile_attributes(other)
262
- self.class.volatile_nondomain_attributes.each do |attr|
334
+ attrs = self.class.volatile_nondomain_attributes
335
+ return if attrs.empty?
336
+ logger.debug { "Merging volatile attributes #{attrs.to_series} from #{other.qp} into #{qp}..." }
337
+ attrs.each do |attr|
263
338
  val = send(attr)
264
339
  oval = other.send(attr)
265
340
  # set the attribute to the other value if it differs from the current value
@@ -287,8 +362,10 @@ module CaRuby
287
362
  ovh = value_hash(self.class.updatable_attributes)
288
363
 
289
364
 
290
- # KLUDGE TODO - fix
365
+ # KLUDGE TODO - confirm this is still a problem and fix
291
366
  # In Galena frozen migration example, SpecimenPosition snapshot doesn't include identifier; work around this here
367
+ # This could be related to the problem of an abstract DomainObject not being added as a domain module class. See the
368
+ # ClinicalTrials::Resource for more info.
292
369
  if ovh[:identifier] and not @snapshot[:identifier] then
293
370
  @snapshot[:identifier] = ovh[:identifier]
294
371
  end
@@ -316,54 +393,43 @@ module CaRuby
316
393
  end
317
394
  end
318
395
 
319
- # Adds this Persistable lazy loader to the given attribute unless the attribute already holds a fetched reference.
320
- # Returns the loader if the loader was added to attribute.
321
- def persistable__add_loader(attribute)
322
- # bail if there is already a fetched reference
323
- return if send(attribute).to_enum.any? { |ref| ref.identifier }
324
-
396
+ # Adds this Persistable lazy loader to the given attribute unless the attribute already holds a
397
+ # fetched reference.
398
+ #
399
+ # @param [Symbol] attribute the attribute to mod
400
+ # @return [Boolean] whether a loader was added to the attribute
401
+ def inject_lazy_loader(attribute)
402
+ # bail if there is already a value
403
+ send(attribute).enumerate { |ref| return false unless ref.identifier }
325
404
  # the accessor methods to modify
326
- attr_md = self.class.attribute_metadata(attribute)
327
- reader, writer = attr_md.accessors
328
- raise NotImplementedError.new("Missing writer method for #{self.class.qp} attribute #{attribute}") if writer.nil?
329
-
330
- # define the singleton attribute reader method
331
- instance_eval "def #{reader}; @lazy_loader ? persistable__load_reference(:#{attribute}) : super; end"
332
- # define the singleton attribute writer method
405
+ reader, writer = self.class.attribute_metadata(attribute).accessors
406
+ # The singleton attribute reader method loads the reference once and thenceforth calls the
407
+ # standard reader.
408
+ instance_eval "def #{reader}; load_reference(:#{attribute}); end"
409
+ # The singleton attribute writer method removes the lazy loader once and thenceforth calls
410
+ # the standard writer.
333
411
  instance_eval "def #{writer}(value); remove_lazy_loader(:#{attribute}); super; end"
334
-
335
- @lazy_loader
412
+ true
336
413
  end
337
414
 
338
415
  # Loads the reference attribute database value into this Persistable.
339
416
  #
340
417
  # @param [Symbol] attribute the attribute to load
341
418
  # @return the attribute value merged from the database value
342
- def persistable__load_reference(attribute)
343
- attr_md = self.class.attribute_metadata(attribute)
419
+ def load_reference(attribute)
420
+ ldr = database.lazy_loader
344
421
  # bypass the singleton method and call the class instance method if the lazy loader is disabled
345
- unless @lazy_loader.enabled? then
346
- # the modified accessor method
347
- reader, writer = attr_md.accessors
348
- return self.class.instance_method(reader).bind(self).call
422
+ unless ldr.enabled? then
423
+ return self.class.instance_method(attribute).bind(self).call
349
424
  end
350
-
351
- # Disable lazy loading first for the attribute, since the reader method might be called in
352
- # the sequel, resulting in an infinite loop when the lazy loader is retriggered.
425
+
426
+ # Disable lazy loading first for the attribute, since the reader method is called by the loader.
353
427
  remove_lazy_loader(attribute)
354
- logger.debug { "Lazy-loading #{qp} #{attribute}..." }
355
- # the current value
356
- oldval = send(attribute)
357
428
  # load the fetched value
358
- fetched = @lazy_loader.load(attribute)
359
- # nothing to do if nothing fetched
360
- return oldval if fetched.nil_or_empty?
429
+ merged = ldr.call(self, attribute)
361
430
 
362
- # merge the fetched into the attribute
363
- logger.debug { "Merging #{qp} fetched #{attribute} value #{fetched.qp}#{' into ' + oldval.qp if oldval}..." }
364
- matcher = @lazy_loader.matcher
365
- merged = merge_attribute_value(attribute, oldval, fetched, &matcher)
366
- # update the snapshot of dependents
431
+ # update dependent snapshots if necessary
432
+ attr_md = self.class.attribute_metadata(attribute)
367
433
  if attr_md.dependent? then
368
434
  # the owner attribute
369
435
  oattr = attr_md.inverse
@@ -373,11 +439,12 @@ module CaRuby
373
439
  merged.enumerate do |dep|
374
440
  if dep.snapshot_taken? then
375
441
  dep.snapshot[oattr] = self
376
- logger.debug { "Updated #{qp} #{attribute} fetched dependent #{dep.qp} snapshot with #{oattr} value #{qp}." }
442
+ logger.debug { "Updated the #{qp} fetched #{attribute} dependent #{dep.qp} snapshot with #{oattr} value #{qp}." }
377
443
  end
378
444
  end
379
445
  end
380
446
  end
447
+
381
448
  merged
382
449
  end
383
450
 
@@ -396,49 +463,5 @@ module CaRuby
396
463
  instance_eval "def #{name_or_sym}(#{args}); super; end"
397
464
  end
398
465
  end
399
-
400
- class LazyLoader
401
- # @return [Proc] the source => target matcher
402
- attr_reader :matcher
403
-
404
- # Creates a new LazyLoader which calls the loader Proc on the subject.
405
- #
406
- # @raise [ArgumentError] if the loader is not given to this initializer
407
- def initialize(subject, loader=nil, &matcher)
408
- @subject = subject
409
- # the loader proc from either the argument or the block
410
- @loader = loader
411
- @matcher = matcher
412
- raise ArgumentError.new("Neither a loader nor a block is given to the LazyLoader initializer") if @loader.nil?
413
- @enabled = true
414
- end
415
-
416
- # Returns whether this loader is enabled.
417
- def enabled?
418
- @enabled
419
- end
420
-
421
- # Returns whether this loader is disabled.
422
- def disabled?
423
- not @enabled
424
- end
425
-
426
- # Disable this loader.
427
- def disable
428
- @enabled = false
429
- end
430
-
431
- # Enables this loader.
432
- def enable
433
- @enabled = true
434
- end
435
-
436
- # Returns the attribute value loaded from the database.
437
- # Raises DatabaseError if this loader is disabled.
438
- def load(attribute)
439
- raise DatabaseError.new("#{qp} lazy load called on disabled loader") unless enabled?
440
- @loader.call(@subject, attribute)
441
- end
442
- end
443
466
  end
444
467
  end