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.
- data/History.txt +10 -0
- data/lib/caruby/cli/command.rb +10 -8
- data/lib/caruby/database/fetched_matcher.rb +28 -39
- data/lib/caruby/database/lazy_loader.rb +101 -0
- data/lib/caruby/database/persistable.rb +190 -167
- data/lib/caruby/database/persistence_service.rb +21 -7
- data/lib/caruby/database/persistifier.rb +185 -0
- data/lib/caruby/database/reader.rb +106 -176
- data/lib/caruby/database/saved_matcher.rb +56 -0
- data/lib/caruby/database/search_template_builder.rb +1 -1
- data/lib/caruby/database/sql_executor.rb +8 -7
- data/lib/caruby/database/store_template_builder.rb +134 -61
- data/lib/caruby/database/writer.rb +252 -52
- data/lib/caruby/database.rb +88 -67
- data/lib/caruby/domain/attribute_initializer.rb +16 -0
- data/lib/caruby/domain/attribute_metadata.rb +161 -72
- data/lib/caruby/domain/id_alias.rb +22 -0
- data/lib/caruby/domain/inversible.rb +91 -0
- data/lib/caruby/domain/merge.rb +116 -35
- data/lib/caruby/domain/properties.rb +1 -1
- data/lib/caruby/domain/reference_visitor.rb +207 -71
- data/lib/caruby/domain/resource_attributes.rb +93 -80
- data/lib/caruby/domain/resource_dependency.rb +22 -97
- data/lib/caruby/domain/resource_introspection.rb +21 -28
- data/lib/caruby/domain/resource_inverse.rb +134 -0
- data/lib/caruby/domain/resource_metadata.rb +41 -19
- data/lib/caruby/domain/resource_module.rb +42 -33
- data/lib/caruby/import/java.rb +8 -9
- data/lib/caruby/migration/migrator.rb +20 -7
- data/lib/caruby/migration/resource_module.rb +0 -2
- data/lib/caruby/resource.rb +132 -351
- data/lib/caruby/util/cache.rb +4 -1
- data/lib/caruby/util/class.rb +48 -1
- data/lib/caruby/util/collection.rb +54 -18
- data/lib/caruby/util/inflector.rb +7 -0
- data/lib/caruby/util/options.rb +35 -31
- data/lib/caruby/util/partial_order.rb +1 -1
- data/lib/caruby/util/properties.rb +2 -2
- data/lib/caruby/util/stopwatch.rb +16 -8
- data/lib/caruby/util/transitive_closure.rb +1 -1
- data/lib/caruby/util/visitor.rb +342 -328
- data/lib/caruby/version.rb +1 -1
- data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
- data/lib/caruby.rb +2 -0
- metadata +10 -9
- data/lib/caruby/database/saved_merger.rb +0 -131
- data/lib/caruby/domain/annotatable.rb +0 -25
- data/lib/caruby/domain/annotation.rb +0 -23
- data/lib/caruby/import/annotatable_class.rb +0 -28
- data/lib/caruby/import/annotation_class.rb +0 -27
- data/lib/caruby/import/annotation_module.rb +0 -67
- data/lib/caruby/migration/resource.rb +0 -8
data/History.txt
CHANGED
data/lib/caruby/cli/command.rb
CHANGED
@@ -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
|
-
# [
|
16
|
+
# [option, short, long, class, description]
|
17
17
|
# where:
|
18
|
-
# *
|
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
|
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
|
-
# [
|
27
|
+
# [arg, text]
|
28
28
|
# where:
|
29
|
-
# *
|
29
|
+
# * arg is the argument symbol, e.g. +:input+
|
30
30
|
# * text is the usage message text, e.g. 'input', '[input]' or 'input ...'
|
31
|
-
#
|
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", "
|
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", "
|
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
|
-
|
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
|
-
|
28
|
-
|
29
|
-
#
|
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
|
-
# @
|
20
|
-
#
|
21
|
-
|
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|
|
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
|
136
|
-
#
|
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
|
-
|
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}")
|
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
|
-
|
155
|
-
return if
|
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 =
|
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}
|
166
|
-
# more than one {ResourceDependency#owner_attributes}
|
167
|
-
# none of the owner attributes are loaded on demand,
|
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
|
-
|
174
|
-
|
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
|
-
|
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
|
239
|
-
# Subclasses can override
|
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
|
-
#
|
242
|
-
# to determine whether a save caCORE result
|
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
|
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
|
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
|
320
|
-
#
|
321
|
-
|
322
|
-
|
323
|
-
|
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
|
-
|
327
|
-
reader
|
328
|
-
|
329
|
-
|
330
|
-
#
|
331
|
-
|
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
|
343
|
-
|
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
|
346
|
-
|
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
|
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
|
-
|
359
|
-
# nothing to do if nothing fetched
|
360
|
-
return oldval if fetched.nil_or_empty?
|
429
|
+
merged = ldr.call(self, attribute)
|
361
430
|
|
362
|
-
#
|
363
|
-
|
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}
|
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
|