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
@@ -1,18 +1,10 @@
|
|
1
|
-
require '
|
2
|
-
require 'caruby/
|
3
|
-
require 'caruby/
|
1
|
+
require 'jinx/helpers/stopwatch'
|
2
|
+
require 'caruby/helpers/version'
|
3
|
+
require 'caruby/database/application_service'
|
4
4
|
|
5
5
|
module CaRuby
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
# The encapsulated caBIG service class.
|
10
|
-
java_import Java::gov.nih.nci.system.applicationservice.ApplicationServiceProvider
|
11
|
-
|
12
|
-
# This import is not strictly necessary, but works around Ticket #5.
|
13
|
-
java_import Java::gov.nih.nci.system.comm.client.ApplicationServiceClientImpl
|
14
|
-
|
15
|
-
# A PersistenceService wraps a caCORE application service.
|
6
|
+
# A PersistenceService is a database mediator which implements the {#query} {#create}, {#update}
|
7
|
+
# and {#delete} methods.
|
16
8
|
class PersistenceService
|
17
9
|
# The service name.
|
18
10
|
attr_reader :name
|
@@ -27,13 +19,14 @@ module CaRuby
|
|
27
19
|
# @option opts [String] :host the service host (default +localhost+)
|
28
20
|
# @option opts [String] :version the caTissue version identifier
|
29
21
|
def initialize(name, opts={})
|
22
|
+
CaRuby::PersistenceService.import_java_classes
|
30
23
|
@name = name
|
31
24
|
ver_opt = opts[:version]
|
32
25
|
@version = ver_opt.to_s.to_version if ver_opt
|
33
26
|
@host = opts[:host] || default_host
|
34
27
|
@port = opts[:port] || 8080
|
35
28
|
@url = "http://#{@host}:#{@port}/#{@name}/http/remoteService"
|
36
|
-
@timer = Stopwatch.new
|
29
|
+
@timer = Jinx::Stopwatch.new
|
37
30
|
logger.debug { "Created persistence service #{name} at #{@host}:#{@port}." }
|
38
31
|
end
|
39
32
|
|
@@ -63,7 +56,7 @@ module CaRuby
|
|
63
56
|
dispatch { |svc| svc.create_object(obj) }
|
64
57
|
rescue Exception => e
|
65
58
|
logger.error("Error creating #{obj} - #{e.message}\n#{dump(obj)}")
|
66
|
-
raise
|
59
|
+
raise e
|
67
60
|
end
|
68
61
|
end
|
69
62
|
|
@@ -73,8 +66,8 @@ module CaRuby
|
|
73
66
|
begin
|
74
67
|
dispatch { |svc| svc.update_object(obj) }
|
75
68
|
rescue Exception => e
|
76
|
-
logger.error("Error updating #{obj} -
|
77
|
-
raise
|
69
|
+
logger.error("Error updating #{obj} - #{e.message}\n#{dump(obj)}")
|
70
|
+
raise e
|
78
71
|
end
|
79
72
|
end
|
80
73
|
|
@@ -84,21 +77,16 @@ module CaRuby
|
|
84
77
|
begin
|
85
78
|
dispatch { |svc| svc.remove_object(obj) }
|
86
79
|
rescue Exception => e
|
87
|
-
logger.error("Error deleting #{obj}
|
88
|
-
raise
|
80
|
+
logger.error("Error deleting #{obj} - #{e.message}\n#{dump(obj)}")
|
81
|
+
raise e
|
89
82
|
end
|
90
83
|
end
|
91
84
|
|
92
|
-
# Returns
|
85
|
+
# Returns the {ApplicationService} remote instance.
|
93
86
|
#
|
94
|
-
# @
|
95
|
-
# must reinitialize the remote instance. E.g. this is done in the caTissue DE examples,
|
96
|
-
# and is a good general practice.
|
97
|
-
#
|
98
|
-
# @return [ApplicationServiceProvider] the CaCORE service provider wrapped by this PersistenceService
|
87
|
+
# @return the CaCORE service provider wrapped by this PersistenceService
|
99
88
|
def app_service
|
100
|
-
|
101
|
-
ApplicationServiceProvider.remote_instance(@url)
|
89
|
+
ApplicationService.for(@url)
|
102
90
|
end
|
103
91
|
|
104
92
|
private
|
@@ -144,13 +132,13 @@ module CaRuby
|
|
144
132
|
logger.debug { "Building HQLCriteria..." }
|
145
133
|
criteria = HQLCriteria.new(hql)
|
146
134
|
target = hql[/from\s+(\S+)/i, 1]
|
147
|
-
|
135
|
+
Jinx.fail(DatabaseError, "HQL does not contain a FROM clause: #{hql}") unless target
|
148
136
|
logger.debug { "Submitting search on target class #{target} with the following HQL:\n #{hql}" }
|
149
137
|
begin
|
150
138
|
dispatch { |svc| svc.query(criteria, target) }
|
151
139
|
rescue Exception => e
|
152
140
|
logger.error("Error querying on HQL - #{$!}:\n#{hql}")
|
153
|
-
raise
|
141
|
+
raise e
|
154
142
|
end
|
155
143
|
end
|
156
144
|
|
@@ -161,9 +149,9 @@ module CaRuby
|
|
161
149
|
logger.debug { "Searching using template #{template.qp}#{', path ' + path.join('.') unless path.empty?}..." }
|
162
150
|
# collect the class search path from the reference attribute domain type Java class names
|
163
151
|
class_name_path = []
|
164
|
-
path.inject(template.class) do |type,
|
165
|
-
ref_type = type.domain_type(
|
166
|
-
|
152
|
+
path.inject(template.class) do |type, pa|
|
153
|
+
ref_type = type.domain_type(pa)
|
154
|
+
Jinx.fail(DatabaseError, "Property in search attribute path #{path.join('.')} is not a #{type} domain reference attribute: #{pa}") if ref_type.nil?
|
167
155
|
class_name_path << ref_type.java_class.name
|
168
156
|
ref_type
|
169
157
|
end
|
@@ -175,7 +163,7 @@ module CaRuby
|
|
175
163
|
dispatch { |svc| svc.search(reverse_class_name_path.join(','), template) }
|
176
164
|
rescue Exception => e
|
177
165
|
logger.error("Error searching on template #{template}#{', path ' + path.join('.') unless path.empty?} - #{$!}\n#{dump(template)}")
|
178
|
-
raise
|
166
|
+
raise e
|
179
167
|
end
|
180
168
|
end
|
181
169
|
|
@@ -198,7 +186,17 @@ module CaRuby
|
|
198
186
|
end
|
199
187
|
|
200
188
|
def dump(obj)
|
201
|
-
Resource === obj ? obj.dump : obj.to_s
|
189
|
+
Jinx::Resource === obj ? obj.dump : obj.to_s
|
202
190
|
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
# Imports this class's Java classes on demand.
|
195
|
+
def self.import_java_classes
|
196
|
+
return if const_defined?(:HQLCriteria)
|
197
|
+
# HQLCriteria is required for the query_hql method.
|
198
|
+
java_import Java::gov.nih.nci.common.util.HQLCriteria
|
199
|
+
end
|
200
|
+
|
203
201
|
end
|
204
202
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'jinx/resource/reference_visitor'
|
1
2
|
require 'caruby/database/persistable'
|
2
3
|
require 'caruby/database/lazy_loader'
|
3
4
|
|
@@ -6,42 +7,53 @@ module CaRuby
|
|
6
7
|
# @return [LazyLoader] this database's lazy loader
|
7
8
|
attr_reader :lazy_loader
|
8
9
|
|
9
|
-
# Database Persistable mediator.
|
10
|
+
# Database {Persistable} mediator.
|
11
|
+
# A module which includes {Persistifier} must implement the {Reader#fetch_association} method.
|
10
12
|
module Persistifier
|
11
13
|
# Adds query capability to this Database.
|
12
14
|
def initialize
|
13
15
|
super
|
14
|
-
@ftchd_vstr = ReferenceVisitor.new { |ref| ref.class.fetched_domain_attributes }
|
16
|
+
@ftchd_vstr = Jinx::ReferenceVisitor.new { |ref| ref.class.fetched_domain_attributes }
|
15
17
|
# the demand loader
|
16
|
-
@lazy_loader = LazyLoader.new { |obj,
|
18
|
+
@lazy_loader = LazyLoader.new { |obj, pa| lazy_load(obj, pa) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Clears the cache.
|
22
|
+
def clear
|
23
|
+
@cache.clear if @cache
|
17
24
|
end
|
18
25
|
|
19
26
|
private
|
20
27
|
|
21
28
|
# Adds this database's lazy loader to the given domain object.
|
22
29
|
#
|
23
|
-
# @param [Resource] obj the domain object to lazy-load
|
30
|
+
# @param [Jinx::Resource] obj the domain object to lazy-load
|
24
31
|
def add_lazy_loader(obj, attributes=nil)
|
25
32
|
obj.add_lazy_loader(@lazy_loader, attributes)
|
26
33
|
end
|
27
34
|
|
28
|
-
# Loads the content of the given attribute.
|
29
|
-
#
|
35
|
+
# Loads the content of the given attribute. If the attribute is independent,
|
36
|
+
# then the fetched objects are replaced by corresponding cached objects,
|
37
|
+
# if cached. The fetched references are persistified with {#persistify}.
|
30
38
|
#
|
31
|
-
# @param [Resource] obj the domain object whose content is to be loaded
|
39
|
+
# @param [Jinx::Resource] obj the domain object whose content is to be loaded
|
32
40
|
# @param [Symbol] attribute the attribute to load
|
33
|
-
# @return [Resource, <Resource>, nil] the loaded value
|
41
|
+
# @return [Jinx::Resource, <Jinx::Resource>, nil] the loaded value
|
34
42
|
def lazy_load(obj, attribute)
|
35
|
-
fetched = fetch_association(obj, attribute)
|
36
|
-
|
43
|
+
fetched = fetch_association(obj, attribute) || return
|
44
|
+
if obj.class.property(attribute).dependent? then
|
45
|
+
persistify(fetched)
|
46
|
+
else
|
47
|
+
reconcile_fetched(fetched)
|
48
|
+
end
|
37
49
|
end
|
38
50
|
|
39
51
|
# For each fetched domain object, if there is a corresponding cached object,
|
40
52
|
# then the reconciled value is that cached object. Otherwise, the reconciled
|
41
53
|
# object is the persistified fetched object.
|
42
54
|
#
|
43
|
-
# @param [Resource, <Resource>] fetched the fetched domain object(s)
|
44
|
-
# @return [Resource, <Resource>] the reconciled domain object(s)
|
55
|
+
# @param [Jinx::Resource, <Jinx::Resource>] fetched the fetched domain object(s)
|
56
|
+
# @return [Jinx::Resource, <Jinx::Resource>] the reconciled domain object(s)
|
45
57
|
def reconcile_fetched(fetched)
|
46
58
|
if Enumerable === fetched then
|
47
59
|
fetched.map { |ref| reconcile_fetched(ref) }
|
@@ -50,18 +62,17 @@ module CaRuby
|
|
50
62
|
end
|
51
63
|
end
|
52
64
|
|
53
|
-
# @param [Resource] fetched the fetched domain object
|
54
|
-
# @return [Resource] the corresponding cached object, if
|
55
|
-
# otherwise the fetched object
|
65
|
+
# @param [Jinx::Resource] fetched the fetched domain object
|
66
|
+
# @return [Jinx::Resource, nil] the corresponding cached object, if any
|
56
67
|
def reconcile_cached(fetched)
|
57
|
-
cached = @cache[fetched]
|
68
|
+
cached = @cache[fetched] if @cache
|
58
69
|
if cached then
|
59
70
|
logger.debug { "Replaced fetched #{fetched} with cached #{cached}." }
|
60
71
|
end
|
61
72
|
cached
|
62
73
|
end
|
63
74
|
|
64
|
-
# This method
|
75
|
+
# This method clears the given toxic domain objects fetched from the database.
|
65
76
|
# The copy nondomain attribute values are set to the fetched object values.
|
66
77
|
# The copy fetched reference attribute values are set to a copy of the result references.
|
67
78
|
#
|
@@ -75,9 +86,9 @@ module CaRuby
|
|
75
86
|
# set to the parent. Rather, it is a toxic caCORE reference which must be purged. This
|
76
87
|
# leaves an empty reference which must be lazy-loaded, which is inefficient and inconsistent.
|
77
88
|
# This situation is rectified in this detoxify method by setting the dependent owner
|
78
|
-
# attribute to the fetched owner in the detoxification {ReferenceVisitor} copy-match-merge.
|
89
|
+
# attribute to the fetched owner in the detoxification {Jinx::ReferenceVisitor} copy-match-merge.
|
79
90
|
#
|
80
|
-
# @return [Resource, <Resource>] the detoxified object(s)
|
91
|
+
# @return [Jinx::Resource, <Jinx::Resource>] the detoxified object(s)
|
81
92
|
def detoxify(toxic)
|
82
93
|
return if toxic.nil?
|
83
94
|
if toxic.collection? then
|
@@ -85,6 +96,7 @@ module CaRuby
|
|
85
96
|
else
|
86
97
|
logger.debug { "Detoxifying the toxic caCORE result #{toxic.qp}..." }
|
87
98
|
@ftchd_vstr.visit(toxic) { |ref| clear_toxic_attributes(ref) }
|
99
|
+
logger.debug { "Detoxified the toxic caCORE result #{toxic.qp}." }
|
88
100
|
end
|
89
101
|
toxic
|
90
102
|
end
|
@@ -92,59 +104,81 @@ module CaRuby
|
|
92
104
|
# Sets each of the toxic attributes in the given domain object to the corresponding
|
93
105
|
# {Metadata#empty_value}.
|
94
106
|
#
|
95
|
-
# @param [Resource] toxic the toxic domain object
|
107
|
+
# @param [Jinx::Resource] toxic the toxic domain object
|
96
108
|
def clear_toxic_attributes(toxic)
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
109
|
+
# The result class might not have been previously referenced. In that case,
|
110
|
+
# introspect the class.
|
111
|
+
ensure_introspected(toxic.class)
|
112
|
+
pas = toxic.class.toxic_attributes
|
113
|
+
return if pas.empty?
|
114
|
+
logger.debug { "Clearing toxic #{toxic.qp} attributes #{pas.to_series}..." }
|
115
|
+
pas.each_pair do |pa, prop|
|
101
116
|
# skip non-Java attributes
|
102
|
-
next unless
|
117
|
+
next unless prop.java_property?
|
103
118
|
# the empty or nil value to set
|
104
|
-
value = toxic.class.empty_value(
|
119
|
+
value = toxic.class.empty_value(pa)
|
105
120
|
# Use the Java writer method rather than the standard attribute writer method.
|
106
121
|
# The standard attribute writer enforces inverse integrity, which potential requires
|
107
122
|
# accessing the current toxic value. The Java writer bypasses inverse integrity.
|
108
|
-
reader, writer =
|
123
|
+
reader, writer = prop.property_accessors
|
109
124
|
# clear the attribute
|
110
125
|
toxic.send(writer, value)
|
111
126
|
end
|
112
127
|
end
|
113
128
|
|
114
|
-
# Persistifies the given domain object and all of its dependents
|
115
|
-
#
|
129
|
+
# Persistifies the given domain object and all of its dependents as follows:
|
130
|
+
# * Ensure that the object class is introspected.
|
131
|
+
# * Set the inverses to enforce inverse integrity.
|
116
132
|
#
|
117
133
|
# @param (see #persistify_object)
|
118
134
|
# @raise [ArgumentError] if obj is a collection and other is not nil
|
119
135
|
def persistify(obj, other=nil)
|
120
136
|
if obj.collection? then
|
121
|
-
if other then
|
137
|
+
if other then Jinx.fail(ArgumentError, "Database reader persistify other argument not supported") end
|
122
138
|
obj.each { |ref| persistify(ref) }
|
123
139
|
return obj
|
124
140
|
end
|
141
|
+
# The attribute type is introspected, but the object might be an unintrospected
|
142
|
+
# subtype. Introspect the object class if necessary.
|
143
|
+
ensure_introspected(obj.class)
|
125
144
|
# set the inverses before recursing to dependents
|
126
145
|
set_inverses(obj)
|
127
146
|
# recurse to dependents before adding a lazy loader to the owner
|
128
|
-
obj.
|
147
|
+
obj.dependents.each { |dep| persistify(dep) if dep.identifier }
|
129
148
|
persistify_object(obj, other)
|
130
149
|
end
|
131
150
|
|
151
|
+
# Introspects the given class, if necessary. The class must be either introspected
|
152
|
+
# or a subclass of an introspected class.
|
153
|
+
#
|
154
|
+
# @param [Class] klass the class to introspect if necessary
|
155
|
+
def ensure_introspected(klass)
|
156
|
+
unless klass.introspected? then
|
157
|
+
sc = klass.superclass
|
158
|
+
ensure_introspected(sc)
|
159
|
+
logger.debug { "Introspecting the fetched object class #{klass}..." }
|
160
|
+
# Resolving the class name in the context of the domain module
|
161
|
+
# introspects the class.
|
162
|
+
sc.domain_module.const_get(klass.name.demodulize)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
132
166
|
# Takes a {Persistable#snapshot} of obj to track changes, adds a lazy loader and
|
133
167
|
# adds the object to the cache.
|
134
168
|
#
|
135
169
|
# If the other fetched source object is given, then the obj snapshot is updated
|
136
170
|
# with the non-nil values from other.
|
137
171
|
#
|
138
|
-
# @param [Resource] obj the domain object to make persistable
|
139
|
-
# @param [Resource] other the source domain object
|
140
|
-
# @return [Resource] obj
|
172
|
+
# @param [Jinx::Resource] obj the domain object to make persistable
|
173
|
+
# @param [Jinx::Resource] other the source domain object
|
174
|
+
# @return [Jinx::Resource] obj
|
141
175
|
def persistify_object(obj, other=nil)
|
142
176
|
# take a snapshot of the database content
|
143
177
|
snapshot(obj, other)
|
144
178
|
# add lazy loader to the unfetched attributes
|
145
179
|
add_lazy_loader(obj)
|
146
180
|
# add to the cache
|
147
|
-
|
181
|
+
encache(obj)
|
148
182
|
obj
|
149
183
|
end
|
150
184
|
|
@@ -155,12 +189,12 @@ module CaRuby
|
|
155
189
|
#
|
156
190
|
# @param obj (see #persistify_object)
|
157
191
|
def set_inverses(obj)
|
158
|
-
obj.class.domain_attributes.each_pair do |
|
159
|
-
|
160
|
-
if
|
161
|
-
obj.send(
|
192
|
+
obj.class.domain_attributes.each_pair do |pa, prop|
|
193
|
+
inv_prop = prop.inverse_property || next
|
194
|
+
if inv_prop.collection? then
|
195
|
+
obj.send(pa).enumerate { |ref| ref.send(inv_prop.attribute) << obj }
|
162
196
|
else
|
163
|
-
obj.send(
|
197
|
+
obj.send(pa).enumerate { |ref| ref.set_property_value(inv_prop.attribute, obj) }
|
164
198
|
end
|
165
199
|
end
|
166
200
|
end
|
@@ -170,9 +204,9 @@ module CaRuby
|
|
170
204
|
# values into the obj snapshot, replacing an existing obj non-domain value with the
|
171
205
|
# corresponding other attribute value if and only if the other attribute value is non-nil.
|
172
206
|
#
|
173
|
-
# @param [Resource] obj the domain object to snapshot
|
174
|
-
# @param [Resource] the source domain object
|
175
|
-
# @return [Resource] the obj snapshot, updated with source content if necessary
|
207
|
+
# @param [Jinx::Resource] obj the domain object to snapshot
|
208
|
+
# @param [Jinx::Resource] the source domain object
|
209
|
+
# @return [Jinx::Resource] the obj snapshot, updated with source content if necessary
|
176
210
|
def snapshot(obj, other=nil)
|
177
211
|
# take a fresh snapshot
|
178
212
|
obj.take_snapshot
|
@@ -180,6 +214,29 @@ module CaRuby
|
|
180
214
|
# merge the other object content if available
|
181
215
|
obj.merge_into_snapshot(other) if other
|
182
216
|
end
|
217
|
+
|
218
|
+
# @param [Jinx::Resource] obj the object to cache
|
219
|
+
# @raise [ArgumentError] if the given item does not have an identifier
|
220
|
+
def encache(obj)
|
221
|
+
@cache ||= create_cache
|
222
|
+
@cache.add(obj)
|
223
|
+
end
|
224
|
+
|
225
|
+
# @quirk JRuby identifier is not a stable object when fetched from the database, i.e.:
|
226
|
+
# obj.identifier.equal?(obj.identifier) #=> false
|
227
|
+
# This is probably an artifact of jRuby Numeric -> Java Long conversion interaction
|
228
|
+
# combined with hash access use of the eql? method. Work-around is to convert the
|
229
|
+
# identifier to a Ruby Integer.
|
230
|
+
#
|
231
|
+
# @return [Cache] a new object cache
|
232
|
+
def create_cache
|
233
|
+
Cache.new do |obj|
|
234
|
+
if obj.identifier.nil? then
|
235
|
+
Jinx.fail(ArgumentError, "Can't cache object without identifier: #{obj}")
|
236
|
+
end
|
237
|
+
obj.identifier.to_s.to_i
|
238
|
+
end
|
239
|
+
end
|
183
240
|
end
|
184
241
|
end
|
185
242
|
end
|
@@ -1,9 +1,9 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
require 'caruby/
|
1
|
+
require 'jinx/helpers/collection'
|
2
|
+
require 'jinx/helpers/pretty_print'
|
3
|
+
require 'jinx/resource/merge_visitor'
|
4
|
+
require 'caruby/database/cache'
|
5
5
|
require 'caruby/database/fetched_matcher'
|
6
|
-
require 'caruby/database/
|
6
|
+
require 'caruby/database/reader_template_builder'
|
7
7
|
|
8
8
|
module CaRuby
|
9
9
|
class Database
|
@@ -13,7 +13,7 @@ module CaRuby
|
|
13
13
|
def initialize
|
14
14
|
super
|
15
15
|
# the query template builder
|
16
|
-
@srch_tmpl_bldr =
|
16
|
+
@srch_tmpl_bldr = TemplateBuilder.new
|
17
17
|
# the fetch result matcher
|
18
18
|
@matcher = FetchedMatcher.new
|
19
19
|
# the fetched copier
|
@@ -23,7 +23,7 @@ module CaRuby
|
|
23
23
|
copy
|
24
24
|
end
|
25
25
|
# visitor that merges the fetched object graph
|
26
|
-
@ftchd_mrg_vstr = MergeVisitor.new(:matcher => @matcher, :copier => copier) { |ref| ref.class.fetched_domain_attributes }
|
26
|
+
@ftchd_mrg_vstr = Jinx::MergeVisitor.new(:matcher => @matcher, :copier => copier) { |ref| ref.class.fetched_domain_attributes }
|
27
27
|
end
|
28
28
|
|
29
29
|
# Returns an array of objects matching the specified query template and attribute path.
|
@@ -32,7 +32,7 @@ module CaRuby
|
|
32
32
|
# is a String, then the HQL statement String is executed.
|
33
33
|
#
|
34
34
|
# Otherwise, the query condition is determined by the values set in the template.
|
35
|
-
# The non-nil {
|
35
|
+
# The non-nil {Propertied#searchable_attributes} are used in the query.
|
36
36
|
#
|
37
37
|
# The optional path arguments are attribute symbols from the template to the
|
38
38
|
# destination class, e.g.:
|
@@ -48,9 +48,9 @@ module CaRuby
|
|
48
48
|
# By contrast, caCORE API search result property access, by design, fails with an
|
49
49
|
# obscure exception when the property is not lazy-loaded in Hibernate.
|
50
50
|
#
|
51
|
-
# @param [Resource, String] obj_or_hql the domain object or HQL to query
|
52
|
-
# @param [<
|
53
|
-
# @return [<Resource>] the domain objects which match the query
|
51
|
+
# @param [Jinx::Resource, String] obj_or_hql the domain object or HQL to query
|
52
|
+
# @param [<Property>] path the attribute path to search
|
53
|
+
# @return [<Jinx::Resource>] the domain objects which match the query
|
54
54
|
def query(obj_or_hql, *path)
|
55
55
|
# the detoxified caCORE query result
|
56
56
|
result = query_safe(obj_or_hql, *path)
|
@@ -66,10 +66,10 @@ module CaRuby
|
|
66
66
|
# If the :create option is set, then this method creates an object if the
|
67
67
|
# find is unsuccessful.
|
68
68
|
#
|
69
|
-
# @param [Resource] obj the domain object to find
|
69
|
+
# @param [Jinx::Resource] obj the domain object to find
|
70
70
|
# @param [Hash, Symbol] opts the find options
|
71
71
|
# @option opts [Boolean] :create whether to create the object if it is not found
|
72
|
-
# @return [Resource, nil] the domain object if found, nil otherwise
|
72
|
+
# @return [Jinx::Resource, nil] the domain object if found, nil otherwise
|
73
73
|
# @raise [DatabaseError] if obj is not a domain object or more than object
|
74
74
|
# matches the obj attribute values
|
75
75
|
def find(obj, opts=nil)
|
@@ -88,7 +88,7 @@ module CaRuby
|
|
88
88
|
# Returns whether the given domain object has a database identifier or exists in the database.
|
89
89
|
# This method fetches the object from the database if necessary.
|
90
90
|
#
|
91
|
-
# @param [Resource, <Resource>] obj the domain object(s) to find
|
91
|
+
# @param [Jinx::Resource, <Jinx::Resource>] obj the domain object(s) to find
|
92
92
|
# @return [Boolean] whether the domain object(s) exist in the database
|
93
93
|
def exists?(obj)
|
94
94
|
if obj.nil? then
|
@@ -131,7 +131,7 @@ module CaRuby
|
|
131
131
|
path_s = path.join('.') unless path.empty?
|
132
132
|
# guard against recursive call back into the same operation
|
133
133
|
if query_redundant?(obj_or_hql, path_s) then
|
134
|
-
|
134
|
+
Jinx.fail(DatabaseError, "Query #{obj_or_hql.qp} #{path_s} recursively called in context #{print_operations}")
|
135
135
|
end
|
136
136
|
# perform the query
|
137
137
|
perform(:query, obj_or_hql, :attribute => path_s) { query_with_path(obj_or_hql, path) }
|
@@ -142,7 +142,7 @@ module CaRuby
|
|
142
142
|
end
|
143
143
|
|
144
144
|
def query_subject_redundant?(s1, s2)
|
145
|
-
s1 == s2 or (Resource === s1 and Resource === s2 and s1.identifier and s1.identifier == s2.identifier)
|
145
|
+
s1 == s2 or (Jinx::Resource === s1 and Jinx::Resource === s2 and s1.identifier and s1.identifier == s2.identifier)
|
146
146
|
end
|
147
147
|
|
148
148
|
# @return an array of objects matching the given query template and path
|
@@ -154,7 +154,7 @@ module CaRuby
|
|
154
154
|
# gather the results of querying on those penultimate result objects with the last
|
155
155
|
# attribute as the path
|
156
156
|
unless path.empty? then
|
157
|
-
if attribute.nil? then
|
157
|
+
if attribute.nil? then Jinx.fail(DatabaseError, "Query path includes empty attribute: #{path.join('.')}.nil") end
|
158
158
|
logger.debug { "Decomposing query on #{obj_or_hql} with path #{path.join('.')}.#{attribute} into query on #{path.join('.')} followed by #{attribute}..." }
|
159
159
|
return query_safe(obj_or_hql, *path).map { |parent| query_toxic(parent, attribute) }.flatten
|
160
160
|
end
|
@@ -183,8 +183,15 @@ module CaRuby
|
|
183
183
|
|
184
184
|
# Merges fetched into target. The fetched references are recursively merged.
|
185
185
|
#
|
186
|
-
# @
|
187
|
-
#
|
186
|
+
# @quirk caCORE caCORE does not enforce reference integrity, i.e. if object _a_ has
|
187
|
+
# a reference path _a_.+b+ = _b_ and _b_.+a+ = _a_, then a search on _a_ with path
|
188
|
+
# +:b+ results in the reference path _a_ => _b_ => _a'_, where
|
189
|
+
# _a.identifier_ == _a'.identifier_ but _a_ != _a_'.
|
190
|
+
# This method remedies the caCORE defect by matching source references on a previously
|
191
|
+
# matched identifier where possible.
|
192
|
+
#
|
193
|
+
# @param [Jinx::Resource] source the fetched domain object result
|
194
|
+
# @param [Jinx::Resource] target the domain object find argument
|
188
195
|
def merge_fetched(source, target)
|
189
196
|
@ftchd_mrg_vstr.visit(source, target) { |src, tgt| tgt.copy_volatile_attributes(src) }
|
190
197
|
end
|
@@ -197,7 +204,7 @@ module CaRuby
|
|
197
204
|
|
198
205
|
def query_hql(hql)
|
199
206
|
java_name = hql[/from\s+(\S+)/i, 1]
|
200
|
-
|
207
|
+
Jinx.fail(DatabaseError, "Could not determine target type from HQL: #{hql}") if java_name.nil?
|
201
208
|
tgt = Class.to_ruby(java_name)
|
202
209
|
persistence_service(tgt).query(hql)
|
203
210
|
end
|
@@ -205,16 +212,16 @@ module CaRuby
|
|
205
212
|
# Returns an array of objects fetched from the database which matches
|
206
213
|
# a template and follows the given optional domain attribute, if present.
|
207
214
|
#
|
208
|
-
# The search template is built by {
|
215
|
+
# The search template is built by {TemplateBuilder#build_template}.
|
209
216
|
# If a template could not be built and obj is dependent, then this method
|
210
217
|
# queries the obj owner with a dependent filter.
|
211
218
|
#
|
212
219
|
# @quirk caCORE Bug #79 - API search with only id returns entire table.
|
213
220
|
# Work around this bug by issuing a HQL query instead.
|
214
221
|
#
|
215
|
-
# @param [Resource] obj the query template object
|
222
|
+
# @param [Jinx::Resource] obj the query template object
|
216
223
|
# @param [Symbol, nil] attribute the optional attribute to fetch
|
217
|
-
# @return [<Resource>] the query result
|
224
|
+
# @return [<Jinx::Resource>] the query result
|
218
225
|
def query_object(obj, attribute=nil)
|
219
226
|
if obj.identifier then
|
220
227
|
query_on_identifier(obj, attribute)
|
@@ -248,7 +255,7 @@ module CaRuby
|
|
248
255
|
|
249
256
|
# the join attribute property
|
250
257
|
if attribute then
|
251
|
-
pd = obj.class.
|
258
|
+
pd = obj.class.property(attribute).property_descriptor
|
252
259
|
hql.insert(0, "select #{sa}.#{pd.name} ")
|
253
260
|
end
|
254
261
|
logger.debug { "Querying on #{obj} #{attribute} using HQL identifier criterion..." }
|
@@ -265,10 +272,10 @@ module CaRuby
|
|
265
272
|
# @return [Boolean] whether the query can be inverted
|
266
273
|
def invertible_query?(obj, attribute=nil)
|
267
274
|
return false if attribute.nil?
|
268
|
-
|
269
|
-
return false if
|
270
|
-
|
271
|
-
|
275
|
+
pa = obj.class.property(attribute)
|
276
|
+
return false if pa.type.abstract?
|
277
|
+
inv_prop = pa.inverse_property
|
278
|
+
inv_prop and inv_prop.searchable? and finder_parameters(obj)
|
272
279
|
end
|
273
280
|
|
274
281
|
# Queries the given query object attribute by querying an attribute type template which references obj.
|
@@ -279,18 +286,18 @@ module CaRuby
|
|
279
286
|
#
|
280
287
|
# @param (see #query_object)
|
281
288
|
def query_with_inverted_reference(obj, attribute=nil)
|
282
|
-
|
283
|
-
logger.debug { "Querying on #{obj.qp} #{attribute} by inverting the query as a #{
|
289
|
+
pa = obj.class.property(attribute)
|
290
|
+
logger.debug { "Querying on #{obj.qp} #{attribute} by inverting the query as a #{pa.type.qp} #{pa.inverse} reference query..." }
|
284
291
|
# the search reference template
|
285
292
|
ref = finder_template(obj)
|
286
293
|
# the attribute inverse query template
|
287
|
-
tmpl =
|
294
|
+
tmpl = pa.type.new
|
288
295
|
# the inverse attribute
|
289
|
-
|
296
|
+
inv_prop = tmpl.class.property(pa.inverse)
|
290
297
|
# The Java property writer to set the tmpl inverse to ref.
|
291
298
|
# Use the property writer rather than the attribute writer in order to curtail automatically
|
292
|
-
# adding tmpl to the ref attribute value when the
|
293
|
-
wtr =
|
299
|
+
# adding tmpl to the ref attribute value when the inv_prop attribute is set to ref.
|
300
|
+
wtr = inv_prop.property_writer
|
294
301
|
# parameterize tmpl with inverse ref
|
295
302
|
tmpl.send(wtr, ref)
|
296
303
|
# submit the query
|
@@ -312,7 +319,7 @@ module CaRuby
|
|
312
319
|
# so it is done manually here.
|
313
320
|
#
|
314
321
|
# @param obj (see #find)
|
315
|
-
# @return [Resource, nil] obj if there is a matching database record, nil otherwise
|
322
|
+
# @return [Jinx::Resource, nil] obj if there is a matching database record, nil otherwise
|
316
323
|
# @raise [DatabaseError] if more than object matches the obj attribute values or if
|
317
324
|
# the search object is a dependent entity that does not reference an owner
|
318
325
|
def find_object(obj)
|
@@ -339,7 +346,12 @@ module CaRuby
|
|
339
346
|
#
|
340
347
|
# @see #find_object
|
341
348
|
def fetch_object(obj)
|
342
|
-
#
|
349
|
+
# If there is an identifier, then work around the caCORE identifier query bug by delegating
|
350
|
+
# to the HQL identifier query.
|
351
|
+
if obj.identifier then
|
352
|
+
return query_on_identifier(obj).first
|
353
|
+
end
|
354
|
+
# Make the finder template with key attributes.
|
343
355
|
tmpl = finder_template(obj)
|
344
356
|
# If a template could be made, then fetch on the template.
|
345
357
|
# Otherwise, if there is an owner, then match on the fetched owner dependents.
|
@@ -361,45 +373,43 @@ module CaRuby
|
|
361
373
|
msg = "More than one match for #{obj.class.qp} find with template #{template}."
|
362
374
|
# it is an error to have an ambiguous result
|
363
375
|
logger.error("Fetch error - #{msg}:\n#{obj}")
|
364
|
-
|
376
|
+
Jinx.fail(DatabaseError, msg)
|
365
377
|
end
|
366
378
|
|
367
379
|
result.first
|
368
380
|
end
|
369
381
|
|
370
|
-
# If
|
371
|
-
#
|
382
|
+
# If the given domain object is a dependent with an unfetched owner, then this method fetches
|
383
|
+
# the owner and attempts to match the owner dependent to this object.
|
372
384
|
#
|
373
|
-
# @param [Resource] the domain object to fetch
|
374
|
-
# @return [Resource, nil] the domain object if it matches a dependent, nil otherwise
|
385
|
+
# @param [Jinx::Resource] obj the domain object to fetch
|
386
|
+
# @return [Jinx::Resource, nil] the domain object if it matches a dependent, nil otherwise
|
375
387
|
def fetch_object_by_fetching_owner(obj)
|
376
388
|
owner = nil
|
377
|
-
oattr = obj.class.owner_attributes.detect { |
|
378
|
-
return
|
389
|
+
oattr = obj.class.owner_attributes.detect { |pa| owner = obj.send(pa) }
|
390
|
+
return if owner.nil? or owner.fetched?
|
379
391
|
|
380
|
-
logger.debug { "Querying #{obj.qp} by matching on the owner #{owner.qp}
|
381
|
-
|
382
|
-
if
|
383
|
-
|
392
|
+
logger.debug { "Querying #{obj.qp} by matching on the #{oattr} owner #{owner.qp} dependents..." }
|
393
|
+
inv_prop = obj.class.property(oattr)
|
394
|
+
if inv_prop.nil? then
|
395
|
+
Jinx.fail(DatabaseError, "#{dep.class.qp} owner attribute #{oattr} does not have a #{owner.class.qp} inverse dependent attribute.")
|
384
396
|
end
|
385
|
-
|
397
|
+
inv = inv_prop.inverse
|
386
398
|
# fetch the owner if necessary
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
return obj
|
393
|
-
end
|
399
|
+
find(owner) || return
|
400
|
+
# if obj dependent was fetched with owner, then done
|
401
|
+
if obj.identifier then
|
402
|
+
logger.debug { "Found #{obj.qp} by fetching the owner #{owner}." }
|
403
|
+
return obj
|
394
404
|
end
|
395
405
|
|
396
406
|
# try to match a fetched owner dependent
|
397
|
-
deps = lazy_loader.enable { owner.send(
|
407
|
+
deps = lazy_loader.enable { owner.send(inv) }
|
398
408
|
if obj.identifier then
|
399
|
-
logger.debug { "Found #{obj.qp} by fetching the owner #{owner} #{
|
409
|
+
logger.debug { "Found #{obj.qp} by fetching the owner #{owner} #{inv} dependents." }
|
400
410
|
return obj
|
401
411
|
else
|
402
|
-
logger.debug { "#{obj.qp} does not match
|
412
|
+
logger.debug { "#{obj.qp} does not match one of the fetched owner #{owner} #{inv} dependents #{deps}." }
|
403
413
|
nil
|
404
414
|
end
|
405
415
|
end
|
@@ -415,7 +425,8 @@ module CaRuby
|
|
415
425
|
@srch_tmpl_bldr.build_template(obj, hash)
|
416
426
|
end
|
417
427
|
|
418
|
-
# Fetches the given
|
428
|
+
# Fetches the given object attribute value from the database.
|
429
|
+
#
|
419
430
|
# @quirk caCORE there is no association fetch for caCORE 3.1 and earlier;
|
420
431
|
# caCORE 4 association search is not yet adequately proven in caRuby testing.
|
421
432
|
# Fall back on a general query instead (the devil we know). See also the
|
@@ -432,35 +443,46 @@ module CaRuby
|
|
432
443
|
# the detoxified copy loses reference integrity. E.g. a query on the children attribute of
|
433
444
|
# a parent object forces lazy load of each child => parent reference separately resolving
|
434
445
|
# in separate parent copies. There is no recognition that the children reference the parent
|
435
|
-
# which generated the query. This anomaly
|
436
|
-
# method by setting the fetched objects inverse to the given search target object
|
437
|
-
#
|
438
|
-
#
|
439
|
-
#
|
440
|
-
#
|
441
|
-
#
|
442
|
-
#
|
446
|
+
# which generated the query. This anomaly cannot be rectified in this fetch_association
|
447
|
+
# method by setting the fetched objects inverse to the given search target object, since
|
448
|
+
# this method does not modify the search target. This hazardous and inefficient caCORE
|
449
|
+
# behavior is partially rectified by setting the fetched object inverse references to
|
450
|
+
# a copy of the search target. If the search target has been fetched, then it is cached.
|
451
|
+
# Therefore, calling {Persistifier#persistify} on the fetched objects will reconcile
|
452
|
+
# the referenced search target copy with the search target. This reconciliation is done
|
453
|
+
# by the caller, e.g. the lazy loader.
|
454
|
+
#
|
455
|
+
# @param [Jinx::Resource] obj the search target object
|
443
456
|
# @param [Symbol] attribute the association to fetch
|
457
|
+
# @return [Jinx::Resource, <Jinx::Resource>, nil] the attribute value
|
444
458
|
# @raise [DatabaseError] if the search target object does not have an identifier
|
445
459
|
def fetch_association(obj, attribute)
|
446
460
|
logger.debug { "Fetching association #{attribute} for #{obj}..." }
|
447
461
|
# load the object if necessary
|
448
462
|
unless exists?(obj) then
|
449
|
-
|
463
|
+
Jinx.fail(DatabaseError, "Can't fetch an association since the referencing object is not found in the database: #{obj}")
|
450
464
|
end
|
451
465
|
# fetch the reference
|
452
466
|
result = query_safe(obj, attribute)
|
453
|
-
#
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
467
|
+
# Set the inverse to a place-holder copy of the search target.
|
468
|
+
# If this method is called by a lazy loader, then the caller will
|
469
|
+
# subsequently reconcile the reference with the cached search target.
|
470
|
+
prop = obj.class.property(attribute)
|
471
|
+
ip = prop.inverse_property
|
472
|
+
if ip and not ip.collection? then
|
473
|
+
place_holder = obj.copy(:identifier)
|
474
|
+
result.each { |ref| ref.send(ip.writer, place_holder) }
|
461
475
|
end
|
462
|
-
#
|
463
|
-
|
476
|
+
# Unbracket the result if the search propery is not a collection.
|
477
|
+
prop.collection? ? result : result.first
|
478
|
+
end
|
479
|
+
|
480
|
+
# Fetches the given object attribute reference from the database and sets the property value.
|
481
|
+
#
|
482
|
+
# @param (see #fetch_association)
|
483
|
+
# @return (see #fetch_association)
|
484
|
+
def load_association(obj, attribute)
|
485
|
+
obj.set_property_value(attribute, fetch_association(obj, attribute))
|
464
486
|
end
|
465
487
|
|
466
488
|
# @return [{Symbol => Object}, nil] the find operation key attributes, or nil if there is no complete key
|
@@ -481,7 +503,7 @@ module CaRuby
|
|
481
503
|
# the key must be non-trivial
|
482
504
|
return if attributes.nil_or_empty?
|
483
505
|
# the attribute => value hash
|
484
|
-
attributes.to_compact_hash { |
|
506
|
+
attributes.to_compact_hash { |pa| finder_parameter(obj, pa) or return }
|
485
507
|
end
|
486
508
|
|
487
509
|
# @return a non-empty, existing find parameter for the given attribute
|
@@ -508,10 +530,10 @@ module CaRuby
|
|
508
530
|
# in the database.
|
509
531
|
#
|
510
532
|
# Raises DatabaseError if the value is nil.
|
511
|
-
def finder_attribute_value_exists?(obj,
|
512
|
-
value = obj.send(
|
533
|
+
def finder_attribute_value_exists?(obj, pa)
|
534
|
+
value = obj.send(pa)
|
513
535
|
return false if value.nil?
|
514
|
-
obj.class.nondomain_attribute?(
|
536
|
+
obj.class.nondomain_attribute?(pa) or value.identifier
|
515
537
|
end
|
516
538
|
|
517
539
|
# Sets the template attribute to a new search reference object created from source.
|
@@ -520,12 +542,12 @@ module CaRuby
|
|
520
542
|
# @quirk caCORE The search template must break inverse integrity by clearing an owner inverse reference,
|
521
543
|
# since a dependent => onwer => dependent cycle causes a caCORE search infinite loop.
|
522
544
|
#
|
523
|
-
# @return [Resource, nil] the search reference, or nil if source does not exist in the database
|
545
|
+
# @return [Jinx::Resource, nil] the search reference, or nil if source does not exist in the database
|
524
546
|
def add_search_template_reference(template, source, attribute)
|
525
547
|
return if not exists?(source)
|
526
548
|
ref = source.copy(:identifier)
|
527
|
-
template.
|
528
|
-
inverse = template.class.
|
549
|
+
template.set_property_value(attribute, ref)
|
550
|
+
inverse = template.class.property(attribute).derived_inverse
|
529
551
|
ref.clear_attribute(inverse) if inverse
|
530
552
|
logger.debug { "Search reference parameter #{attribute} for #{template.qp} set to #{ref} copied from #{source.qp}" }
|
531
553
|
ref
|