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