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