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
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'caruby/database/fetched_matcher'
|
2
|
+
|
3
|
+
module CaRuby
|
4
|
+
class Database
|
5
|
+
# Proc that matches saved result sources to targets.
|
6
|
+
class SavedMatcher < FetchedMatcher
|
7
|
+
# Initializes a new SavedMatcher.
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
# Returns a target => source match hash for the given targets and sources.
|
15
|
+
#
|
16
|
+
# @param (see FetchedMatcher#initialize)
|
17
|
+
# @return (see FetchedMatcher#initialize)
|
18
|
+
def match_fetched(sources, targets)
|
19
|
+
# match source => target based on the key
|
20
|
+
matches = super
|
21
|
+
# match residual targets, if any, on a relaxed criterion
|
22
|
+
if matches.size != targets.size and not sources.empty? then
|
23
|
+
match_fetched_residual(sources, targets, matches)
|
24
|
+
end
|
25
|
+
matches
|
26
|
+
end
|
27
|
+
|
28
|
+
# Adds to the given target => source matches hash for the unmatched targets and sources
|
29
|
+
# using {#match_minimal}.
|
30
|
+
#
|
31
|
+
# @param sources (see #match_fetched)
|
32
|
+
# @param targets (see #match_fetched)
|
33
|
+
# @param [{Resource => Resource}] the source => target matches so far
|
34
|
+
def match_fetched_residual(sources, targets, matches)
|
35
|
+
unmtchd_tgts = targets.to_set - matches.keys.delete_if { |tgt| tgt.identifier }
|
36
|
+
unmtchd_srcs = sources.to_set - matches.values
|
37
|
+
min_mtchs = match_minimal(unmtchd_srcs, unmtchd_tgts)
|
38
|
+
matches.merge!(min_mtchs)
|
39
|
+
end
|
40
|
+
|
41
|
+
#@param [<Resource>] sources the source objects to match
|
42
|
+
#@param [<Resource>] targets the potential match target objects
|
43
|
+
# @return (see #match_saved)
|
44
|
+
def match_minimal(sources, targets)
|
45
|
+
matches = {}
|
46
|
+
unmatched = Set === sources ? sources.to_set : sources.dup
|
47
|
+
targets.each do |tgt|
|
48
|
+
src = unmatched.detect { |src| tgt.minimal_match?(src) } || next
|
49
|
+
matches[src] = tgt
|
50
|
+
unmatched.delete(src)
|
51
|
+
end
|
52
|
+
matches
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -31,7 +31,7 @@ module CaRuby
|
|
31
31
|
# the searchable attribute => value hash
|
32
32
|
ref_hash, nonref_hash = hash.hash_partition { |attr, value| Resource === value }
|
33
33
|
# make the search template from the non-reference attributes
|
34
|
-
tmpl = obj.class.new(nonref_hash)
|
34
|
+
tmpl = obj.class.new.merge_attributes(nonref_hash)
|
35
35
|
# get references for the search template
|
36
36
|
unless ref_hash.empty? then
|
37
37
|
logger.debug { "Collecting search reference parameters for #{obj.qp} from attributes #{ref_hash.keys.to_series}..." }
|
@@ -42,7 +42,8 @@ module CaRuby
|
|
42
42
|
end
|
43
43
|
|
44
44
|
# Connects to the database, yields the DBI handle to the given block and disconnects.
|
45
|
-
#
|
45
|
+
#
|
46
|
+
# @return [Array] the execution result
|
46
47
|
def execute
|
47
48
|
logger.debug { "Connecting to database with user #{@username}, address #{@address}..." }
|
48
49
|
result = DBI.connect(@address, @username, @password, "driver"=>"com.mysql.jdbc.Driver") { |dbh| yield dbh }
|
@@ -54,17 +55,17 @@ module CaRuby
|
|
54
55
|
|
55
56
|
def default_driver_string(db_type)
|
56
57
|
case db_type.downcase
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
when 'mysql' then 'jdbc:mysql'
|
59
|
+
when 'oracle' then 'Oracle'
|
60
|
+
else raise CaRuby::ConfigurationError.new("Default database connection driver string could not be determined for database type #{db_type}")
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
63
64
|
def default_port(db_type)
|
64
65
|
case db_type.downcase
|
65
|
-
|
66
|
-
|
67
|
-
|
66
|
+
when 'mysql' then 3306
|
67
|
+
when 'oracle' then 1521
|
68
|
+
else raise CaRuby::ConfigurationError.new("Default database connection port could not be determined for database type #{db_type}")
|
68
69
|
end
|
69
70
|
end
|
70
71
|
|
@@ -3,17 +3,29 @@ require 'caruby/domain/reference_visitor'
|
|
3
3
|
module CaRuby
|
4
4
|
# StoreTemplateBuilder creates a template suitable for a create or update database operation.
|
5
5
|
class StoreTemplateBuilder
|
6
|
-
# Creates a new StoreTemplateBuilder for the given database.
|
6
|
+
# Creates a new StoreTemplateBuilder for the given database. The attributes to merge into
|
7
|
+
# the template are determined by the block given to this initializer, filtered as follows:
|
8
|
+
# * If the save operation is a create, then exclude the auto-generated attributes.
|
9
|
+
# * If the visited object has an identifier, then include only those attributes
|
10
|
+
# which {AttributeMetadata#cascade_update_to_create?} or have an identifier.
|
7
11
|
#
|
8
12
|
# @param [Database] database the target database
|
9
|
-
# @yield [ref] the required selector block which determines
|
13
|
+
# @yield [ref] the required selector block which determines which attributes are copied into the template
|
10
14
|
# @yieldparam [Resource] ref the domain object to copy
|
11
15
|
def initialize(database)
|
12
16
|
@database = database
|
13
|
-
unless block_given? then
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
+
unless block_given? then
|
18
|
+
raise ArgumentError.new("StoreTemplateBuilder is missing the required template copy attribute selector block")
|
19
|
+
end
|
20
|
+
|
21
|
+
# the mergeable attributes filter the given block with exclusions
|
22
|
+
@mergeable = Proc.new { |obj| mergeable_attributes(obj, yield(obj)) }
|
23
|
+
# the storable prerequisite reference visitor
|
24
|
+
@prereq_vstr = ReferenceVisitor.new(:prune_cycle) { |ref| savable_cascaded_attributes(ref) }
|
25
|
+
|
26
|
+
# the savable attributes filter the given block with exclusions
|
27
|
+
savable = Proc.new { |obj| savable_attributes(obj, yield(obj)) }
|
28
|
+
# the domain attributes to copy is determined by the constructor caller
|
17
29
|
# caTissue alert - must copy all of the non-domain attributes rather than just the identifier,
|
18
30
|
# since caTissue auto-generated Specimen update requires the parent collection status. This
|
19
31
|
# is the only known occurrence of a referenced object required non-identifier attribute.
|
@@ -24,10 +36,8 @@ module CaRuby
|
|
24
36
|
copy_proxied_save_references(src, copy)
|
25
37
|
copy
|
26
38
|
end
|
27
|
-
|
28
|
-
@copy_vstr = CopyVisitor.new(:mergeable =>
|
29
|
-
# the storable prerequisite reference visitor
|
30
|
-
@prereq_vstr = ReferenceVisitor.new(:prune_cycle) { |ref| savable_cascaded_attributes(ref) }
|
39
|
+
# the template copier
|
40
|
+
@copy_vstr = CopyVisitor.new(:mergeable => savable, :copier => copier) { |ref| savable_cascaded_attributes(ref) }
|
31
41
|
end
|
32
42
|
|
33
43
|
# Returns a new domain object which serves as the argument for obj create or update.
|
@@ -40,16 +50,14 @@ module CaRuby
|
|
40
50
|
# caCORE alert - +caCORE+ expects the store argument to be carefully prepared prior to
|
41
51
|
# the create or update. build_storable_template culls the target object with a template
|
42
52
|
# which includes only those references which are necessary for the store to succeed.
|
43
|
-
# This
|
44
|
-
# are included in the template but are not created before submission
|
45
|
-
#
|
46
|
-
# in the +caRuby+ application domain class definition using ResourceMetadata
|
47
|
-
#
|
48
|
-
# caCORE alert - +caCORE+ occasionally induces a stack overflow if a create argument
|
49
|
-
# contains a reference cycle. The template fixes this.
|
53
|
+
# This template builder ensures that mandatory independent references exist. Cascaded
|
54
|
+
# dependent references are included in the template but are not created before submission
|
55
|
+
# to +caCORE+. These reference attribute distinctions are implicit application rules which
|
56
|
+
# are explicated in the +caRuby+ application domain class definition using ResourceMetadata
|
57
|
+
# methods.
|
50
58
|
#
|
51
|
-
# caCORE alert - +caCORE+ create
|
52
|
-
# indirectly references a domain
|
59
|
+
# caCORE alert - +caCORE+ create issues an error if a create argument directly or
|
60
|
+
# indirectly references a non-cascaded domain object without an identifier, even if the
|
53
61
|
# reference is not relevant to the create. The template returned by this method elides
|
54
62
|
# all non-essential references.
|
55
63
|
#
|
@@ -67,7 +75,9 @@ module CaRuby
|
|
67
75
|
#
|
68
76
|
# @param [Resource] obj the domain object to save
|
69
77
|
# @return [Resource] the template to use as the caCORE argument
|
70
|
-
def build_template(obj)
|
78
|
+
def build_template(obj, autogenerated=false)
|
79
|
+
# set the database operation subject
|
80
|
+
@subject = obj
|
71
81
|
# prepare the object for a store operation
|
72
82
|
ensure_storable(obj)
|
73
83
|
# copy the cascade hierarchy
|
@@ -87,7 +97,8 @@ module CaRuby
|
|
87
97
|
# java.lang.IllegalArgumentException: id to load is required for loading
|
88
98
|
# The server log stack trace indicates a bizlogic line that offers a clue to the offending reference.
|
89
99
|
def ensure_storable(obj)
|
90
|
-
# Add defaults, which might introduce independent references.
|
100
|
+
# Add defaults, which might introduce independent references. Enable the lazy loader to fetch
|
101
|
+
# create references from the database where needed to build defaults.
|
91
102
|
obj.add_defaults
|
92
103
|
# create the prerequisite references if necessary
|
93
104
|
prereqs = collect_prerequisites(obj)
|
@@ -96,42 +107,83 @@ module CaRuby
|
|
96
107
|
@database.ensure_exists(prereqs)
|
97
108
|
logger.debug { "Prerequisite references for #{obj.qp} exist: #{prereqs.map { |ref| ref }.to_series}." }
|
98
109
|
end
|
99
|
-
# If obj is being created then add defaults again, since fetched independent references might introduce new defaults.
|
100
|
-
obj.add_defaults unless obj.identifier
|
101
110
|
# Verify that the object is complete
|
102
111
|
obj.validate
|
103
112
|
end
|
104
|
-
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
113
|
+
|
114
|
+
# Returns the attributes to visit in building the template for the given
|
115
|
+
# domain object. The visitable attributes consist of the following:
|
116
|
+
# * The {ResourceAttributes#unproxied_cascaded_attributes} filtered as follows:
|
117
|
+
# * If the database operation is a create, then exclude the cascaded attributes.
|
118
|
+
# * If the given object has an identifier, then exclude the attributes which
|
119
|
+
# have the the :no_cascade_update_to_create flag set.
|
120
|
+
# * The {ResourceAttributes#proxied_cascaded_attributes} are included if and
|
121
|
+
# only if every referenced object has an identifier, and therefore does not
|
122
|
+
# need to be proxied.
|
123
|
+
#
|
124
|
+
# caTissue alert - caTissue ignores some references, e.g. Participant CPR, and auto-generates
|
125
|
+
# the values instead. Therefore, the create template builder excludes these auto-generated
|
126
|
+
# attributes. After the create, the auto-generated references are merged into the created
|
127
|
+
# object graph and the references are updated if necessary.
|
109
128
|
#
|
110
129
|
# @param [Resource] obj the domain object copied to the update template
|
111
130
|
# @return [<Symbol>] the reference attributes to include in the update template
|
112
131
|
def savable_cascaded_attributes(obj)
|
113
|
-
#
|
114
|
-
unproxied = obj.class.unproxied_cascaded_attributes
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
132
|
+
# The starting set of candidate attributes is the unproxied cascaded references.
|
133
|
+
unproxied = savable_attributes(obj, obj.class.unproxied_cascaded_attributes)
|
134
|
+
# The proxied attributes to save.
|
135
|
+
proxied = savable_proxied_attributes(obj)
|
136
|
+
# The combined set of savable attributes
|
137
|
+
proxied.empty? ? unproxied : unproxied + proxied
|
138
|
+
end
|
139
|
+
|
140
|
+
# Composes the given attributes, if necessary, to exclude attributes as follows:
|
141
|
+
# * If the save operation is a create, then exclude the auto-generated attributes.
|
142
|
+
#
|
143
|
+
# @param [Resource] obj the visited domain object
|
144
|
+
# @param [ResourceAttributes::Filter] the savable attribute filter
|
145
|
+
# @return [ResourceAttributes::Filter] the composed attribute filter
|
146
|
+
def mergeable_attributes(obj, attributes)
|
147
|
+
# If this is an update, then there is no filter on the given attributes.
|
148
|
+
return attributes if @subject.identifier
|
149
|
+
# This is a create: ignore the optional auto-generated attributes.
|
150
|
+
mas = obj.mandatory_attributes.to_set
|
151
|
+
attributes.compose { |attr_md| mas.include?(attr_md.to_sym) or not attr_md.autogenerated? }
|
152
|
+
end
|
153
|
+
|
154
|
+
# Composes the given attributes, if necessary, to exclude attributes as follows:
|
155
|
+
# * If the save operation is a create, then exclude the auto-generated attributes.
|
156
|
+
# * If the visited object has an identifier, then include only those attributes
|
157
|
+
# which {AttributeMetadata#cascade_update_to_create?} or have an identifier.
|
158
|
+
#
|
159
|
+
# @param (see #mergeable_attributes)
|
160
|
+
# @return (see #mergeable_attributes)
|
161
|
+
def savable_attributes(obj, attributes)
|
162
|
+
mgbl = mergeable_attributes(obj, attributes)
|
163
|
+
return mgbl if obj.identifier.nil?
|
164
|
+
# The currently visited object is an update: include attributes which
|
165
|
+
# either cascade update to create or have saved references.
|
166
|
+
mgbl.compose do |attr_md|
|
167
|
+
attr_md.cascade_update_to_create? or Persistable.saved?(obj.send(attr_md.to_sym))
|
120
168
|
end
|
121
|
-
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns the proxied attributes to save. A proxied attribute is included only if the proxied
|
172
|
+
# dependents have an identifier, since those without an identifer are created separately via
|
173
|
+
# the proxy.
|
174
|
+
#
|
175
|
+
# @param [Resource] obj the visited domain object
|
176
|
+
# @return [<Attribute>] the proxied cascaded attributes with an unsaved reference
|
177
|
+
def savable_proxied_attributes(obj)
|
122
178
|
# Include a proxied reference only if the proxied dependents have an identifier,
|
123
179
|
# since those without an identifer are created separately via the proxy.
|
124
|
-
|
180
|
+
obj.class.proxied_cascaded_attributes.reject do |attr|
|
125
181
|
ref = obj.send(attr)
|
126
182
|
case ref
|
127
|
-
|
128
|
-
|
129
|
-
when Resource then
|
130
|
-
not ref.identifier
|
183
|
+
when Enumerable then ref.any? { |dep| not dep.identifier }
|
184
|
+
when Resource then not ref.identifier
|
131
185
|
end
|
132
186
|
end
|
133
|
-
|
134
|
-
proxied.empty? ? unproxied : unproxied + proxied
|
135
187
|
end
|
136
188
|
|
137
189
|
# Copies proxied references as needed.
|
@@ -148,17 +200,21 @@ module CaRuby
|
|
148
200
|
def copy_proxied_save_references(obj, template)
|
149
201
|
return unless obj.identifier
|
150
202
|
obj.class.proxied_cascaded_attributes.each do |attr|
|
203
|
+
# the proxy source
|
151
204
|
ref = obj.send(attr)
|
152
205
|
case ref
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
206
|
+
when Enumerable then
|
207
|
+
# recurse on the source collection
|
208
|
+
coll = template.send(attr)
|
209
|
+
ref.each do |dep|
|
210
|
+
copy = copy_proxied_save_reference(obj, attr, template, dep)
|
211
|
+
coll << copy if copy
|
212
|
+
end
|
213
|
+
when Resource then
|
214
|
+
# copy the source
|
215
|
+
copy = copy_proxied_save_reference(obj, attr, template, ref)
|
216
|
+
# set the attribute to the copy
|
217
|
+
template.set_attribute(attr, copy) if copy
|
162
218
|
end
|
163
219
|
end
|
164
220
|
end
|
@@ -174,27 +230,44 @@ module CaRuby
|
|
174
230
|
# map references to either the copied owner or a new copy of the reference
|
175
231
|
tvh = vh.transform { |value| Resource === value ? (value == obj ? template : value.copy) : value }
|
176
232
|
# the copy with the adjusted values
|
177
|
-
copy = proxied.class.new(tvh)
|
233
|
+
copy = proxied.class.new.merge_attributes(tvh)
|
178
234
|
logger.debug { "Created #{obj.qp} proxied #{attribute} save template copy #{proxied.pp_s}." }
|
179
235
|
copy
|
180
236
|
end
|
181
237
|
|
182
|
-
#
|
238
|
+
# @param [Resource] obj the domain object to store
|
239
|
+
# @return [<Resource>] the references which must be created in order to store the object
|
183
240
|
def collect_prerequisites(obj)
|
184
241
|
prereqs = Set.new
|
242
|
+
# visit the cascaded attributes
|
185
243
|
@prereq_vstr.visit(obj) do |stbl|
|
186
|
-
|
244
|
+
# Check each mergeable attribute for prerequisites. The mergeable attributes includes
|
245
|
+
# both cascaded and independent attributes. The selection block filters for independent
|
246
|
+
# domain objects which don't have an identifier.
|
247
|
+
@mergeable.call(stbl).each_pair do |attr, attr_md|
|
248
|
+
# Cascaded attributes are not prerequisite, since they are created when the owner is created.
|
249
|
+
# Note that each non-prerequisite cascaded reference is still visited in order to ensure
|
250
|
+
# that each independent object referenced by a cascaded reference is recognized as a
|
251
|
+
# candidate prerequisite.
|
252
|
+
next if attr_md.cascaded?
|
187
253
|
# add qualified prerequisite attribute references
|
188
|
-
stbl.send(attr).enumerate do |
|
189
|
-
#
|
190
|
-
|
191
|
-
unless prereq == obj or prereq.identifier or prereq.owner_ancestor?(obj) then
|
192
|
-
prereqs << prereq
|
193
|
-
end
|
254
|
+
stbl.send(attr).enumerate do |ref|
|
255
|
+
# Add the prerequisite if it satisfies the prerequisite? condition.
|
256
|
+
prereqs << ref if prerequisite?(ref, obj)
|
194
257
|
end
|
195
258
|
end
|
196
259
|
end
|
197
260
|
prereqs
|
198
261
|
end
|
262
|
+
|
263
|
+
# A referenced object is a target object save prerequisite if is the target object, was already created
|
264
|
+
# or is in an immediate or recursive dependent of the target object.
|
265
|
+
#
|
266
|
+
# @param [Resource] ref the reference to check
|
267
|
+
# @param [Resource] obj the object being stored
|
268
|
+
# @return [Boolean] whether the reference should exist before storing the object
|
269
|
+
def prerequisite?(ref, obj)
|
270
|
+
not (ref == obj or ref.identifier or ref.owner_ancestor?(obj))
|
271
|
+
end
|
199
272
|
end
|
200
273
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'caruby/util/collection'
|
2
2
|
require 'caruby/util/pretty_print'
|
3
3
|
require 'caruby/domain/reference_visitor'
|
4
|
-
require 'caruby/database/
|
4
|
+
require 'caruby/database/saved_matcher'
|
5
5
|
require 'caruby/database/store_template_builder'
|
6
6
|
|
7
7
|
module CaRuby
|
@@ -11,9 +11,17 @@ module CaRuby
|
|
11
11
|
# Adds store capability to this Database.
|
12
12
|
def initialize
|
13
13
|
super
|
14
|
-
@
|
14
|
+
@ftchd_vstr = ReferenceVisitor.new { |tgt| tgt.class.fetched_domain_attributes }
|
15
|
+
@cr_tmpl_bldr = StoreTemplateBuilder.new(self) { |ref| creatable_domain_attributes(ref) }
|
15
16
|
@upd_tmpl_bldr = StoreTemplateBuilder.new(self) { |ref| updatable_domain_attributes(ref) }
|
16
|
-
|
17
|
+
# the save result => argument reference matcher
|
18
|
+
svd_mtchr = SavedMatcher.new
|
19
|
+
# the save (result, argument) synchronization visitor
|
20
|
+
@svd_sync_vstr = MatchVisitor.new(:matcher => svd_mtchr) { |ref| ref.class.dependent_attributes }
|
21
|
+
# the attributes to merge from the save result
|
22
|
+
mgbl = Proc.new { |ref| ref.class.domain_attributes }
|
23
|
+
# the save result => argument merge visitor
|
24
|
+
@svd_mrg_vstr = MergeVisitor.new(:matcher => svd_mtchr, :mergeable => mgbl) { |ref| ref.class.dependent_attributes }
|
17
25
|
end
|
18
26
|
|
19
27
|
# Creates the specified domain object obj and returns obj. The pre-condition for this method is as
|
@@ -106,8 +114,6 @@ module CaRuby
|
|
106
114
|
# @raise [DatabaseError] if the database operation fails
|
107
115
|
def save(obj)
|
108
116
|
logger.debug { "Storing #{obj}..." }
|
109
|
-
# add defaults now, since a default key value could be used in the existence check
|
110
|
-
obj.add_defaults
|
111
117
|
# if obj exists then update it, otherwise create it
|
112
118
|
exists?(obj) ? update(obj) : create(obj)
|
113
119
|
end
|
@@ -134,7 +140,7 @@ module CaRuby
|
|
134
140
|
raise ArgumentError.new("Database ensure_exists is missing a domain object argument") if obj.nil_or_empty?
|
135
141
|
obj.enumerate { |ref| find(ref, :create) unless ref.identifier }
|
136
142
|
end
|
137
|
-
|
143
|
+
|
138
144
|
# Returns whether there is already the given obj operation in progress that is not in the scope of
|
139
145
|
# an operation performed on a dependent obj owner, i.e. a second obj save operation of the same type
|
140
146
|
# is only allowed if the obj operation was delegated to an owner save which in turn saves the dependent
|
@@ -144,8 +150,8 @@ module CaRuby
|
|
144
150
|
# @param [Symbol] operation the +:create+ or +:update+ save operation
|
145
151
|
# @return [Boolean] whether the save operation is redundant
|
146
152
|
def recursive_save?(obj, operation)
|
147
|
-
@operations.
|
148
|
-
|
153
|
+
@operations.any? { |op| op.type == operation and op.subject == obj } and
|
154
|
+
not obj.owner_ancestor?(@operations.last.subject)
|
149
155
|
end
|
150
156
|
|
151
157
|
private
|
@@ -167,8 +173,8 @@ module CaRuby
|
|
167
173
|
# A dependent of an uncreated owner can be created by creating the owner.
|
168
174
|
# Otherwise, create obj from a template.
|
169
175
|
create_as_dependent(obj) or
|
170
|
-
|
171
|
-
|
176
|
+
create_from_template(obj) or
|
177
|
+
raise DatabaseError.new("#{obj.class.qp} is not creatable in context #{print_operations}")
|
172
178
|
ensure
|
173
179
|
# since obj now has an id, removed from transients set
|
174
180
|
@transients.delete(obj)
|
@@ -188,17 +194,16 @@ module CaRuby
|
|
188
194
|
# @return [Resource] dep
|
189
195
|
def create_as_dependent(dep)
|
190
196
|
# bail if not dependent or owner is not set
|
191
|
-
|
192
|
-
unless
|
193
|
-
logger.debug { "Adding #{
|
194
|
-
dep.
|
195
|
-
|
196
|
-
ensure_exists(owner)
|
197
|
+
ownr = dep.owner || return
|
198
|
+
unless ownr.identifier then
|
199
|
+
logger.debug { "Adding #{ownr.qp} dependent #{dep.qp} defaults..." }
|
200
|
+
logger.debug { "Ensuring that dependent #{dep.qp} owner #{ownr.qp} exists..." }
|
201
|
+
ensure_exists(ownr)
|
197
202
|
end
|
198
203
|
|
199
204
|
# If the dependent was created as a side-effect of creating the owner, then we are done.
|
200
205
|
if dep.identifier then
|
201
|
-
logger.debug { "Created dependent #{dep.qp} by saving owner #{
|
206
|
+
logger.debug { "Created dependent #{dep.qp} by saving owner #{ownr.qp}." }
|
202
207
|
return dep
|
203
208
|
end
|
204
209
|
|
@@ -274,10 +279,9 @@ module CaRuby
|
|
274
279
|
# The create template. Independent saved references are created as necessary.
|
275
280
|
tmpl = build_create_template(obj)
|
276
281
|
save_with_template(obj, tmpl) { |svc| svc.create(tmpl) }
|
277
|
-
|
278
282
|
# If obj is a top-level create, then ensure that remaining references exist.
|
279
283
|
if @operations.first.subject == obj then
|
280
|
-
refs = obj.
|
284
|
+
refs = obj.references.reject { |ref| ref.identifier }
|
281
285
|
logger.debug { "Ensuring that created #{obj.qp} references exist: #{refs.qp}..." } unless refs.empty?
|
282
286
|
refs.each { |ref| ensure_exists(ref) }
|
283
287
|
end
|
@@ -285,8 +289,10 @@ module CaRuby
|
|
285
289
|
obj
|
286
290
|
end
|
287
291
|
|
292
|
+
# @param (see #create)
|
293
|
+
# @return (see #build_save_template)
|
288
294
|
def build_create_template(obj)
|
289
|
-
@cr_tmpl_bldr
|
295
|
+
build_save_template(obj, @cr_tmpl_bldr)
|
290
296
|
end
|
291
297
|
#
|
292
298
|
# caCORE alert - application create logic might ignore a non-domain attribute value,
|
@@ -313,19 +319,64 @@ module CaRuby
|
|
313
319
|
end
|
314
320
|
end
|
315
321
|
|
316
|
-
# Returns the {MetadataAttributes#
|
317
|
-
#
|
318
|
-
#
|
322
|
+
# Returns the {MetadataAttributes#creatable_domain_attributes} which are not contravened by a
|
323
|
+
# one-to-one independent pending create.
|
324
|
+
#
|
325
|
+
# @param (see #create)
|
326
|
+
# @return [<Symbol>] the attributes to include in the create template
|
327
|
+
def creatable_domain_attributes(obj)
|
328
|
+
# filter the creatable attributes
|
329
|
+
obj.class.creatable_domain_attributes.compose do |attr_md|
|
330
|
+
if exclude_pending_create_attribute?(obj, attr_md) then
|
331
|
+
# Avoid printing duplicate log message.
|
332
|
+
if obj != @cr_dom_attr_log_obj then
|
333
|
+
logger.debug { "Excluded #{obj.qp} #{attr_md} in the create template since it references a 1:1 bidirectional independent pending create." }
|
334
|
+
@cr_dom_attr_log_obj = obj
|
335
|
+
end
|
336
|
+
false
|
337
|
+
else
|
338
|
+
true
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# Returns whether the given creatable domain attribute with value obj is a
|
344
|
+
# {AttributeMetadata#one_to_one_bidirectional_independent?}
|
345
|
+
# unsaved optional attribute without an obj {#pending_create?} save context.
|
346
|
+
#
|
347
|
+
# @param obj (see #create)
|
348
|
+
# @param [AttributeMetadata] attr_md candidate attribute metadata
|
349
|
+
# @return [Boolean] whether the attribute should not be included in the create template
|
350
|
+
def exclude_pending_create_attribute?(obj, attr_md)
|
351
|
+
attr_md.one_to_one_bidirectional_independent? and
|
352
|
+
obj.identifier.nil? and
|
353
|
+
not obj.mandatory_attributes.include?(attr_md.to_sym) and
|
354
|
+
ref = obj.send(attr_md.to_sym) and
|
355
|
+
ref.identifier.nil? and
|
356
|
+
pending_create?(ref)
|
357
|
+
end
|
358
|
+
|
359
|
+
# @param [Resource] obj the object to check
|
360
|
+
# @return [Boolean] whether the penultimate create operation is on the object
|
361
|
+
def pending_create?(obj)
|
362
|
+
op = penultimate_create_operation
|
363
|
+
op and op.subject == obj
|
364
|
+
end
|
365
|
+
|
366
|
+
# @return [Operation] the create operation which scopes the innermost create operation
|
367
|
+
def penultimate_create_operation
|
368
|
+
@operations.reverse_each { |op| return op if op.type == :create and op != @operations.last }
|
369
|
+
nil
|
370
|
+
end
|
371
|
+
|
372
|
+
# Returns the {MetadataAttributes#updatable_domain_attributes}.
|
319
373
|
#
|
320
374
|
# @param (see #update)
|
321
375
|
# @return the attributes to include in the update template
|
322
376
|
def updatable_domain_attributes(obj)
|
323
|
-
obj.class.updatable_domain_attributes
|
324
|
-
obj.class.attribute_metadata(attr).cascade_update_to_create? or
|
325
|
-
obj.send(attr).to_enum.all? { |ref| ref.identifier }
|
326
|
-
end
|
377
|
+
obj.class.updatable_domain_attributes
|
327
378
|
end
|
328
|
-
|
379
|
+
|
329
380
|
# @param (see #update)
|
330
381
|
def update_object(obj)
|
331
382
|
# database identifier is required for update
|
@@ -389,8 +440,16 @@ module CaRuby
|
|
389
440
|
end
|
390
441
|
|
391
442
|
# @param (see #update)
|
443
|
+
# @return (see #build_save_template)
|
392
444
|
def build_update_template(obj)
|
393
|
-
@upd_tmpl_bldr
|
445
|
+
build_save_template(obj, @upd_tmpl_bldr)
|
446
|
+
end
|
447
|
+
|
448
|
+
# @param obj (see #save)
|
449
|
+
# @param [StoreTemplateBuilder] builder the builder to use
|
450
|
+
# @return [Resource] the template to use as the save argument
|
451
|
+
def build_save_template(obj, builder)
|
452
|
+
builder.build_template(obj)
|
394
453
|
end
|
395
454
|
|
396
455
|
# @param (see #delete)
|
@@ -421,49 +480,190 @@ module CaRuby
|
|
421
480
|
sync_saved(obj, result)
|
422
481
|
end
|
423
482
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
483
|
+
# Synchronizes the content of the given saved domain object and the save result source as follows:
|
484
|
+
# 1. The save result source is first synchronized with the database content as necessary.
|
485
|
+
# 2. Then the source is merged into the target.
|
486
|
+
# 3. If the target must be resaved based on the call to {#update_saved?}, then the source
|
487
|
+
# result is resaved.
|
488
|
+
# 4. Each target dependent which differs from the corresponding source dependent is saved.
|
489
|
+
#
|
490
|
+
# @param [Resource] target the saved domain object
|
491
|
+
# @param [Resource] source the caCORE save result
|
492
|
+
def sync_saved(target, source)
|
493
|
+
# clear the toxic source attributes
|
494
|
+
detoxify(source)
|
495
|
+
# sync the save result source with the database
|
496
|
+
sync_saved_result_with_database(source, target)
|
497
|
+
# merge the source into the target
|
498
|
+
merge_saved(target, source)
|
429
499
|
|
430
500
|
# If saved must be updated, then update recursively.
|
431
501
|
# Otherwise, save dependents as needed.
|
432
|
-
if update_saved?(
|
433
|
-
logger.debug { "Updating saved #{
|
434
|
-
# call
|
435
|
-
perform(:update,
|
502
|
+
if update_saved?(target, source) then
|
503
|
+
logger.debug { "Updating saved #{target} to store unsaved attributes..." }
|
504
|
+
# call update_object(saved) rather than update(saved) to bypass the redundant update check
|
505
|
+
perform(:update, target) { update_object(target) }
|
436
506
|
else
|
437
507
|
# recursively save the dependents
|
438
|
-
|
508
|
+
save_changed_dependents(target)
|
439
509
|
end
|
440
510
|
end
|
441
|
-
|
511
|
+
|
512
|
+
# Synchronizes the given saved target result source with the database content.
|
513
|
+
# The source is synchronized by {#sync_save_result}.
|
514
|
+
#
|
515
|
+
# @param (see #sync_saved)
|
516
|
+
def sync_saved_result_with_database(source, target)
|
517
|
+
@svd_sync_vstr.visit(source, target) { |src, tgt| sync_save_result(src, tgt) }
|
518
|
+
end
|
519
|
+
|
520
|
+
# Merges the database content into the given saved domain object.
|
521
|
+
# Dependents are merged recursively.
|
522
|
+
#
|
523
|
+
# caTissue alert - the auto-generated references are not necessarily valid, e.g. the auto-generated
|
524
|
+
# SpecimenRequirement characteristics tissue site is nil rather than the default 'Not Specified'.
|
525
|
+
# This results in an obscure downstream error when creating an CPR which auto-generates a SCG
|
526
|
+
# which auto-generates a Specimen which copies the invalid characteristics. The work-around for
|
527
|
+
# this bug is to add defaults to auto-generated references. Then, if the content differs from
|
528
|
+
# the database, the difference induces an update of the reference.
|
529
|
+
#
|
530
|
+
# @param (see #sync_saved)
|
531
|
+
# @return [Resource] the merged target object
|
532
|
+
def merge_saved(target, source)
|
533
|
+
logger.debug { "Merging saved result #{source} into saved #{target.qp}..." }
|
534
|
+
# Update each saved reference snapshot to reflect the database state and add a lazy loader if necessary.
|
535
|
+
@svd_mrg_vstr.visit(source, target) do |src, tgt|
|
536
|
+
# capture the id
|
537
|
+
prev_id = tgt.identifier
|
538
|
+
persistify_object(tgt, src)
|
539
|
+
# if tgt is an auto-generated reference, then add defaults
|
540
|
+
if target != tgt and prev_id.nil? then tgt.add_defaults end
|
541
|
+
end
|
542
|
+
logger.debug { "Merged saved result #{source} into saved #{target.qp}." }
|
543
|
+
end
|
544
|
+
|
545
|
+
# Synchronizes the given save result source object to reflect the database content, as follows:
|
546
|
+
# * If the save result has autogenerated non-domain attributes, then the source is refetched.
|
547
|
+
# * Each of the dependent {#synchronization_attributes} is fetched.
|
548
|
+
# * Inverses are set consistently within the save result object graph.
|
549
|
+
#
|
550
|
+
# @param (see #sync_saved)
|
551
|
+
def sync_save_result(source, target)
|
552
|
+
logger.debug { "Synchronizing #{target} save result #{source} with the database..." }
|
553
|
+
# If the target was created, then refetch and merge the source if necessary to reflect auto-generated
|
554
|
+
# non-domain attribute values.
|
555
|
+
if target.identifier.nil? then sync_created_result_object(source) end
|
556
|
+
# If there are auto-generated attributes, then merge them into the save result.
|
557
|
+
sync_save_result_references(source, target)
|
558
|
+
# Set inverses consistently in the source object graph
|
559
|
+
set_inverses(source)
|
560
|
+
logger.debug { "Synchronized #{target} save result #{source} with the database." }
|
561
|
+
end
|
562
|
+
|
563
|
+
# Refetches the given create result source if there are any {ResourceAttributes#autogenerated_nondomain_attributes}
|
564
|
+
# which must be fetched to reflect the database state.
|
565
|
+
#
|
566
|
+
# @param source (see #sync_saved)
|
567
|
+
def sync_created_result_object(source)
|
568
|
+
attrs = source.class.autogenerated_nondomain_attributes
|
569
|
+
return if attrs.empty?
|
570
|
+
logger.debug { "Refetch #{source} to reflect auto-generated database content for attributes #{attrs.to_series}..." }
|
571
|
+
find(source)
|
572
|
+
end
|
573
|
+
|
574
|
+
# Fetches the {#synchronization_attributes} into the given target save result source.
|
575
|
+
#
|
576
|
+
# @param (see #sync_saved)
|
577
|
+
def sync_save_result_references(source, target)
|
578
|
+
attrs = synchronization_attributes(source, target)
|
579
|
+
return if attrs.empty?
|
580
|
+
logger.debug { "Fetching the saved #{target.qp} attributes #{attrs.to_series} into save result #{source.qp}..." }
|
581
|
+
attrs.each { |attr| sync_save_result_attribute(source, attr) }
|
582
|
+
logger.debug { "Fetched the saved #{target.qp} attributes #{attrs.to_series} into the save result #{source.qp}." }
|
583
|
+
end
|
584
|
+
|
585
|
+
# @see #sync_save_result_references
|
586
|
+
def sync_save_result_attribute(source, attribute)
|
587
|
+
# fetch the value
|
588
|
+
fetched = fetch_association(source, attribute)
|
589
|
+
# set the attribute
|
590
|
+
source.set_attribute(attribute, fetched)
|
591
|
+
end
|
592
|
+
|
593
|
+
# Returns the saved target attributes which must be fetched to reflect the database content, consisting
|
594
|
+
# of the following:
|
595
|
+
# * {Persistable#saved_fetch_attributes}
|
596
|
+
# * {ResourceAttributes#domain_attributes} which include a source reference without an identifier
|
597
|
+
#
|
598
|
+
# @param (see #sync_saved)
|
599
|
+
# @return [<Symbol>] the attributes which must be fetched
|
600
|
+
def synchronization_attributes(source, target)
|
601
|
+
# the target save operation
|
602
|
+
op = @operations.last
|
603
|
+
# the attributes to fetch
|
604
|
+
attrs = target.saved_fetch_attributes(op).to_set
|
605
|
+
# the pending create, if any
|
606
|
+
pndg_op = penultimate_create_operation
|
607
|
+
pndg = pndg_op.subject if pndg_op
|
608
|
+
# add in the domain attributes whose identifier was not set in the result
|
609
|
+
source.class.saved_domain_attributes.select do |attr|
|
610
|
+
srcval = source.send(attr)
|
611
|
+
tgtval = target.send(attr)
|
612
|
+
if Persistable.unsaved?(srcval) then
|
613
|
+
logger.debug { "Fetching save result #{source.qp} #{attr} since a referenced object identifier was not set in the result..." }
|
614
|
+
attrs << attr
|
615
|
+
elsif srcval.nil_or_empty? and Persistable.unsaved?(tgtval) and tgtval != pndg then
|
616
|
+
logger.debug { "Fetching save result #{source.qp} #{attr} since the target #{target.qp} value #{tgtval.qp} is missing an identifier..." }
|
617
|
+
attrs << attr
|
618
|
+
end
|
619
|
+
end
|
620
|
+
attrs
|
621
|
+
end
|
622
|
+
|
442
623
|
# Saves the given domain object dependents.
|
443
624
|
#
|
444
625
|
# @param [Resource] obj the owner domain object
|
445
|
-
def
|
446
|
-
|
626
|
+
def save_changed_dependents(obj)
|
627
|
+
# JRuby alert - copy the Resource dependents call result to an array, since iteration based on
|
628
|
+
# Forwardable enum_for breaks here with an obscure Java ConcurrentModificationException
|
629
|
+
# in the CaTissue SCG save test case. TODO - isolate and fix at source.
|
630
|
+
obj.class.dependent_attributes.each do |attr|
|
631
|
+
deps = obj.send(attr).to_enum
|
632
|
+
logger.debug { "Saving the #{obj} #{attr} dependents #{deps.qp} which have changed..." } unless deps.empty?
|
633
|
+
deps.each { |dep| save_dependent_if_changed(obj, attr, dep) }
|
634
|
+
end
|
447
635
|
end
|
448
636
|
|
449
637
|
# Saves the given dependent domain object if necessary.
|
450
638
|
# Recursively saves the obj dependents as necessary.
|
451
639
|
#
|
452
640
|
# @param [Resource] obj the dependent domain object to save
|
453
|
-
def
|
454
|
-
if
|
455
|
-
logger.debug { "Creating dependent #{
|
456
|
-
return create(
|
641
|
+
def save_dependent_if_changed(owner, attribute, dependent)
|
642
|
+
if dependent.identifier.nil? then
|
643
|
+
logger.debug { "Creating #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
|
644
|
+
return create(dependent)
|
457
645
|
end
|
458
|
-
changes =
|
459
|
-
logger.debug { "#{owner.qp} dependent #{
|
460
|
-
if changes.any? { |attr| not
|
461
|
-
|
462
|
-
|
646
|
+
changes = dependent.changed_attributes
|
647
|
+
logger.debug { "#{owner.qp} #{attribute} dependent #{dependent.qp} changed for attributes #{changes.to_series}." } unless changes.empty?
|
648
|
+
if changes.any? { |attr| not dependent.class.attribute_metadata(attr).dependent? } then
|
649
|
+
# the owner save operation
|
650
|
+
op = operations.last
|
651
|
+
# The dependent is auto-generated if the owner was created or auto-generated and
|
652
|
+
# the dependent attribute is auto-generated.
|
653
|
+
ag = (op.type == :create or op.autogenerated?) && owner.class.attribute_metadata(attribute).autogenerated?
|
654
|
+
logger.debug { "Updating the changed #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
|
655
|
+
perform(:update, dependent, :autogenerated => ag) { update_object(dependent) }
|
463
656
|
else
|
464
|
-
|
657
|
+
save_changed_dependents(dependent)
|
465
658
|
end
|
466
659
|
end
|
660
|
+
|
661
|
+
# Creates the given dependent domain object.
|
662
|
+
#
|
663
|
+
# @param (see #save_dependent)
|
664
|
+
def create_dependent(owner, dep)
|
665
|
+
create(dep)
|
666
|
+
end
|
467
667
|
end
|
468
668
|
end
|
469
669
|
end
|