caruby-core 1.5.5 → 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +9 -0
- data/History.md +5 -1
- data/lib/caruby.rb +3 -5
- data/lib/caruby/caruby-src.tar.gz +0 -0
- data/lib/caruby/database.rb +53 -69
- data/lib/caruby/database/application_service.rb +25 -0
- data/lib/caruby/database/cache.rb +60 -0
- data/lib/caruby/database/fetched_matcher.rb +52 -38
- data/lib/caruby/database/lazy_loader.rb +4 -4
- data/lib/caruby/database/operation.rb +34 -0
- data/lib/caruby/database/persistable.rb +171 -86
- data/lib/caruby/database/persistence_service.rb +32 -34
- data/lib/caruby/database/persistifier.rb +100 -43
- data/lib/caruby/database/reader.rb +107 -85
- data/lib/caruby/database/reader_template_builder.rb +60 -0
- data/lib/caruby/database/saved_matcher.rb +3 -3
- data/lib/caruby/database/sql_executor.rb +88 -17
- data/lib/caruby/database/writer.rb +213 -177
- data/lib/caruby/database/writer_template_builder.rb +334 -0
- data/lib/caruby/{util → helpers}/controlled_value.rb +0 -0
- data/lib/caruby/{util → helpers}/coordinate.rb +4 -4
- data/lib/caruby/{util → helpers}/person.rb +3 -3
- data/lib/caruby/{util → helpers}/properties.rb +7 -9
- data/lib/caruby/{util → helpers}/roman.rb +2 -2
- data/lib/caruby/{util → helpers}/version.rb +1 -1
- data/lib/caruby/json/deserializer.rb +2 -2
- data/lib/caruby/json/serializer.rb +49 -7
- data/lib/caruby/metadata.rb +30 -0
- data/lib/caruby/metadata/java_property.rb +21 -0
- data/lib/caruby/metadata/propertied.rb +191 -0
- data/lib/caruby/metadata/property.rb +22 -0
- data/lib/caruby/metadata/property_characteristics.rb +201 -0
- data/lib/caruby/migration/migratable.rb +11 -182
- data/lib/caruby/rdbi/driver/jdbc.rb +446 -0
- data/lib/caruby/resource.rb +20 -823
- data/lib/caruby/version.rb +1 -1
- data/test/lib/caruby/database/cache_test.rb +54 -0
- data/test/lib/caruby/{util → helpers}/controlled_value_test.rb +3 -5
- data/test/lib/caruby/{util → helpers}/person_test.rb +4 -6
- data/test/lib/caruby/helpers/properties_test.rb +34 -0
- data/test/lib/caruby/{util → helpers}/roman_test.rb +2 -3
- data/test/lib/caruby/{util → helpers}/version_test.rb +2 -3
- data/test/lib/helper.rb +7 -0
- metadata +161 -214
- data/lib/caruby/cli/application.rb +0 -36
- data/lib/caruby/cli/command.rb +0 -202
- data/lib/caruby/csv/csv_mapper.rb +0 -159
- data/lib/caruby/csv/csvio.rb +0 -203
- data/lib/caruby/database/search_template_builder.rb +0 -56
- data/lib/caruby/database/store_template_builder.rb +0 -278
- data/lib/caruby/domain.rb +0 -193
- data/lib/caruby/domain/attribute.rb +0 -584
- data/lib/caruby/domain/attributes.rb +0 -628
- data/lib/caruby/domain/dependency.rb +0 -225
- data/lib/caruby/domain/id_alias.rb +0 -22
- data/lib/caruby/domain/importer.rb +0 -183
- data/lib/caruby/domain/introspection.rb +0 -176
- data/lib/caruby/domain/inverse.rb +0 -172
- data/lib/caruby/domain/inversible.rb +0 -90
- data/lib/caruby/domain/java_attribute.rb +0 -173
- data/lib/caruby/domain/merge.rb +0 -185
- data/lib/caruby/domain/metadata.rb +0 -142
- data/lib/caruby/domain/mixin.rb +0 -35
- data/lib/caruby/domain/properties.rb +0 -95
- data/lib/caruby/domain/reference_visitor.rb +0 -428
- data/lib/caruby/domain/uniquify.rb +0 -50
- data/lib/caruby/import/java.rb +0 -387
- data/lib/caruby/migration/migrator.rb +0 -918
- data/lib/caruby/migration/resource_module.rb +0 -9
- data/lib/caruby/migration/uniquify.rb +0 -17
- data/lib/caruby/util/attribute_path.rb +0 -44
- data/lib/caruby/util/cache.rb +0 -56
- data/lib/caruby/util/class.rb +0 -149
- data/lib/caruby/util/collection.rb +0 -1152
- data/lib/caruby/util/domain_extent.rb +0 -46
- data/lib/caruby/util/file_separator.rb +0 -65
- data/lib/caruby/util/inflector.rb +0 -27
- data/lib/caruby/util/log.rb +0 -95
- data/lib/caruby/util/math.rb +0 -12
- data/lib/caruby/util/merge.rb +0 -59
- data/lib/caruby/util/module.rb +0 -18
- data/lib/caruby/util/options.rb +0 -97
- data/lib/caruby/util/partial_order.rb +0 -35
- data/lib/caruby/util/pretty_print.rb +0 -204
- data/lib/caruby/util/stopwatch.rb +0 -74
- data/lib/caruby/util/topological_sync_enumerator.rb +0 -62
- data/lib/caruby/util/transitive_closure.rb +0 -55
- data/lib/caruby/util/tree.rb +0 -48
- data/lib/caruby/util/trie.rb +0 -37
- data/lib/caruby/util/uniquifier.rb +0 -30
- data/lib/caruby/util/validation.rb +0 -20
- data/lib/caruby/util/visitor.rb +0 -365
- data/lib/caruby/util/weak_hash.rb +0 -36
- data/test/lib/caruby/csv/csv_mapper_test.rb +0 -40
- data/test/lib/caruby/csv/csvio_test.rb +0 -69
- data/test/lib/caruby/database/persistable_test.rb +0 -92
- data/test/lib/caruby/domain/domain_test.rb +0 -112
- data/test/lib/caruby/domain/inversible_test.rb +0 -99
- data/test/lib/caruby/domain/reference_visitor_test.rb +0 -130
- data/test/lib/caruby/import/java_test.rb +0 -80
- data/test/lib/caruby/import/mixed_case_test.rb +0 -14
- data/test/lib/caruby/migration/test_case.rb +0 -102
- data/test/lib/caruby/test_case.rb +0 -230
- data/test/lib/caruby/util/cache_test.rb +0 -23
- data/test/lib/caruby/util/class_test.rb +0 -61
- data/test/lib/caruby/util/collection_test.rb +0 -398
- data/test/lib/caruby/util/command_test.rb +0 -55
- data/test/lib/caruby/util/domain_extent_test.rb +0 -60
- data/test/lib/caruby/util/file_separator_test.rb +0 -30
- data/test/lib/caruby/util/inflector_test.rb +0 -12
- data/test/lib/caruby/util/lazy_hash_test.rb +0 -34
- data/test/lib/caruby/util/merge_test.rb +0 -83
- data/test/lib/caruby/util/module_test.rb +0 -25
- data/test/lib/caruby/util/options_test.rb +0 -59
- data/test/lib/caruby/util/partial_order_test.rb +0 -42
- data/test/lib/caruby/util/pretty_print_test.rb +0 -85
- data/test/lib/caruby/util/properties_test.rb +0 -50
- data/test/lib/caruby/util/stopwatch_test.rb +0 -18
- data/test/lib/caruby/util/topological_sync_enumerator_test.rb +0 -69
- data/test/lib/caruby/util/transitive_closure_test.rb +0 -67
- data/test/lib/caruby/util/tree_test.rb +0 -23
- data/test/lib/caruby/util/trie_test.rb +0 -14
- data/test/lib/caruby/util/visitor_test.rb +0 -278
- data/test/lib/caruby/util/weak_hash_test.rb +0 -45
- data/test/lib/examples/clinical_trials/migration/migration_test.rb +0 -58
- data/test/lib/examples/clinical_trials/migration/test_case.rb +0 -38
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'jinx/resource'
|
2
|
+
require 'jinx/helpers/pretty_print'
|
3
|
+
|
4
|
+
module CaRuby
|
5
|
+
class Database
|
6
|
+
module Reader
|
7
|
+
# TemplateBuilder builds a template suitable for a caCORE saarch database operation.
|
8
|
+
class TemplateBuilder
|
9
|
+
# Returns a template for matching the domain object obj and the optional hash values.
|
10
|
+
# The default hash attributes are the {Propertied#searchable_attributes}.
|
11
|
+
# The template includes only the non-domain attributes of the hash references.
|
12
|
+
#
|
13
|
+
# @quirk caCORE Because of caCORE API limitations, the obj searchable attribute
|
14
|
+
# values are limited to the following:
|
15
|
+
# * non-domain attribute values
|
16
|
+
# * non-collection domain attribute references which contain a key
|
17
|
+
#
|
18
|
+
# @quirk caCORE the caCORE query builder breaks on reference cycles and is easily confused
|
19
|
+
# by extraneous references, so it is necessary to search with a template instead that contains
|
20
|
+
# only references essential to the search. Each reference is confirmed to exist and the
|
21
|
+
# reference content in the template consists entirely of the fetched identifier attribute.
|
22
|
+
def build_template(obj, hash=nil)
|
23
|
+
# split the attributes into reference and non-reference attributes.
|
24
|
+
# the new search template object is built from the non-reference attributes.
|
25
|
+
# the reference attributes values are copied and added.
|
26
|
+
logger.debug { "Building search template for #{obj.qp}..." }
|
27
|
+
hash ||= obj.value_hash(obj.class.searchable_attributes)
|
28
|
+
# the searchable attribute => value hash
|
29
|
+
rh, nrh = hash.split { |pa, value| Jinx::Resource === value }
|
30
|
+
# make the search template from the non-reference attributes
|
31
|
+
tmpl = obj.class.new.merge_attributes(nrh)
|
32
|
+
# get references for the search template
|
33
|
+
unless rh.empty? then
|
34
|
+
logger.debug { "Collecting search reference parameters for #{obj.qp} from attributes #{rh.keys.to_series}..." }
|
35
|
+
end
|
36
|
+
rh.each { |pa, ref| add_search_template_reference(tmpl, ref, pa) }
|
37
|
+
tmpl
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Sets the template attribute to a new search reference object created from the given
|
43
|
+
# source domain object. The reference contains only the source identifier, if it exists,
|
44
|
+
# or the source non-domain attributes otherwise.
|
45
|
+
#
|
46
|
+
# @return [Jinx::Resource] the search reference
|
47
|
+
def add_search_template_reference(template, source, attribute)
|
48
|
+
ref = source.identifier ? source.copy(:identifier) : source.copy
|
49
|
+
# Disable inverse integrity, since the template attribute assignment might have added a reference
|
50
|
+
# from ref to template, which introduces a template => ref => template cycle that causes a caCORE
|
51
|
+
# search infinite loop. Use the Java property writer instead.
|
52
|
+
wtr = template.class.property(attribute).property_writer
|
53
|
+
template.send(wtr, ref)
|
54
|
+
logger.debug { "Search reference parameter #{attribute} for #{template.qp} set to #{ref} copied from #{source.qp}" }
|
55
|
+
ref
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -26,7 +26,7 @@ module CaRuby
|
|
26
26
|
#
|
27
27
|
# @param sources (see #match_fetched)
|
28
28
|
# @param targets (see #match_fetched)
|
29
|
-
# @param [{Resource => Resource}] the source => target matches so far
|
29
|
+
# @param [{Jinx::Resource => Jinx::Resource}] the source => target matches so far
|
30
30
|
def match_fetched_residual(sources, targets, matches)
|
31
31
|
unmtchd_tgts = targets.to_set - matches.keys.delete_if { |tgt| tgt.identifier }
|
32
32
|
unmtchd_srcs = sources.to_set - matches.values
|
@@ -34,8 +34,8 @@ module CaRuby
|
|
34
34
|
matches.merge!(min_mtchs)
|
35
35
|
end
|
36
36
|
|
37
|
-
#@param [<Resource>] sources the source objects to match
|
38
|
-
#@param [<Resource>] targets the potential match target objects
|
37
|
+
#@param [<Jinx::Resource>] sources the source objects to match
|
38
|
+
#@param [<Jinx::Resource>] targets the potential match target objects
|
39
39
|
# @return (see #match_saved)
|
40
40
|
def match_minimal(sources, targets)
|
41
41
|
matches = {}
|
@@ -1,10 +1,5 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
require 'dbi'
|
5
|
-
require 'caruby/util/options'
|
6
|
-
require 'caruby/util/log'
|
7
|
-
require 'caruby/domain/properties'
|
1
|
+
require 'jinx/helpers/options'
|
2
|
+
require 'caruby/rdbi/driver/jdbc'
|
8
3
|
|
9
4
|
module CaRuby
|
10
5
|
# SQLExecutor executes an SQL statement against the database.
|
@@ -23,19 +18,22 @@ module CaRuby
|
|
23
18
|
# The default database driver class is +com.mysql.jdbc.Driver+ for MySQL,
|
24
19
|
# +oracle.jdbc.OracleDriver+ for Oracle.
|
25
20
|
#
|
21
|
+
# The default database URI is +dbi:+_db_driver_+://+_db_host_+:+_db_port_+/+_db_name_.
|
22
|
+
#
|
26
23
|
# @param [Hash] opts the connect options
|
27
24
|
# @option opts [String] :database the mandatory database name
|
28
25
|
# @option opts [String] :database_user the mandatory database username (not the application login name)
|
29
26
|
# @option opts [String] :database_password the optional database password (not the application login password)
|
30
27
|
# @option opts [String] :database_host the optional database host
|
31
28
|
# @option opts [Integer] :database_port the optional database port number
|
32
|
-
# @option opts [
|
29
|
+
# @option opts [Integer] :database_port the optional database port number
|
33
30
|
# @option opts [String] :database_driver the optional DBI connect driver string, e.g. +jdbc:mysql+
|
31
|
+
# @option opts [String] :database_url the optional database connection URL
|
34
32
|
# @option opts [String] :database_driver_class the optional DBI connect driver class name
|
35
33
|
# @raise [CaRuby::ConfigurationError] if an option is invalid
|
36
34
|
def initialize(opts)
|
37
35
|
if opts.empty? then
|
38
|
-
|
36
|
+
Jinx.fail(CaRuby::ConfigurationError, "The caRuby database connection properties were not found.")
|
39
37
|
end
|
40
38
|
app_host = Options.get(:host, opts, 'localhost')
|
41
39
|
db_host = Options.get(:database_host, opts, app_host)
|
@@ -43,7 +41,8 @@ module CaRuby
|
|
43
41
|
db_driver = Options.get(:database_driver, opts) { default_driver_string(db_type) }
|
44
42
|
db_port = Options.get(:database_port, opts) { default_port(db_type) }
|
45
43
|
db_name = Options.get(:database, opts) { raise_missing_option_exception(:database) }
|
46
|
-
@
|
44
|
+
@db_url = Options.get(:database_url, opts) { "#{db_driver}://#{db_host}:#{db_port}/#{db_name}" }
|
45
|
+
@dbi_url = 'dbi:' + @db_url
|
47
46
|
@username = Options.get(:database_user, opts) { raise_missing_option_exception(:database_user) }
|
48
47
|
@password = Options.get(:database_password, opts)
|
49
48
|
@driver_class = Options.get(:database_driver_class, opts, default_driver_class(db_type))
|
@@ -56,16 +55,56 @@ module CaRuby
|
|
56
55
|
:database_port => db_port,
|
57
56
|
:database_driver => db_driver,
|
58
57
|
:database_driver_class => @driver_class,
|
59
|
-
:
|
58
|
+
:database_url => @db_url
|
60
59
|
}
|
61
60
|
logger.debug { "Database connection parameters (excluding password): #{eff_opts.qp}" }
|
62
61
|
end
|
63
62
|
|
64
63
|
# Connects to the database, yields the DBI handle to the given block and disconnects.
|
65
64
|
#
|
66
|
-
# @
|
65
|
+
# @yield [dbh] the transaction statements
|
66
|
+
# @yieldparam [RDBI::Database] dbh the database handle
|
67
67
|
def execute
|
68
|
-
|
68
|
+
RDBI.connect(:JDBC, :database => @db_url, :user => @username, :password => @password, :driver_class=> @driver_class) do |dbh|
|
69
|
+
yield dbh
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Runs the given query.
|
74
|
+
#
|
75
|
+
# @param [String] sql the SQL to execute
|
76
|
+
# @param [Array] args the SQL bindings
|
77
|
+
# @return [Array] the query result
|
78
|
+
def query(sql, *args)
|
79
|
+
fetched = nil
|
80
|
+
execute do |dbh|
|
81
|
+
res = dbh.execute(sql, *args)
|
82
|
+
fetched = res.fetch(:all)
|
83
|
+
res.finish
|
84
|
+
end
|
85
|
+
fetched
|
86
|
+
end
|
87
|
+
|
88
|
+
# Runs the given SQL or block in a transaction. If SQL is provided, then that
|
89
|
+
# SQL is executed. Otherwise, the block is called.
|
90
|
+
#
|
91
|
+
# @quirk RDBI RDBI converts nil args to 12. Work-around this bug by embedding
|
92
|
+
# +NULL+ in the SQL instead.
|
93
|
+
#
|
94
|
+
# @param [String] sql the SQL to execute
|
95
|
+
# @param [Array] args the SQL bindings
|
96
|
+
# @yield [dbh] the transaction statements
|
97
|
+
# @yieldparam [RDBI::Database] dbh the database handle
|
98
|
+
def transact(sql=nil, *args)
|
99
|
+
# Work-around for rcbi nil substitution.
|
100
|
+
if sql then
|
101
|
+
sql, *args = replace_nil_binds(sql, args)
|
102
|
+
transact { |dbh| dbh.execute(sql, *args) }
|
103
|
+
elsif block_given? then
|
104
|
+
execute { |dbh| dbh.transaction { yield dbh } }
|
105
|
+
else
|
106
|
+
raise ArgumentError.new("SQL executor is missing the required execution block")
|
107
|
+
end
|
69
108
|
end
|
70
109
|
|
71
110
|
private
|
@@ -73,12 +112,44 @@ module CaRuby
|
|
73
112
|
MYSQL_DRIVER_CLASS_NAME = 'com.mysql.jdbc.Driver'
|
74
113
|
|
75
114
|
ORACLE_DRIVER_CLASS_NAME = 'oracle.jdbc.OracleDriver'
|
115
|
+
|
116
|
+
# Replaces nil arguments with a +NULL+ literal in the given SQL.
|
117
|
+
#
|
118
|
+
# @param (see #transact)
|
119
|
+
# @return [Array] the (possibly modified) SQL followed by the non-nil arguments
|
120
|
+
def replace_nil_binds(sql, args)
|
121
|
+
nils = []
|
122
|
+
args.each_with_index { |value, i| nils << i if value.nil? }
|
123
|
+
unless nils.empty? then
|
124
|
+
logger.debug { "SQL executor working around RDBI bug by eliminating the nil arguments #{nils.to_series} for the SQL:\n#{sql}..." }
|
125
|
+
# Quoted ? is too much of a pain for this hack; bail out.
|
126
|
+
raise ArgumentError.new("RDBI work-around does not support quoted ? in transactional SQL: #{sql}") if sql =~ /'[^,]*[?][^,]*'/
|
127
|
+
prefix, binds_s, suffix = /(.+\s*values\s*\()([^)]*)(\).*)/i.match(sql).captures
|
128
|
+
sql = prefix
|
129
|
+
binds = binds_s.split('?')
|
130
|
+
last = binds_s[-1, 1]
|
131
|
+
del_cnt = 0
|
132
|
+
binds.each_with_index do |s, i|
|
133
|
+
sql << s
|
134
|
+
if nils.include?(i) then
|
135
|
+
args.delete_at(i - del_cnt)
|
136
|
+
del_cnt += 1
|
137
|
+
sql << 'NULL'
|
138
|
+
elsif i < binds.size - 1 or last == '?'
|
139
|
+
sql << '?'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
sql << suffix
|
143
|
+
end
|
144
|
+
logger.debug { "SQL executor converted the SQL to:\n#{sql}\nwith arguments #{args.qp}" }
|
145
|
+
return args.unshift(sql)
|
146
|
+
end
|
76
147
|
|
77
148
|
def default_driver_string(db_type)
|
78
149
|
case db_type.downcase
|
79
150
|
when 'mysql' then 'Jdbc:mysql'
|
80
151
|
when 'oracle' then 'Oracle'
|
81
|
-
else
|
152
|
+
else Jinx.fail(CaRuby::ConfigurationError, "Default database connection driver string could not be determined for database type #{db_type}")
|
82
153
|
end
|
83
154
|
end
|
84
155
|
|
@@ -86,7 +157,7 @@ module CaRuby
|
|
86
157
|
case db_type.downcase
|
87
158
|
when 'mysql' then MYSQL_DRIVER_CLASS_NAME
|
88
159
|
when 'oracle' then ORACLE_DRIVER_CLASS_NAME
|
89
|
-
else
|
160
|
+
else Jinx.fail(CaRuby::ConfigurationError, "Default database connection driver class could not be determined for database type #{db_type}")
|
90
161
|
end
|
91
162
|
end
|
92
163
|
|
@@ -94,12 +165,12 @@ module CaRuby
|
|
94
165
|
case db_type.downcase
|
95
166
|
when 'mysql' then 3306
|
96
167
|
when 'oracle' then 1521
|
97
|
-
else
|
168
|
+
else Jinx.fail(CaRuby::ConfigurationError, "Default database connection port could not be determined for database type #{db_type}")
|
98
169
|
end
|
99
170
|
end
|
100
171
|
|
101
172
|
def raise_missing_option_exception(option)
|
102
|
-
|
173
|
+
Jinx.fail(CaRuby::ConfigurationError, "Database connection property not found: #{option}")
|
103
174
|
end
|
104
175
|
end
|
105
176
|
end
|
@@ -1,8 +1,10 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
1
|
+
require 'jinx/helpers/collection'
|
2
|
+
require 'jinx/helpers/pretty_print'
|
3
|
+
require 'jinx/resource/reference_visitor'
|
4
|
+
require 'jinx/resource/match_visitor'
|
5
|
+
require 'jinx/resource/merge_visitor'
|
4
6
|
require 'caruby/database/saved_matcher'
|
5
|
-
require 'caruby/database/
|
7
|
+
require 'caruby/database/writer_template_builder'
|
6
8
|
|
7
9
|
module CaRuby
|
8
10
|
class Database
|
@@ -11,23 +13,23 @@ module CaRuby
|
|
11
13
|
# Adds store capability to this Database.
|
12
14
|
def initialize
|
13
15
|
super
|
14
|
-
@ftchd_vstr = ReferenceVisitor.new { |tgt| tgt.class.fetched_domain_attributes }
|
15
|
-
@cr_tmpl_bldr =
|
16
|
-
@upd_tmpl_bldr =
|
16
|
+
@ftchd_vstr = Jinx::ReferenceVisitor.new { |tgt| tgt.class.fetched_domain_attributes }
|
17
|
+
@cr_tmpl_bldr = TemplateBuilder.new(self) { |ref| creatable_domain_attributes(ref) }
|
18
|
+
@upd_tmpl_bldr = TemplateBuilder.new(self) { |ref| updatable_domain_attributes(ref) }
|
17
19
|
# the save result => argument reference matcher
|
18
20
|
svd_mtchr = SavedMatcher.new
|
19
21
|
# the save (result, argument) synchronization visitor
|
20
|
-
@svd_sync_vstr = MatchVisitor.new(:matcher => svd_mtchr) { |ref| ref.class.dependent_attributes }
|
22
|
+
@svd_sync_vstr = Jinx::MatchVisitor.new(:matcher => svd_mtchr) { |ref| ref.class.dependent_attributes }
|
21
23
|
# the attributes to merge from the save result
|
22
24
|
mgbl = Proc.new { |ref| ref.class.domain_attributes }
|
23
25
|
# the save result => argument merge visitor
|
24
|
-
@svd_mrg_vstr = MergeVisitor.new(:matcher => svd_mtchr, :mergeable => mgbl) { |ref| ref.class.dependent_attributes }
|
26
|
+
@svd_mrg_vstr = Jinx::MergeVisitor.new(:matcher => svd_mtchr, :mergeable => mgbl) { |ref| ref.class.dependent_attributes }
|
25
27
|
end
|
26
28
|
|
27
29
|
# Creates the specified domain object obj and returns obj. The pre-condition for this method is as
|
28
30
|
# follows:
|
29
31
|
# * obj is a well-formed domain object with the necessary required attributes as determined by the
|
30
|
-
# {Resource#validate} method
|
32
|
+
# {Jinx::Resource#validate} method
|
31
33
|
# * obj does not have a database identifier attribute value
|
32
34
|
# * obj does not yet exist in the database
|
33
35
|
# The post-condition is that obj is created and assigned a database identifier.
|
@@ -55,8 +57,8 @@ module CaRuby
|
|
55
57
|
# owner.dependents.size == dependents_count #=> true
|
56
58
|
#
|
57
59
|
# If obj is not dependent, then the create strategy is as follows:
|
58
|
-
# * add default attribute values using {Resource#add_defaults}
|
59
|
-
# * validate obj using {Resource#validate}
|
60
|
+
# * add default attribute values using {Jinx::Resource#add_defaults}
|
61
|
+
# * validate obj using {Jinx::Resource#validate}
|
60
62
|
# * ensure that all saved independent reference attribute values exist in the database, creating
|
61
63
|
# them if necessary
|
62
64
|
# * ensure that all dependent reference attribute values can be created according to this set of rules
|
@@ -66,16 +68,16 @@ module CaRuby
|
|
66
68
|
# * copy the new database identifier to each created object, i.e. the transitive closure of obj and
|
67
69
|
# its dependents
|
68
70
|
#
|
69
|
-
# @param [Resource] the domain object to create
|
70
|
-
# @return [Resource] obj
|
71
|
+
# @param [Jinx::Resource] the domain object to create
|
72
|
+
# @return [Jinx::Resource] obj
|
71
73
|
# @raise [DatabaseError] if the database operation fails
|
72
74
|
def create(obj)
|
73
75
|
# guard against recursive call back into the same operation
|
74
76
|
# the only allowed recursive call is a dependent create which first creates the owner
|
75
77
|
if recursive_save?(obj, :create) then
|
76
|
-
|
78
|
+
Jinx.fail(DatabaseError, "Create #{obj.qp} recursively called in context #{print_operations}")
|
77
79
|
elsif obj.identifier then
|
78
|
-
|
80
|
+
Jinx.fail(DatabaseError, "Create unsuccessful since #{obj.qp} already has identifier #{obj.identifier}")
|
79
81
|
end
|
80
82
|
# create the object
|
81
83
|
perform(:create, obj) { create_object(obj) }
|
@@ -90,11 +92,12 @@ module CaRuby
|
|
90
92
|
# * validate that obj has a database identifier
|
91
93
|
# * the template is submitted to the application service update method
|
92
94
|
#
|
93
|
-
#
|
95
|
+
# @param [Jinx::Resource] the object to update
|
96
|
+
# @raise [DatabaseError] if the database operation fails
|
94
97
|
def update(obj)
|
95
98
|
# guard against a recursive call back into the same operation.
|
96
99
|
if recursive_save?(obj, :update) then
|
97
|
-
|
100
|
+
Jinx.fail(DatabaseError, "Update #{obj.qp} recursively called in context #{print_operations}")
|
98
101
|
end
|
99
102
|
# update the object
|
100
103
|
perform(:update, obj) { update_object(obj) }
|
@@ -109,8 +112,8 @@ module CaRuby
|
|
109
112
|
# @see #create
|
110
113
|
# @see #update
|
111
114
|
#
|
112
|
-
#@param [Resource] obj the domain object to save
|
113
|
-
# @return [Resource] obj
|
115
|
+
#@param [Jinx::Resource] obj the domain object to save
|
116
|
+
# @return [Jinx::Resource] obj
|
114
117
|
# @raise [DatabaseError] if the database operation fails
|
115
118
|
def save(obj)
|
116
119
|
logger.debug { "Saving #{obj}..." }
|
@@ -125,7 +128,7 @@ module CaRuby
|
|
125
128
|
# Note that some applications restrict or forbid delete operations. Check the specific application
|
126
129
|
# documentation to determine whether deletion is supported.
|
127
130
|
#
|
128
|
-
# @param [Resource] obj the domain object to delete
|
131
|
+
# @param [Jinx::Resource] obj the domain object to delete
|
129
132
|
# @raise [DatabaseError] if the database operation fails
|
130
133
|
def delete(obj)
|
131
134
|
perform(:delete, obj) { delete_object(obj) }
|
@@ -133,51 +136,61 @@ module CaRuby
|
|
133
136
|
|
134
137
|
# Creates the domain object obj, if necessary.
|
135
138
|
#
|
136
|
-
# @param [Resource, <Resource>] obj the domain object or collection of domain objects to find or create
|
139
|
+
# @param [Jinx::Resource, <Jinx::Resource>] obj the domain object or collection of domain objects to find or create
|
137
140
|
# @raise [ArgumentError] if obj is nil or empty
|
138
141
|
# @raise [DatabaseError] if obj could not be created
|
139
142
|
def ensure_exists(obj)
|
140
|
-
|
143
|
+
Jinx.fail(ArgumentError, "Database ensure_exists is missing a domain object argument") if obj.nil_or_empty?
|
141
144
|
obj.enumerate { |ref| find(ref, :create) unless ref.identifier }
|
142
145
|
|
143
146
|
end
|
144
147
|
|
145
148
|
private
|
146
149
|
|
147
|
-
# Returns whether
|
148
|
-
#
|
149
|
-
#
|
150
|
-
#
|
150
|
+
# Returns whether the given operation is already in progress on the same object.
|
151
|
+
# The operation is only allowed if it is in the scope of a dependent owner
|
152
|
+
# operation of the same type.
|
153
|
+
#
|
154
|
+
# For example, if a StudyEvent is owned by a Study, then the following is allowed:
|
155
|
+
# create StudyEvent
|
156
|
+
# ...
|
157
|
+
# create Study
|
158
|
+
# ...
|
159
|
+
# create StudyEvent
|
160
|
+
#
|
161
|
+
# same type on the same dependent is only allowed if the precursor operation was delegated to an owner save which
|
162
|
+
# in turn saves the dependent.
|
151
163
|
#
|
152
|
-
# @param [Resource] obj the domain object to save
|
164
|
+
# @param [Jinx::Resource] obj the domain object to save
|
153
165
|
# @param [Symbol] operation the +:create+ or +:update+ save operation
|
154
166
|
# @return [Boolean] whether the save operation is redundant
|
155
167
|
def recursive_save?(obj, operation)
|
156
168
|
@operations.any? { |op| op.type == operation and op.subject == obj } and
|
157
|
-
not obj.owner_ancestor?(@operations.last.subject)
|
169
|
+
not obj.owner_ancestor?(@operations.last.subject) and
|
170
|
+
not obj.dependent_update_only?(@operations.last.subject)
|
158
171
|
end
|
159
172
|
|
160
|
-
# Creates
|
173
|
+
# Creates the given object as follows:
|
161
174
|
# * if obj has an uncreated owner, then store the owner, which in turn will create a physical dependent
|
162
|
-
# * otherwise, create a
|
175
|
+
# * otherwise, create a savable template. The template is a copy of obj containing a recursive copy
|
163
176
|
# of each saved obj reference and resolved independent references
|
164
177
|
# * submit the template to the create application service
|
165
178
|
# * update the obj dependency transitive closure content from the create result
|
166
179
|
# * add a lazy-loader to obj for unfetched domain references
|
167
180
|
#
|
168
181
|
# @param (see #create)
|
169
|
-
# @return [Resource] obj
|
182
|
+
# @return [Jinx::Resource] obj
|
170
183
|
def create_object(obj)
|
171
184
|
# add obj to the transients set
|
172
185
|
@transients << obj
|
173
186
|
begin
|
174
187
|
# A dependent is created by saving the owner.
|
175
188
|
# Otherwise, create the object from a template.
|
176
|
-
owner =
|
189
|
+
owner = cascaded_dependent_owner(obj)
|
177
190
|
result = create_dependent(owner, obj) if owner
|
178
191
|
result ||= create_from_template(obj)
|
179
192
|
if result.nil? then
|
180
|
-
|
193
|
+
Jinx.fail(DatabaseError, "#{obj.class.qp} is not creatable in context #{print_operations}")
|
181
194
|
end
|
182
195
|
ensure
|
183
196
|
# since obj now has an id, removed from transients set
|
@@ -194,8 +207,8 @@ module CaRuby
|
|
194
207
|
# A logical dependent is created by its parent unless the parent already exists.
|
195
208
|
# If the logical parent exists, then dep must be created.
|
196
209
|
#
|
197
|
-
|
198
|
-
# @return [Resource] dep
|
210
|
+
# @param [Jinx::Resource] dep the dependent domain object to create
|
211
|
+
# @return [Jinx::Resource] dep
|
199
212
|
def create_dependent(owner, dep)
|
200
213
|
if owner.identifier.nil? then
|
201
214
|
logger.debug { "Adding #{owner.qp} dependent #{dep.qp} defaults..." }
|
@@ -211,22 +224,29 @@ module CaRuby
|
|
211
224
|
|
212
225
|
# If there is a saver proxy, then use the proxy.
|
213
226
|
if dep.class.method_defined?(:saver_proxy) then
|
214
|
-
save_with_proxy(dep)
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
227
|
+
saved = save_with_proxy(dep)
|
228
|
+
if saved then
|
229
|
+
# remove obj from transients to clear previous fetch, if any
|
230
|
+
@transients.delete(dep)
|
231
|
+
logger.debug { "Fetching #{dep.qp} to reflect the proxy save..." }
|
232
|
+
find(dep)
|
233
|
+
end
|
234
|
+
dep
|
219
235
|
end
|
220
236
|
end
|
221
237
|
|
222
238
|
# Saves the given domain object using a proxy.
|
223
239
|
#
|
224
|
-
# @param [Resource] obj the proxied domain object
|
225
|
-
# @return [Resource] obj
|
240
|
+
# @param [Jinx::Resource] obj the proxied domain object
|
241
|
+
# @return [Jinx::Resource] obj
|
226
242
|
# @raise [DatabaseError] if obj does not have a proxy
|
227
243
|
def save_with_proxy(obj)
|
228
244
|
proxy = obj.saver_proxy
|
229
|
-
if proxy.nil? then
|
245
|
+
if proxy.nil? then Jinx.fail(DatabaseError, "#{obj.class.qp} does not have a proxy") end
|
246
|
+
if recursive_save?(proxy, :create) then
|
247
|
+
logger.debug { "Foregoing #{obj.qp} save, since it will be handled by creating the proxy #{proxy}." }
|
248
|
+
return
|
249
|
+
end
|
230
250
|
logger.debug { "Saving #{obj.qp} by creating the proxy #{proxy}..." }
|
231
251
|
create(proxy)
|
232
252
|
logger.debug { "Created the #{obj.qp} proxy #{proxy}." }
|
@@ -235,17 +255,18 @@ module CaRuby
|
|
235
255
|
obj
|
236
256
|
end
|
237
257
|
|
238
|
-
# Creates
|
239
|
-
# objects referenced by the created
|
258
|
+
# Creates the given domain object by submitting a template to the persistence service.
|
259
|
+
# This method ensures that the domain objects referenced by the created object exist
|
260
|
+
# and are correctly stored.
|
240
261
|
#
|
241
262
|
# @quirk caCORE submitting the object directly for create runs into various caTissue bizlogic
|
242
263
|
# traps, e.g. Participant CPR is not cascaded but Participant bizlogic checks that each CPR
|
243
264
|
# referenced by Participant is ready to be created. It is treacherous to make assumptions
|
244
265
|
# about what caTissue bizlogic will or will not check. Therefore, the safer strategy is to
|
245
|
-
# build a template for submission that includes only the
|
246
|
-
#
|
247
|
-
#
|
248
|
-
#
|
266
|
+
# build a template for submission that includes only the cascaded and direct non-cascaded
|
267
|
+
# independent references. The independent references are created if necessary. The template
|
268
|
+
# thus includes only as much content as can safely pass through the caTissue bizlogic
|
269
|
+
# minefield.
|
249
270
|
#
|
250
271
|
# @quirk caCORE caCORE create does not update the submitted object to reflect the created
|
251
272
|
# content. The create result is a separate object, which in turn does not always reflect
|
@@ -295,43 +316,45 @@ module CaRuby
|
|
295
316
|
def build_create_template(obj)
|
296
317
|
build_save_template(obj, @cr_tmpl_bldr)
|
297
318
|
end
|
298
|
-
|
319
|
+
|
299
320
|
# @quirk caCORE application create logic might ignore a non-domain attribute value,
|
300
|
-
# e.g. the caTissue StorageContainer auto-generated name attribute. In other cases,
|
301
|
-
# always ignores a non-domain attribute value,
|
302
|
-
#
|
303
|
-
#
|
304
|
-
#
|
305
|
-
#
|
306
|
-
#
|
307
|
-
# This method returns whether the
|
308
|
-
# {
|
309
|
-
#
|
310
|
-
# @param [Resource] the
|
311
|
-
# @param [Resource] the
|
312
|
-
# @return [Boolean] whether
|
321
|
+
# e.g. the caTissue StorageContainer auto-generated name attribute. In other cases,
|
322
|
+
# the application always ignores a non-domain attribute value, e.g. the caTissue
|
323
|
+
# CollectionProtocolRegistration registration_date. The work-around is to
|
324
|
+
# check whether the create result differs from the create argument for the
|
325
|
+
# {Propertied#autogenerated_nondomain_attributes}, and, if so, to perform an update
|
326
|
+
# on the save argument.
|
327
|
+
#
|
328
|
+
# This method returns whether the save result differs from the save source for any
|
329
|
+
# {Propertied#autogenerated_nondomain_attributes}.
|
330
|
+
#
|
331
|
+
# @param [Jinx::Resource] obj the save argument
|
332
|
+
# @param [Jinx::Resource] source the save result
|
333
|
+
# @return [Boolean] whether the save result differs from the save source on the
|
334
|
+
# autogenerated non-domain attributes
|
313
335
|
def update_saved?(obj, source)
|
314
|
-
obj.class.autogenerated_nondomain_attributes.any? do |
|
315
|
-
intended = obj.send(
|
316
|
-
stored = source.send(
|
336
|
+
obj.class.autogenerated_nondomain_attributes.any? do |pa|
|
337
|
+
intended = obj.send(pa)
|
338
|
+
stored = source.send(pa)
|
317
339
|
if intended != stored then
|
318
|
-
logger.debug { "Saved #{obj.qp} #{
|
340
|
+
logger.debug { "Saved #{obj.qp} #{pa} value #{intended} differs from result value #{stored}..." }
|
341
|
+
true
|
319
342
|
end
|
320
343
|
end
|
321
344
|
end
|
322
345
|
|
323
|
-
# Returns the {
|
324
|
-
# one-to-one independent pending create.
|
346
|
+
# Returns the {MetadataPropertied#creatable_domain_attributes} which are not contravened
|
347
|
+
# by a one-to-one independent pending create.
|
325
348
|
#
|
326
349
|
# @param (see #create)
|
327
350
|
# @return [<Symbol>] the attributes to include in the create template
|
328
351
|
def creatable_domain_attributes(obj)
|
329
352
|
# filter the creatable attributes
|
330
|
-
obj.class.creatable_domain_attributes.compose do |
|
331
|
-
if exclude_pending_create_attribute?(obj,
|
353
|
+
obj.class.creatable_domain_attributes.compose do |pa|
|
354
|
+
if exclude_pending_create_attribute?(obj, pa) then
|
332
355
|
# Avoid printing duplicate log message.
|
333
356
|
if obj != @cr_dom_attr_log_obj then
|
334
|
-
logger.debug { "Excluded #{obj.qp} #{
|
357
|
+
logger.debug { "Excluded #{obj.qp} #{pa} in the create template since it references a 1:1 bidirectional independent pending create." }
|
335
358
|
@cr_dom_attr_log_obj = obj
|
336
359
|
end
|
337
360
|
false
|
@@ -343,24 +366,24 @@ module CaRuby
|
|
343
366
|
|
344
367
|
# Returns whether the given creatable domain attribute with value obj satisfies
|
345
368
|
# each of the following conditions:
|
346
|
-
# * the attribute is {
|
347
|
-
# * the attribute is not an {
|
369
|
+
# * the attribute is {Property#independent?}
|
370
|
+
# * the attribute is not an {Property#owner?}
|
348
371
|
# * the obj value is unsaved
|
349
372
|
# * the attribute is not mandatory
|
350
373
|
# * the attribute references a {#pending_create?} save context.
|
351
374
|
#
|
352
375
|
# @param obj (see #create)
|
353
|
-
# @param [
|
376
|
+
# @param [Property] pa the candidate attribute metadata
|
354
377
|
# @return [Boolean] whether the attribute should not be included in the create template
|
355
|
-
def exclude_pending_create_attribute?(obj,
|
356
|
-
|
357
|
-
not
|
378
|
+
def exclude_pending_create_attribute?(obj, pa)
|
379
|
+
pa.independent? and
|
380
|
+
not pa.owner? and
|
358
381
|
obj.identifier.nil? and
|
359
|
-
not obj.mandatory_attributes.include?(
|
360
|
-
exclude_pending_create_value?(obj.send(
|
382
|
+
not obj.mandatory_attributes.include?(pa.to_sym) and
|
383
|
+
exclude_pending_create_value?(obj.send(pa.to_sym))
|
361
384
|
end
|
362
385
|
|
363
|
-
# @param [Resource, <Resource>, nil] value the referenced value
|
386
|
+
# @param [Jinx::Resource, <Jinx::Resource>, nil] value the referenced value
|
364
387
|
# @return [Boolean] whether the value includes a {#pending_create?} save context object
|
365
388
|
def exclude_pending_create_value?(value)
|
366
389
|
return false if value.nil?
|
@@ -371,7 +394,7 @@ module CaRuby
|
|
371
394
|
end
|
372
395
|
end
|
373
396
|
|
374
|
-
# @param [Resource] obj the object to check
|
397
|
+
# @param [Jinx::Resource] obj the object to check
|
375
398
|
# @return [Boolean] whether the penultimate create operation is on the object
|
376
399
|
def pending_create?(obj)
|
377
400
|
op = penultimate_create_operation
|
@@ -384,7 +407,7 @@ module CaRuby
|
|
384
407
|
nil
|
385
408
|
end
|
386
409
|
|
387
|
-
# Returns the {
|
410
|
+
# Returns the {MetadataPropertied#updatable_domain_attributes}.
|
388
411
|
#
|
389
412
|
# @param (see #update)
|
390
413
|
# @return the attributes to include in the update template
|
@@ -396,49 +419,39 @@ module CaRuby
|
|
396
419
|
def update_object(obj)
|
397
420
|
# database identifier is required for update
|
398
421
|
if obj.identifier.nil? then
|
399
|
-
|
422
|
+
Jinx.fail(DatabaseError, "Update target is missing a database identifier: #{obj}")
|
400
423
|
end
|
401
424
|
|
402
|
-
#
|
425
|
+
# If this object is proxied, then delegate to the proxy.
|
403
426
|
if obj.class.method_defined?(:saver_proxy) then
|
404
427
|
return save_with_proxy(obj)
|
405
428
|
end
|
406
429
|
|
407
|
-
#
|
430
|
+
# If a changed dependent is saved with a proxy, then update that dependent first.
|
408
431
|
proxied = updatable_proxied_dependents(obj)
|
409
432
|
unless proxied.empty? then
|
410
433
|
proxied.each { |dep| update(dep) }
|
411
434
|
end
|
412
435
|
|
413
|
-
#
|
414
|
-
owner =
|
436
|
+
# Update a cascaded dependent by updating the owner.
|
437
|
+
owner = cascaded_dependent_owner(obj)
|
415
438
|
if owner then return update_cascaded_dependent(owner, obj) end
|
416
|
-
|
417
|
-
|
418
|
-
tmpl = build_update_template(obj)
|
419
|
-
# call the caCORE service with an obj update template
|
420
|
-
save_with_template(obj, tmpl) { |svc| svc.update(tmpl) }
|
421
|
-
# take a snapshot of the updated content
|
422
|
-
obj.take_snapshot
|
439
|
+
# Not a cascaded dependent; update using a template.
|
440
|
+
update_from_template(obj)
|
423
441
|
end
|
424
442
|
|
425
|
-
# Returns the owner that can cascade update to the given object.
|
426
|
-
#
|
427
|
-
#
|
428
|
-
# is {Attribute#cascaded?}.
|
443
|
+
# Returns the owner that can cascade update to the given object. The owner is the
|
444
|
+
# {Jinx::Resource#effective_owner_attribute} value for which the owner attribute
|
445
|
+
# {Property#inverse_property} is {Property#cascaded?}.
|
429
446
|
#
|
430
|
-
# @param [Resource] obj the domain object to update
|
431
|
-
# @return [Resource, nil] the owner which can cascade an update to the object, or nil if none
|
447
|
+
# @param [Jinx::Resource] obj the domain object to update
|
448
|
+
# @return [Jinx::Resource, nil] the owner which can cascade an update to the object, or nil if none
|
432
449
|
# @raise [DatabaseError] if the domain object is a cascaded dependent but does not have an owner
|
433
|
-
def
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
if
|
438
|
-
dep_md = obj.class.attribute_metadata(oattr).inverse_metadata
|
439
|
-
if dep_md and dep_md.cascaded? then
|
440
|
-
obj.send(oattr)
|
441
|
-
end
|
450
|
+
def cascaded_dependent_owner(obj)
|
451
|
+
# the owner attribute and value
|
452
|
+
op, oref = obj.effective_owner_property_value || return
|
453
|
+
dp = op.inverse_property
|
454
|
+
oref if dp and dp.cascaded?
|
442
455
|
end
|
443
456
|
|
444
457
|
def update_cascaded_dependent(owner, obj)
|
@@ -477,14 +490,14 @@ module CaRuby
|
|
477
490
|
# must be created via the proxy after the Specimen is created.
|
478
491
|
#
|
479
492
|
# @param (see #update)
|
480
|
-
# @return [<Resource>] the #{
|
493
|
+
# @return [<Jinx::Resource>] the #{Propertied#proxied_savable_template_attributes} dependents
|
481
494
|
# which are #{Persistable#changed?}
|
482
495
|
def updatable_proxied_dependents(obj)
|
483
|
-
|
484
|
-
return Array::EMPTY_ARRAY if
|
496
|
+
pas = obj.class.proxied_savable_template_attributes
|
497
|
+
return Array::EMPTY_ARRAY if pas.empty?
|
485
498
|
deps = []
|
486
|
-
|
487
|
-
obj.send(
|
499
|
+
pas.each do |pa|
|
500
|
+
obj.send(pa).enumerate { |dep| deps << dep if dep.identifier and dep.changed? }
|
488
501
|
end
|
489
502
|
deps
|
490
503
|
end
|
@@ -496,8 +509,8 @@ module CaRuby
|
|
496
509
|
end
|
497
510
|
|
498
511
|
# @param obj (see #save)
|
499
|
-
# @param [
|
500
|
-
# @return [Resource] the template to use as the save argument
|
512
|
+
# @param [TemplateBuilder] builder the builder to use
|
513
|
+
# @return [Jinx::Resource] the template to use as the save argument
|
501
514
|
def build_save_template(obj, builder)
|
502
515
|
builder.build_template(obj)
|
503
516
|
end
|
@@ -507,7 +520,7 @@ module CaRuby
|
|
507
520
|
def delete_object(obj)
|
508
521
|
# database identifier is required for delete
|
509
522
|
if obj.identifier.nil? then
|
510
|
-
|
523
|
+
Jinx.fail(DatabaseError, "Delete target is missing a database identifier: #{obj}")
|
511
524
|
end
|
512
525
|
persistence_service(obj.class).delete_object(obj)
|
513
526
|
end
|
@@ -518,7 +531,7 @@ module CaRuby
|
|
518
531
|
# create method is called on the template. Dependents are saved as well, if necessary.
|
519
532
|
#
|
520
533
|
# @param obj (see #store)
|
521
|
-
# @param [Resource] template the obj template to submit to caCORE
|
534
|
+
# @param [Jinx::Resource] template the obj template to submit to caCORE
|
522
535
|
def save_with_template(obj, template)
|
523
536
|
logger.debug { "Saving #{obj.qp} from template:\n#{template.dump}" }
|
524
537
|
# dispatch to the application service
|
@@ -537,15 +550,16 @@ module CaRuby
|
|
537
550
|
result
|
538
551
|
end
|
539
552
|
|
540
|
-
# Synchronizes the content of the given saved
|
541
|
-
# 1. The save result
|
542
|
-
# 2. Then the
|
543
|
-
# 3. If the
|
544
|
-
#
|
545
|
-
# 4. Each
|
546
|
-
#
|
547
|
-
#
|
548
|
-
# @param [Resource]
|
553
|
+
# Synchronizes the content of the given saved argument and result as follows:
|
554
|
+
# 1. The save result is first synchronized with the database content as necessary.
|
555
|
+
# 2. Then the result is merged into the argument.
|
556
|
+
# 3. If the argument must be resaved based on the call to {#update_saved?}, then the
|
557
|
+
# argument is resaved.
|
558
|
+
# 4. Each argument dependent which differs from the corresponding result dependent
|
559
|
+
# is saved.
|
560
|
+
#
|
561
|
+
# @param [Jinx::Resource] target the save argument
|
562
|
+
# @param [Jinx::Resource] source the caCORE save result
|
549
563
|
def sync_saved(target, source)
|
550
564
|
# clear the toxic source attributes
|
551
565
|
detoxify(source)
|
@@ -564,7 +578,7 @@ module CaRuby
|
|
564
578
|
save_changed_dependents(target)
|
565
579
|
end
|
566
580
|
|
567
|
-
# Synchronizes the given
|
581
|
+
# Synchronizes the given save result with the database content.
|
568
582
|
# The source is synchronized by {#sync_save_result}.
|
569
583
|
#
|
570
584
|
# @param (see #sync_saved)
|
@@ -583,7 +597,7 @@ module CaRuby
|
|
583
597
|
# the database, the difference induces an update of the reference.
|
584
598
|
#
|
585
599
|
# @param (see #sync_saved)
|
586
|
-
# @return [Resource] the merged target object
|
600
|
+
# @return [Jinx::Resource] the merged target object
|
587
601
|
def merge_saved(target, source)
|
588
602
|
logger.debug { "Merging saved result #{source} into saved #{target.qp}..." }
|
589
603
|
# Update each saved reference snapshot to reflect the database state and add a lazy loader
|
@@ -612,33 +626,48 @@ module CaRuby
|
|
612
626
|
return if source == target
|
613
627
|
# If the target was created, then refetch and merge the source if necessary to reflect auto-generated
|
614
628
|
# non-domain attribute values.
|
615
|
-
if target.identifier.nil? then sync_created_result_object(source) end
|
629
|
+
if target.identifier.nil? then sync_created_result_object(target, source) end
|
616
630
|
# If there are auto-generated attributes, then merge them into the save result.
|
617
631
|
sync_save_result_references(source, target)
|
618
632
|
# Set inverses consistently in the source object graph
|
619
633
|
set_inverses(source)
|
620
634
|
end
|
621
635
|
|
622
|
-
# Refetches the given create result
|
636
|
+
# Refetches the given create result if there are any {#autogenerated_nondomain_attributes}
|
623
637
|
# which must be fetched to reflect the database state.
|
624
638
|
#
|
625
|
-
# @param
|
626
|
-
def sync_created_result_object(source)
|
627
|
-
|
628
|
-
return if
|
629
|
-
logger.debug { "Refetch #{source} to reflect auto-generated database content for attributes #{
|
639
|
+
# @param (see #sync_save_result)
|
640
|
+
def sync_created_result_object(target, source)
|
641
|
+
pas = nondomain_sync_attributes(target, source)
|
642
|
+
return if pas.empty?
|
643
|
+
logger.debug { "Refetch #{source} to reflect auto-generated database content for attributes #{pas.to_series}..." }
|
630
644
|
find(source)
|
631
645
|
end
|
632
646
|
|
633
|
-
#
|
647
|
+
# Returns the attributes which must be fetched into the given source prior to a merge with
|
648
|
+
# the save argument target. This default implementation returns the subject class's
|
649
|
+
# {Propertied#autogenerated_nondomain_attributes} if the save is a create, the empty array
|
650
|
+
# otherwise. Subclasses can specialize this method for any special cases that depend on the
|
651
|
+
# instance state.
|
652
|
+
#
|
653
|
+
# @return [<Symbol>] the attributes which must be fetched into the source
|
654
|
+
def nondomain_sync_attributes(target, source)
|
655
|
+
if target.identifier.nil? then
|
656
|
+
source.class.autogenerated_nondomain_attributes
|
657
|
+
else
|
658
|
+
Array::EMPTY_ARRAY
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
# Fetches the {#synchronization_attributes} into the given save result.
|
634
663
|
#
|
635
664
|
# @param (see #sync_saved)
|
636
665
|
def sync_save_result_references(source, target)
|
637
|
-
|
638
|
-
return if
|
639
|
-
logger.debug { "Fetching the saved #{target.qp} attributes #{
|
640
|
-
|
641
|
-
logger.debug { "Fetched the saved #{target.qp} attributes #{
|
666
|
+
pas = synchronization_attributes(source, target)
|
667
|
+
return if pas.empty?
|
668
|
+
logger.debug { "Fetching the saved #{target.qp} attributes #{pas.to_series} into the save result #{source.qp}..." }
|
669
|
+
pas.each { |pa| sync_save_result_attribute(source, pa) }
|
670
|
+
logger.debug { "Fetched the saved #{target.qp} attributes #{pas.to_series} into the save result #{source.qp}." }
|
642
671
|
end
|
643
672
|
|
644
673
|
# @see #sync_save_result_references
|
@@ -646,13 +675,13 @@ module CaRuby
|
|
646
675
|
# fetch the value
|
647
676
|
fetched = fetch_association(source, attribute)
|
648
677
|
# set the attribute
|
649
|
-
source.
|
678
|
+
source.set_property_value(attribute, fetched)
|
650
679
|
end
|
651
680
|
|
652
|
-
# Returns the saved target attributes which must be fetched to reflect the database content,
|
653
|
-
# of the following:
|
654
|
-
# * {Persistable#
|
655
|
-
# * {
|
681
|
+
# Returns the saved target attributes which must be fetched to reflect the database content,
|
682
|
+
# consisting of the following:
|
683
|
+
# * {Persistable#saved_attributes_to_fetch}
|
684
|
+
# * {Propertied#domain_attributes} which include a source reference without an identifier
|
656
685
|
#
|
657
686
|
# @param (see #sync_saved)
|
658
687
|
# @return [<Symbol>] the attributes which must be fetched
|
@@ -660,63 +689,69 @@ module CaRuby
|
|
660
689
|
# the target save operation
|
661
690
|
op = @operations.last
|
662
691
|
# the attributes to fetch
|
663
|
-
|
692
|
+
pas = target.saved_attributes_to_fetch(op).to_set
|
664
693
|
# the pending create, if any
|
665
694
|
pndg_op = penultimate_create_operation
|
666
695
|
pndg = pndg_op.subject if pndg_op
|
667
696
|
# add in the domain attributes whose identifier was not set in the result
|
668
|
-
source.class.
|
669
|
-
srcval = source.send(
|
670
|
-
tgtval = target.send(
|
697
|
+
source.class.sync_domain_attributes.select do |pa|
|
698
|
+
srcval = source.send(pa)
|
699
|
+
tgtval = target.send(pa)
|
671
700
|
if Persistable.unsaved?(srcval) then
|
672
|
-
logger.debug { "Fetching save result #{source.qp} #{
|
673
|
-
|
701
|
+
logger.debug { "Fetching the save result #{source.qp} #{pa} since a referenced object identifier was not set in the result..." }
|
702
|
+
pas << pa
|
674
703
|
elsif srcval.nil_or_empty? and Persistable.unsaved?(tgtval) and tgtval != pndg then
|
675
|
-
logger.debug { "Fetching save result #{source.qp} #{
|
676
|
-
|
704
|
+
logger.debug { "Fetching the save result #{source.qp} #{pa} since the target #{target.qp} value #{tgtval.qp} is missing an identifier..." }
|
705
|
+
pas << pa
|
677
706
|
end
|
678
707
|
end
|
679
|
-
|
708
|
+
pas
|
680
709
|
end
|
681
710
|
|
682
711
|
# Saves the given domain object dependents.
|
683
712
|
#
|
684
|
-
# @param [Resource] obj the owner domain object
|
713
|
+
# @param [Jinx::Resource] obj the owner domain object
|
685
714
|
def save_changed_dependents(obj)
|
686
|
-
obj.class.dependent_attributes.
|
687
|
-
deps = obj.
|
688
|
-
|
689
|
-
|
715
|
+
obj.class.dependent_attributes.each_property do |prop|
|
716
|
+
deps = obj.dependents(prop)
|
717
|
+
unless deps.nil_or_empty? then
|
718
|
+
logger.debug { "Saving the #{obj} dependents #{deps.to_enum.qp(:single_line)} which have changed..." }
|
719
|
+
end
|
720
|
+
deps.enumerate { |dep| save_dependent_if_changed(obj, prop, dep) }
|
690
721
|
end
|
691
722
|
end
|
692
723
|
|
693
724
|
# Saves the given dependent domain object if necessary.
|
694
725
|
# Recursively saves the obj dependents as necessary.
|
695
726
|
#
|
696
|
-
# @param [Resource] owner the dependent owner
|
697
|
-
# @param [
|
698
|
-
# @param [Resource] dependent the dependent to save
|
699
|
-
def save_dependent_if_changed(owner,
|
727
|
+
# @param [Jinx::Resource] owner the dependent owner
|
728
|
+
# @param [Property] property the dependent property
|
729
|
+
# @param [Jinx::Resource] dependent the dependent to save
|
730
|
+
def save_dependent_if_changed(owner, property, dependent)
|
731
|
+
# If the dependent identifier is missing, then the owner save did not cascade to the
|
732
|
+
# dependent. Create the dependent here.
|
700
733
|
if dependent.identifier.nil? then
|
701
|
-
logger.debug { "Creating #{owner.qp} #{
|
734
|
+
logger.debug { "Creating #{owner.qp} #{property} dependent #{dependent.qp}..." }
|
702
735
|
return create(dependent)
|
703
736
|
end
|
737
|
+
# Collect the changed attributes.
|
704
738
|
changes = dependent.changed_attributes
|
705
|
-
#
|
706
|
-
|
707
|
-
|
739
|
+
logger.debug { "#{owner.qp} #{property} dependent #{dependent.qp} changed for attributes #{changes.to_series}." } unless changes.empty?
|
740
|
+
# If any changed attribute is a reference to dependents, then check for
|
741
|
+
# special auto-generation handling. Otherwise, simpl save the referenced
|
742
|
+
# dependents.
|
743
|
+
if changes.any? { |pa| not dependent.class.property(pa).dependent? } then
|
708
744
|
# the owner save operation
|
709
745
|
op = operations.last
|
710
746
|
# The dependent is auto-generated if the owner was created or auto-generated and
|
711
747
|
# the dependent attribute is auto-generated.
|
712
|
-
|
713
|
-
|
714
|
-
logger.debug { "Updating the changed #{owner.qp} #{attribute} dependent #{dependent.qp}..." }
|
748
|
+
ag = (op.type == :create or op.autogenerated?) && property.autogenerated?
|
749
|
+
logger.debug { "Updating the changed #{owner.qp} #{property} dependent #{dependent.qp}..." }
|
715
750
|
if ag then
|
716
|
-
logger.debug { "Adding defaults to the auto-generated #{owner.qp} #{
|
751
|
+
logger.debug { "Adding defaults to the auto-generated #{owner.qp} #{property} dependent #{dependent.qp}..." }
|
717
752
|
dependent.add_defaults_autogenerated
|
718
753
|
end
|
719
|
-
update_changed_dependent(owner,
|
754
|
+
update_changed_dependent(owner, property, dependent, ag)
|
720
755
|
else
|
721
756
|
save_changed_dependents(dependent)
|
722
757
|
end
|
@@ -725,7 +760,8 @@ module CaRuby
|
|
725
760
|
# Updates the given dependent.
|
726
761
|
#
|
727
762
|
# @param (see #save_dependent_if_changed)
|
728
|
-
|
763
|
+
# @param [Boolean] autogenerated flag indicating whether the dependent was auto-generated
|
764
|
+
def update_changed_dependent(owner, property, dependent, autogenerated)
|
729
765
|
perform(:update, dependent, :autogenerated => autogenerated) { update_object(dependent) }
|
730
766
|
end
|
731
767
|
end
|