caruby-core 1.4.2 → 1.4.3

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