caruby-core 1.4.2 → 1.4.3
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +10 -0
- data/lib/caruby/cli/command.rb +10 -8
- data/lib/caruby/database/fetched_matcher.rb +28 -39
- data/lib/caruby/database/lazy_loader.rb +101 -0
- data/lib/caruby/database/persistable.rb +190 -167
- data/lib/caruby/database/persistence_service.rb +21 -7
- data/lib/caruby/database/persistifier.rb +185 -0
- data/lib/caruby/database/reader.rb +106 -176
- data/lib/caruby/database/saved_matcher.rb +56 -0
- data/lib/caruby/database/search_template_builder.rb +1 -1
- data/lib/caruby/database/sql_executor.rb +8 -7
- data/lib/caruby/database/store_template_builder.rb +134 -61
- data/lib/caruby/database/writer.rb +252 -52
- data/lib/caruby/database.rb +88 -67
- data/lib/caruby/domain/attribute_initializer.rb +16 -0
- data/lib/caruby/domain/attribute_metadata.rb +161 -72
- data/lib/caruby/domain/id_alias.rb +22 -0
- data/lib/caruby/domain/inversible.rb +91 -0
- data/lib/caruby/domain/merge.rb +116 -35
- data/lib/caruby/domain/properties.rb +1 -1
- data/lib/caruby/domain/reference_visitor.rb +207 -71
- data/lib/caruby/domain/resource_attributes.rb +93 -80
- data/lib/caruby/domain/resource_dependency.rb +22 -97
- data/lib/caruby/domain/resource_introspection.rb +21 -28
- data/lib/caruby/domain/resource_inverse.rb +134 -0
- data/lib/caruby/domain/resource_metadata.rb +41 -19
- data/lib/caruby/domain/resource_module.rb +42 -33
- data/lib/caruby/import/java.rb +8 -9
- data/lib/caruby/migration/migrator.rb +20 -7
- data/lib/caruby/migration/resource_module.rb +0 -2
- data/lib/caruby/resource.rb +132 -351
- data/lib/caruby/util/cache.rb +4 -1
- data/lib/caruby/util/class.rb +48 -1
- data/lib/caruby/util/collection.rb +54 -18
- data/lib/caruby/util/inflector.rb +7 -0
- data/lib/caruby/util/options.rb +35 -31
- data/lib/caruby/util/partial_order.rb +1 -1
- data/lib/caruby/util/properties.rb +2 -2
- data/lib/caruby/util/stopwatch.rb +16 -8
- data/lib/caruby/util/transitive_closure.rb +1 -1
- data/lib/caruby/util/visitor.rb +342 -328
- data/lib/caruby/version.rb +1 -1
- data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
- data/lib/caruby.rb +2 -0
- metadata +10 -9
- data/lib/caruby/database/saved_merger.rb +0 -131
- data/lib/caruby/domain/annotatable.rb +0 -25
- data/lib/caruby/domain/annotation.rb +0 -23
- data/lib/caruby/import/annotatable_class.rb +0 -28
- data/lib/caruby/import/annotation_class.rb +0 -27
- data/lib/caruby/import/annotation_module.rb +0 -67
- data/lib/caruby/migration/resource.rb +0 -8
@@ -13,7 +13,12 @@ module CaRuby
|
|
13
13
|
# The {Util::Stopwatch} which captures the time spent in database operations performed by the application service.
|
14
14
|
attr_reader :timer
|
15
15
|
|
16
|
-
# Creates a new PersistenceService with the specified application service name.
|
16
|
+
# Creates a new PersistenceService with the specified application service name and options.
|
17
|
+
#
|
18
|
+
# @param [String] the caBIG application service name
|
19
|
+
# @param [{Symbol => Object}] opts the options
|
20
|
+
# @option opts :host the service host (default +localhost+)
|
21
|
+
# @option opts :version the caTissue version identifier
|
17
22
|
def initialize(name, opts={})
|
18
23
|
@name = name
|
19
24
|
ver_opt = opts[:version]
|
@@ -74,7 +79,7 @@ module CaRuby
|
|
74
79
|
end
|
75
80
|
end
|
76
81
|
|
77
|
-
#
|
82
|
+
# @return [ApplicationServiceProvider] the CaCORE service provider wrapped by this PersistenceService
|
78
83
|
def app_service
|
79
84
|
url = "http://#{@host}:8080/#{name}/http/remoteService"
|
80
85
|
logger.debug { "Connecting to service provider at #{url}..." }
|
@@ -87,15 +92,24 @@ module CaRuby
|
|
87
92
|
ASSOCIATION_SUPPORT_VERSION = "4".to_version
|
88
93
|
|
89
94
|
# Calls the block given to this method. The execution duration is captured in the {#timer}.
|
90
|
-
#
|
91
|
-
|
95
|
+
#
|
96
|
+
# @return the block result
|
97
|
+
def time
|
92
98
|
result = nil
|
93
|
-
seconds = @timer.run { result = yield
|
99
|
+
seconds = @timer.run { result = yield }.elapsed
|
94
100
|
millis = (seconds * 1000).round
|
95
101
|
logger.debug { "Database operation took #{millis} milliseconds." }
|
96
102
|
result
|
97
103
|
end
|
98
104
|
|
105
|
+
# Calls the block given to this method on the #{app_service}.
|
106
|
+
# The execution duration is captured in the {#timer}.
|
107
|
+
#
|
108
|
+
# @return the block result
|
109
|
+
def dispatch
|
110
|
+
time { yield app_service }
|
111
|
+
end
|
112
|
+
|
99
113
|
def query_hql(hql)
|
100
114
|
logger.debug { "Building HQLCriteria..." }
|
101
115
|
criteria = HQLCriteria.new(hql)
|
@@ -103,7 +117,7 @@ module CaRuby
|
|
103
117
|
# TODO caCORE 4 - remove target parameter
|
104
118
|
target = hql[/from\s+(\S+)/i, 1]
|
105
119
|
raise DatabaseError.new("HQL does not contain a FROM clause: #{hql}") unless target
|
106
|
-
logger.debug { "
|
120
|
+
logger.debug { "Submitting search on target class #{target} with the following HQL:\n #{hql}" }
|
107
121
|
begin
|
108
122
|
dispatch { |svc| svc.query(criteria, target) }
|
109
123
|
rescue Exception => e
|
@@ -128,7 +142,7 @@ module CaRuby
|
|
128
142
|
# the caCORE app service search path is in reverse path traversal order (go figure!)
|
129
143
|
reverse_class_name_path = class_name_path.reverse << template.java_class
|
130
144
|
# call the caCORE app service search
|
131
|
-
logger.debug { "
|
145
|
+
logger.debug { "Submitting search with template #{template.qp}, target-first class path #{reverse_class_name_path.pp_s(:single_line)}, criterion:\n#{dump(template)}" }
|
132
146
|
begin
|
133
147
|
dispatch { |svc| svc.search(reverse_class_name_path.join(','), template) }
|
134
148
|
rescue Exception => e
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'caruby/database/persistable'
|
2
|
+
require 'caruby/database/lazy_loader'
|
3
|
+
|
4
|
+
module CaRuby
|
5
|
+
class Database
|
6
|
+
# @return [LazyLoader] this database's lazy loader
|
7
|
+
attr_reader :lazy_loader
|
8
|
+
|
9
|
+
# Database Persistable mediator.
|
10
|
+
module Persistifier
|
11
|
+
# Adds query capability to this Database.
|
12
|
+
def initialize
|
13
|
+
super
|
14
|
+
@ftchd_vstr = ReferenceVisitor.new { |ref| ref.class.fetched_domain_attributes }
|
15
|
+
# the demand loader
|
16
|
+
@lazy_loader = LazyLoader.new { |obj, attr| lazy_load(obj, attr) }
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Adds this database's lazy loader to the given domain object.
|
22
|
+
#
|
23
|
+
# @param [Resource] obj the domain object to lazy-load
|
24
|
+
def add_lazy_loader(obj, attributes=nil)
|
25
|
+
obj.add_lazy_loader(@lazy_loader, attributes)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Loads the content of the given attribute.
|
29
|
+
# The fetched references are persistified with {#persistify}.
|
30
|
+
#
|
31
|
+
# @param [Resource] obj the domain object whose content is to be loaded
|
32
|
+
# @param [Symbol] attribute the attribute to load
|
33
|
+
# @return [Resource, <Resource>, nil] the loaded value
|
34
|
+
def lazy_load(obj, attribute)
|
35
|
+
fetched = fetch_association(obj, attribute)
|
36
|
+
reconcile_fetched(fetched) if fetched
|
37
|
+
end
|
38
|
+
|
39
|
+
# For each fetched domain object, if there is a corresponding cached object,
|
40
|
+
# then the reconciled value is that cached object. Otherwise, the reconciled
|
41
|
+
# object is the persistified fetched object.
|
42
|
+
#
|
43
|
+
# @param [Resource, <Resource>] fetched the fetched domain object(s)
|
44
|
+
# @return [Resource, <Resource>] the reconciled domain object(s)
|
45
|
+
def reconcile_fetched(fetched)
|
46
|
+
if Enumerable === fetched then
|
47
|
+
fetched.map { |ref| reconcile_fetched(ref) }
|
48
|
+
else
|
49
|
+
reconcile_cached(fetched) or persistify(fetched)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [Resource] fetched the fetched domain object
|
54
|
+
# @return [Resource] the corresponding cached object, if cached,
|
55
|
+
# otherwise the fetched object
|
56
|
+
def reconcile_cached(fetched)
|
57
|
+
cached = @cache[fetched]
|
58
|
+
if cached then
|
59
|
+
logger.debug { "Replaced fetched #{fetched} with cached #{cached}." }
|
60
|
+
end
|
61
|
+
cached
|
62
|
+
end
|
63
|
+
|
64
|
+
# caCORE alert - Dereferencing a caCORE search result uncascaded collection attribute
|
65
|
+
# raises a Hibernate missing session error.
|
66
|
+
# This problem is addressed by post-processing the +caCORE+ search result to set the
|
67
|
+
# toxic attributes to an empty value.
|
68
|
+
#
|
69
|
+
# caCORE alert - The caCORE search result does not set the obvious inverse attributes,
|
70
|
+
# e.g. children fetched with a parent do not have the children inverse parent attribute
|
71
|
+
# set to the parent. Rather, it is a toxic caCORE reference which must be purged. This
|
72
|
+
# leaves an empty reference which must be lazy-loaded, which is inefficient and inconsistent.
|
73
|
+
# This situation is rectified in this detoxify method by setting the dependent owner
|
74
|
+
# attribute to the fetched owner in the detoxification {ReferenceVisitor} copy-match-merge.
|
75
|
+
#
|
76
|
+
# This method copies each result domain object into a new object of the same type.
|
77
|
+
# The copy nondomain attribute values are set to the fetched object values.
|
78
|
+
# The copy fetched reference attribute values are set to a copy of the result references.
|
79
|
+
#
|
80
|
+
# @return [Resource, <Resource>] the detoxified object(s)
|
81
|
+
def detoxify(toxic)
|
82
|
+
return if toxic.nil?
|
83
|
+
if toxic.collection? then
|
84
|
+
toxic.each { |obj| detoxify(obj) }
|
85
|
+
else
|
86
|
+
logger.debug { "Detoxifying the toxic caCORE result #{toxic.qp}..." }
|
87
|
+
@ftchd_vstr.visit(toxic) { |ref| clear_toxic_attributes(ref) }
|
88
|
+
end
|
89
|
+
toxic
|
90
|
+
end
|
91
|
+
|
92
|
+
# Sets each of the toxic attributes in the given domain object to the corresponding
|
93
|
+
# {ResourceMetadata#empty_value}.
|
94
|
+
#
|
95
|
+
# @param [Resource] toxic the toxic domain object
|
96
|
+
def clear_toxic_attributes(toxic)
|
97
|
+
attrs = toxic.class.toxic_attributes
|
98
|
+
return if attrs.empty?
|
99
|
+
logger.debug { "Clearing toxic #{toxic.qp} attributes #{attrs.to_series}..." }
|
100
|
+
attrs.each_pair do |attr, attr_md|
|
101
|
+
# skip non-Java attributes
|
102
|
+
next unless attr_md.java_property?
|
103
|
+
# the empty or nil value to set
|
104
|
+
value = toxic.class.empty_value(attr)
|
105
|
+
# Use the Java writer method rather than the standard attribute writer method.
|
106
|
+
# The standard attribute writer enforces inverse integrity, which potential requires
|
107
|
+
# accessing the current toxic value. The Java writer bypasses inverse integrity.
|
108
|
+
reader, writer = attr_md.property_accessors
|
109
|
+
# clear the attribute
|
110
|
+
toxic.send(writer, value)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Persistifies the given domain object and all of its dependents. Sets the inverses
|
115
|
+
# using #{#set_inverses} to enforce inverse integrity.
|
116
|
+
#
|
117
|
+
# @param (see #persistify_object)
|
118
|
+
# @raise [ArgumentError] if obj is a collection and other is not nil
|
119
|
+
def persistify(obj, other=nil)
|
120
|
+
if obj.collection? then
|
121
|
+
if other then raise ArgumentError.new("Database reader persistify other argument not supported") end
|
122
|
+
obj.each { |ref| persistify(ref) }
|
123
|
+
return obj
|
124
|
+
end
|
125
|
+
# set the inverses before recursing to dependents
|
126
|
+
set_inverses(obj)
|
127
|
+
# recurse to dependents before adding a lazy loader to the owner
|
128
|
+
obj.each_dependent { |dep| persistify(dep) if dep.identifier }
|
129
|
+
persistify_object(obj, other)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Takes a {Persistable#snapshot} of obj to track changes, adds a lazy loader and
|
133
|
+
# adds the object to the cache.
|
134
|
+
#
|
135
|
+
# If the other fetched source object is given, then the obj snapshot is updated
|
136
|
+
# with the non-nil values from other.
|
137
|
+
#
|
138
|
+
# @param [Resource] obj the domain object to make persistable
|
139
|
+
# @param [Resource] other the source domain object
|
140
|
+
# @return [Resource] obj
|
141
|
+
def persistify_object(obj, other=nil)
|
142
|
+
# take a snapshot of the database content
|
143
|
+
snapshot(obj, other)
|
144
|
+
# add lazy loader to the unfetched attributes
|
145
|
+
add_lazy_loader(obj)
|
146
|
+
# add to the cache
|
147
|
+
@cache.add(obj)
|
148
|
+
obj
|
149
|
+
end
|
150
|
+
|
151
|
+
# Sets each inversible domain attribute reference inverse to the given domain object.
|
152
|
+
# For each inversible domain attribute, if the attribute inverse is a collection,
|
153
|
+
# then obj is added to the inverse collection. Otherwise, the inverse attribute
|
154
|
+
# is set to obj.
|
155
|
+
#
|
156
|
+
# @param obj (see #persistify_object)
|
157
|
+
def set_inverses(obj)
|
158
|
+
obj.class.domain_attributes.each_pair do |attr, attr_md|
|
159
|
+
inv_md = attr_md.inverse_attribute_metadata || next
|
160
|
+
if inv_md.collection? then
|
161
|
+
obj.send(attr).enumerate { |ref| ref.send(inv_md.to_sym) << obj }
|
162
|
+
else
|
163
|
+
obj.send(attr).enumerate { |ref| ref.set_attribute(inv_md.to_sym, obj) }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Take a snapshot of the current object state.
|
169
|
+
# If the other fetched object is given, then merge the fetched non-domain attribute
|
170
|
+
# values into the obj snapshot, replacing an existing obj non-domain value with the
|
171
|
+
# corresponding other attribute value if and only if the other attribute value is non-nil.
|
172
|
+
#
|
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
|
176
|
+
def snapshot(obj, other=nil)
|
177
|
+
# take a fresh snapshot
|
178
|
+
obj.take_snapshot
|
179
|
+
logger.debug { "Snapshot taken of #{obj.qp}." }
|
180
|
+
# merge the other object content if available
|
181
|
+
obj.merge_into_snapshot(other) if other
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -12,17 +12,10 @@ module CaRuby
|
|
12
12
|
# Adds query capability to this Database.
|
13
13
|
def initialize
|
14
14
|
super
|
15
|
-
# the demand loader
|
16
|
-
@lazy_loader = lambda { |obj, attr| lazy_load(obj, attr) }
|
17
15
|
# the query template builder
|
18
16
|
@srch_tmpl_bldr = SearchTemplateBuilder.new(self)
|
19
17
|
# the fetch result matcher
|
20
18
|
@matcher = FetchedMatcher.new
|
21
|
-
|
22
|
-
# cache not yet tested - TODO: test and replace copier below with cacher
|
23
|
-
# the fetched object cacher
|
24
|
-
#cacher = Proc.new { |src| @cache[src] }
|
25
|
-
|
26
19
|
# the fetched copier
|
27
20
|
copier = Proc.new do |src|
|
28
21
|
copy = src.copy
|
@@ -30,10 +23,7 @@ module CaRuby
|
|
30
23
|
copy
|
31
24
|
end
|
32
25
|
# visitor that merges the fetched object graph
|
33
|
-
@
|
34
|
-
@ftchd_mrg_vstr = MergeVisitor.new(:matcher => @matcher, :copier => copier) { |src, tgt| tgt.class.fetched_domain_attributes }
|
35
|
-
# visitor that copies the fetched object graph
|
36
|
-
@detoxifier = CopyVisitor.new(:copier => copier) { |src, tgt| src.class.fetched_domain_attributes }
|
26
|
+
@ftchd_mrg_vstr = MergeVisitor.new(:matcher => @matcher, :copier => copier) { |ref| ref.class.fetched_domain_attributes }
|
37
27
|
end
|
38
28
|
|
39
29
|
# Returns an array of objects matching the specified query template and attribute path.
|
@@ -68,41 +58,6 @@ module CaRuby
|
|
68
58
|
persistify(result)
|
69
59
|
end
|
70
60
|
|
71
|
-
# Queries the given obj_or_hql as described in {#query} and makes a detoxified copy of the
|
72
|
-
# toxic caCORE search result.
|
73
|
-
#
|
74
|
-
# caCORE alert - The query result consists of new domain objects whose content is copied
|
75
|
-
# from the caBIG application search result. The caBIG result is Hibernate-enhanced but
|
76
|
-
# sessionless. This result contains toxic broken objects whose access methods fail.
|
77
|
-
# Therefore, this method sanitizes the toxic caBIG result to reflect the persistent state
|
78
|
-
# of the domain objects. Persistent references are loaded on demand from the database if
|
79
|
-
# necessary.
|
80
|
-
#
|
81
|
-
# @param (see #query)
|
82
|
-
# @return (see #query)
|
83
|
-
def query_safe(obj_or_hql, *path)
|
84
|
-
# the caCORE search result
|
85
|
-
toxic = query_toxic(obj_or_hql, *path)
|
86
|
-
logger.debug { "Copying caCORE query toxic #{toxic.qp}..." } unless toxic.empty?
|
87
|
-
# detoxify the toxic caCORE result
|
88
|
-
detoxify(toxic)
|
89
|
-
end
|
90
|
-
|
91
|
-
# Queries the given obj_or_hql as described in {#query} and returns the toxic caCORE search result.
|
92
|
-
#
|
93
|
-
# @param (see #query)
|
94
|
-
# @return (see #query)
|
95
|
-
def query_toxic(obj_or_hql, *path)
|
96
|
-
# the attribute path as a string
|
97
|
-
path_s = path.join('.') unless path.empty?
|
98
|
-
# guard against recursive call back into the same operation
|
99
|
-
if query_redundant?(obj_or_hql, path_s) then
|
100
|
-
raise DatabaseError.new("Query #{obj_or_hql.qp} #{path_s} recursively called in context #{print_operations}")
|
101
|
-
end
|
102
|
-
# perform the query
|
103
|
-
perform(:query, obj_or_hql, path_s) { query_with_path(obj_or_hql, path) }
|
104
|
-
end
|
105
|
-
|
106
61
|
# Fetches the given domain object from the database.
|
107
62
|
# Only secondary key attributes are used in the match.
|
108
63
|
# If no secondary key is defined for the object's class, then this method returns nil.
|
@@ -142,17 +97,44 @@ module CaRuby
|
|
142
97
|
obj.identifier or (obj.searchable? and find(obj))
|
143
98
|
end
|
144
99
|
end
|
145
|
-
|
100
|
+
|
146
101
|
private
|
147
102
|
|
148
|
-
RESULT_PRINTER = PrintWrapper.new { |obj| obj.
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
103
|
+
RESULT_PRINTER = PrintWrapper.new { |obj| obj.pp_s }
|
104
|
+
|
105
|
+
# Queries the given obj_or_hql as described in {#query} and makes a detoxified copy of the
|
106
|
+
# toxic caCORE search result.
|
107
|
+
#
|
108
|
+
# caCORE alert - The query result consists of new domain objects whose content is copied
|
109
|
+
# from the caBIG application search result. The caBIG result is Hibernate-enhanced but
|
110
|
+
# sessionless. This result contains toxic broken objects whose access methods fail.
|
111
|
+
# Therefore, this method sanitizes the toxic caBIG result to reflect the persistent state
|
112
|
+
# of the domain objects.
|
113
|
+
#
|
114
|
+
# @param (see #query)
|
115
|
+
# @return (see #query)
|
116
|
+
def query_safe(obj_or_hql, *path)
|
117
|
+
# the caCORE search result
|
118
|
+
toxic = query_toxic(obj_or_hql, *path)
|
119
|
+
# detoxify the toxic caCORE result
|
120
|
+
detoxify(toxic)
|
154
121
|
end
|
155
122
|
|
123
|
+
# Queries the given obj_or_hql as described in {#query} and returns the toxic caCORE search result.
|
124
|
+
#
|
125
|
+
# @param (see #query)
|
126
|
+
# @return (see #query)
|
127
|
+
def query_toxic(obj_or_hql, *path)
|
128
|
+
# the attribute path as a string
|
129
|
+
path_s = path.join('.') unless path.empty?
|
130
|
+
# guard against recursive call back into the same operation
|
131
|
+
if query_redundant?(obj_or_hql, path_s) then
|
132
|
+
raise DatabaseError.new("Query #{obj_or_hql.qp} #{path_s} recursively called in context #{print_operations}")
|
133
|
+
end
|
134
|
+
# perform the query
|
135
|
+
perform(:query, obj_or_hql, :attribute => path_s) { query_with_path(obj_or_hql, path) }
|
136
|
+
end
|
137
|
+
|
156
138
|
def query_redundant?(obj_or_hql, path)
|
157
139
|
@operations.detect { |op| op.type == :query and query_subject_redundant?(op.subject, obj_or_hql) and op.attribute == path }
|
158
140
|
end
|
@@ -183,9 +165,9 @@ module CaRuby
|
|
183
165
|
def query_with_attribute(obj_or_hql, attribute=nil)
|
184
166
|
toxic = if String === obj_or_hql then
|
185
167
|
hql = obj_or_hql
|
186
|
-
# if there is an attribute, then compose
|
168
|
+
# if there is an attribute, then compose an hql query with a recursive object query
|
187
169
|
if attribute then
|
188
|
-
query_safe(hql).map { |
|
170
|
+
query_safe(hql).map { |ref| query_with_attribute(ref, attribute) }.flatten
|
189
171
|
else
|
190
172
|
query_hql(hql)
|
191
173
|
end
|
@@ -196,33 +178,13 @@ module CaRuby
|
|
196
178
|
logger.debug { print_query_result(toxic) }
|
197
179
|
toxic
|
198
180
|
end
|
199
|
-
|
200
|
-
# caCORE alert - post-process the +caCORE+ search result to fix the following problem:
|
201
|
-
# * de-referencing a search result domain object raises a Hibernate missing session error
|
202
|
-
#
|
203
|
-
# caCORE alert - The caCORE search result does not set the obvious inverse attributes,
|
204
|
-
# e.g. the children fetched with a parent do not have the children inverse parent attribute
|
205
|
-
# set to the parent. Rather, it is a toxic caCORE reference which must be purged. This
|
206
|
-
# leaves an empty reference which must be lazy-loaded, which is inefficient and inconsistent.
|
207
|
-
# This situation is rectified in this detoxify method by setting the dependent owner
|
208
|
-
# attribute to the fetched owner in the detoxification {ReferenceVisitor} copy-match-merge.
|
209
|
-
#
|
210
|
-
# This method copies each result domain object into a new object of the same type.
|
211
|
-
# The copy nondomain attribute values are set to the fetched object values.
|
212
|
-
# The copy fetched reference attribute values are set to a copy of the result references.
|
213
|
-
#
|
214
|
-
# Returns the detoxified copy.
|
215
|
-
def detoxify(toxic)
|
216
|
-
return toxic.map { |obj| detoxify(obj) } if toxic.collection?
|
217
|
-
@detoxifier.visit(toxic)
|
218
|
-
end
|
219
|
-
|
181
|
+
|
220
182
|
# Merges fetched into target. The fetched references are recursively merged.
|
221
183
|
#
|
222
|
-
# @param [Resource] target the domain object find argument
|
223
184
|
# @param [Resource] source the fetched domain object result
|
224
|
-
|
225
|
-
|
185
|
+
# @param [Resource] target the domain object find argument
|
186
|
+
def merge_fetched(source, target)
|
187
|
+
@ftchd_mrg_vstr.visit(source, target) { |src, tgt| tgt.copy_volatile_attributes(src) }
|
226
188
|
end
|
227
189
|
|
228
190
|
def print_query_result(result)
|
@@ -253,12 +215,10 @@ module CaRuby
|
|
253
215
|
# @param [Symbol, nil] attribute the optional attribute to fetch
|
254
216
|
# @return [<Resource>] the query result
|
255
217
|
def query_object(obj, attribute=nil)
|
256
|
-
|
257
|
-
# caCORE alert - search with attribute ignores id (cf. caTissue Bug #79);
|
258
|
-
# inverted query is safer if possible
|
259
|
-
query_with_inverted_reference(obj, attribute)
|
260
|
-
elsif obj.identifier then
|
218
|
+
if obj.identifier then
|
261
219
|
query_on_identifier(obj, attribute)
|
220
|
+
elsif invertible_query?(obj, attribute) then
|
221
|
+
query_with_inverted_reference(obj, attribute)
|
262
222
|
else
|
263
223
|
tmpl = @srch_tmpl_bldr.build_template(obj)
|
264
224
|
return Array::EMPTY_ARRAY if tmpl.nil?
|
@@ -274,8 +234,10 @@ module CaRuby
|
|
274
234
|
attribute ? service.query(template, attribute) : service.query(template)
|
275
235
|
end
|
276
236
|
|
277
|
-
# Queries the given
|
278
|
-
|
237
|
+
# Queries on the given template and attribute by issuing a HQL query with an identifier condition.
|
238
|
+
#
|
239
|
+
# @param (see #query_object)
|
240
|
+
def query_on_identifier(obj, attribute=nil)
|
279
241
|
# the source class
|
280
242
|
source = obj.class.java_class.name
|
281
243
|
# the source alias is the lower-case first letter of the source class name without package prefix
|
@@ -288,15 +250,19 @@ module CaRuby
|
|
288
250
|
pd = obj.class.attribute_metadata(attribute).property_descriptor
|
289
251
|
hql.insert(0, "select #{sa}.#{pd.name} ")
|
290
252
|
end
|
291
|
-
logger.debug { "Querying on #{obj
|
253
|
+
logger.debug { "Querying on #{obj} #{attribute} using HQL identifier criterion..." }
|
292
254
|
|
293
255
|
query_hql(hql)
|
294
256
|
end
|
295
257
|
|
296
|
-
# Returns whether the query specified by
|
297
|
-
# on a template of type attribute which references
|
298
|
-
# has a key and attribute is a non-abstract
|
299
|
-
|
258
|
+
# Returns whether the query specified by the given search object and attribute can be
|
259
|
+
# inverted as a query on a template of type attribute which references the object.
|
260
|
+
# This condition holds if the search object has a key and attribute is a non-abstract
|
261
|
+
# reference with a searchable inverse.
|
262
|
+
#
|
263
|
+
# @param (see #query_object)
|
264
|
+
# @return [Boolean] whether the query can be inverted
|
265
|
+
def invertible_query?(obj, attribute=nil)
|
300
266
|
return false if attribute.nil?
|
301
267
|
attr_md = obj.class.attribute_metadata(attribute)
|
302
268
|
return false if attr_md.type.abstract?
|
@@ -304,11 +270,13 @@ module CaRuby
|
|
304
270
|
inv_md and inv_md.searchable? and finder_parameters(obj)
|
305
271
|
end
|
306
272
|
|
307
|
-
# Queries the given
|
308
|
-
|
273
|
+
# Queries the given query object attribute by querying an attribute type template which references obj.
|
274
|
+
#
|
275
|
+
# @param (see #query_object)
|
276
|
+
def query_with_inverted_reference(obj, attribute=nil)
|
309
277
|
attr_md = obj.class.attribute_metadata(attribute)
|
310
278
|
logger.debug { "Querying on #{obj.qp} #{attribute} by inverting the query as a #{attr_md.type.qp} #{attr_md.inverse} reference query..." }
|
311
|
-
#
|
279
|
+
# the search reference template
|
312
280
|
ref = finder_template(obj)
|
313
281
|
# the attribute inverse query template
|
314
282
|
tmpl = attr_md.type.new
|
@@ -327,18 +295,20 @@ module CaRuby
|
|
327
295
|
persistence_service(tmpl).query(tmpl)
|
328
296
|
end
|
329
297
|
|
330
|
-
# Finds the
|
331
|
-
#
|
332
|
-
#
|
298
|
+
# Finds the database content matching the given search object and merges the matching
|
299
|
+
# database values into the object. The find uses the search object secondary or alternate
|
300
|
+
# key for the search.
|
333
301
|
#
|
334
|
-
# Returns nil if
|
335
|
-
# there is no matching database
|
302
|
+
# Returns nil if the search object does not have a complete secondary or alternate key or if
|
303
|
+
# there is no matching database record.
|
336
304
|
#
|
337
|
-
# If a match is found, then each missing
|
338
|
-
# fetched attribute value and this method returns
|
305
|
+
# If a match is found, then each missing search object non-domain-valued attribute is set to
|
306
|
+
# the fetched attribute value and this method returns the search object.
|
339
307
|
#
|
340
|
-
#
|
341
|
-
# obj is a
|
308
|
+
# @param obj (see #find)
|
309
|
+
# @return [Resource, nil] obj if there is a matching database record, nil otherwise
|
310
|
+
# @raise [DatabaseError] if more than object matches the obj attribute values or if
|
311
|
+
# the search object is a dependent entity that does not reference an owner
|
342
312
|
def find_object(obj)
|
343
313
|
if @transients.include?(obj) then
|
344
314
|
logger.debug { "Find #{obj.qp} obviated since the search was previously unsuccessful in the current database operation context." }
|
@@ -356,10 +326,10 @@ module CaRuby
|
|
356
326
|
# caCORE alert - there is no caCORE find utility method to update a search target with persistent content,
|
357
327
|
# so it is done manually here.
|
358
328
|
# recursively copy the nondomain attributes, esp. the identifer, of the fetched domain object references
|
359
|
-
merge_fetched(
|
329
|
+
merge_fetched(fetched, obj)
|
360
330
|
|
361
|
-
# caCORE alert - see query method alerts
|
362
|
-
#
|
331
|
+
# caCORE alert - see query method alerts.
|
332
|
+
# Inject the lazy loader for loadable domain reference attributes.
|
363
333
|
persistify(obj, fetched)
|
364
334
|
obj
|
365
335
|
end
|
@@ -388,15 +358,9 @@ module CaRuby
|
|
388
358
|
# a fetch query which returns more than one result is an error.
|
389
359
|
# possible cause is an incorrect secondary key.
|
390
360
|
if result.size > 1 then
|
391
|
-
# caCORE alert - annotations are not easily searchable; allow but bail out
|
392
|
-
# TODO Annotation - always disable annotation find?
|
393
|
-
# if CaRuby::Annotation === obj then
|
394
|
-
# logger.debug { "Annotation #{obj} search unsuccessful with template #{template}." }
|
395
|
-
# return
|
396
|
-
# end
|
397
361
|
msg = "More than one match for #{obj.class.qp} find with template #{template}."
|
398
362
|
# it is an error to have an ambiguous result
|
399
|
-
logger.error("Fetch error - #{msg}:\n#{obj
|
363
|
+
logger.error("Fetch error - #{msg}:\n#{obj}")
|
400
364
|
raise DatabaseError.new(msg)
|
401
365
|
end
|
402
366
|
|
@@ -405,28 +369,39 @@ module CaRuby
|
|
405
369
|
|
406
370
|
# If obj is a dependent, then returns the obj owner dependent which matches obj.
|
407
371
|
# Otherwise, returns nil.
|
372
|
+
#
|
373
|
+
# @param [Resource] the domain object to fetch
|
374
|
+
# @return [Resource, nil] the domain object if it matches a dependent, nil otherwise
|
408
375
|
def fetch_object_by_fetching_owner(obj)
|
409
376
|
owner = nil
|
410
377
|
oattr = obj.class.owner_attributes.detect { |attr| owner = obj.send(attr) }
|
411
378
|
return unless owner
|
412
379
|
|
413
|
-
logger.debug { "Querying #{obj.qp} by matching on the
|
414
|
-
|
415
|
-
if
|
416
|
-
raise DatabaseError.new("#{dep.class.qp} owner attribute #{oattr} does not have a #{
|
380
|
+
logger.debug { "Querying #{obj.qp} by matching on the owner #{owner.qp} #{oattr} dependents..." }
|
381
|
+
inv_md = obj.class.attribute_metadata(oattr)
|
382
|
+
if inv_md.nil? then
|
383
|
+
raise DatabaseError.new("#{dep.class.qp} owner attribute #{oattr} does not have a #{owner.class.qp} inverse dependent attribute.")
|
417
384
|
end
|
385
|
+
inverse = inv_md.inverse
|
418
386
|
# fetch the owner if necessary
|
419
387
|
unless owner.identifier then
|
420
388
|
find(owner) || return
|
421
389
|
# if obj dependent was fetched with owner, then done
|
422
|
-
|
390
|
+
if obj.identifier then
|
391
|
+
logger.debug { "Found #{obj.qp} by fetching the owner #{owner}." }
|
392
|
+
return obj
|
393
|
+
end
|
423
394
|
end
|
424
395
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
396
|
+
# try to match a fetched owner dependent
|
397
|
+
deps = lazy_loader.enable { owner.send(inverse) }
|
398
|
+
if obj.identifier then
|
399
|
+
logger.debug { "Found #{obj.qp} by fetching the owner #{owner} #{inverse} dependent #{deps.qp}." }
|
400
|
+
return obj
|
401
|
+
else
|
402
|
+
logger.debug { "#{obj.qp} does not match a fetched owner #{owner} #{inverse} dependent #{deps.qp}." }
|
403
|
+
nil
|
404
|
+
end
|
430
405
|
end
|
431
406
|
|
432
407
|
# Returns a copy of obj containing only those key attributes used in a find operation.
|
@@ -459,26 +434,28 @@ module CaRuby
|
|
459
434
|
# in separate parent copies. There is no recognition that the children reference the parent
|
460
435
|
# which generated the query. This anomaly is partially rectified in this fetch_association
|
461
436
|
# method by setting the fetched objects inverse to the given search target object. The
|
462
|
-
# inconsistent and inefficient caCORE behavior is further corrected by setting
|
463
|
-
# owners
|
437
|
+
# inconsistent and inefficient caCORE behavior is further corrected by setting inverse
|
438
|
+
# owners when the fetch result is persistified, as described in {Persistifier#persistify}.
|
439
|
+
# Callers who do not persistify the result should call {Persistifier#set_inverses} on the
|
440
|
+
# result.
|
464
441
|
#
|
465
442
|
# @param [Resource] obj the search target object
|
466
443
|
# @param [Symbol] attribute the association to fetch
|
467
444
|
# @raise [DatabaseError] if the search target object does not have an identifier
|
468
445
|
def fetch_association(obj, attribute)
|
469
|
-
logger.debug { "Fetching association #{attribute} for #{obj
|
446
|
+
logger.debug { "Fetching association #{attribute} for #{obj}..." }
|
470
447
|
# load the object if necessary
|
471
448
|
unless exists?(obj) then
|
472
449
|
raise DatabaseError.new("Can't fetch an association since the referencing object is not found in the database: #{obj}")
|
473
450
|
end
|
474
451
|
# fetch the reference
|
475
452
|
result = query_safe(obj, attribute)
|
476
|
-
# set inverse references
|
453
|
+
# set the result inverse references
|
477
454
|
inv_md = obj.class.attribute_metadata(attribute).inverse_attribute_metadata
|
478
455
|
if inv_md and not inv_md.collection? then
|
479
|
-
inv_obj = obj.copy
|
456
|
+
inv_obj = obj.copy(:identifier)
|
480
457
|
result.each do |ref|
|
481
|
-
logger.debug { "Setting fetched #{obj} #{attribute} inverse #{inv_md} to #{obj
|
458
|
+
logger.debug { "Setting fetched #{obj} #{attribute} value #{ref} inverse #{inv_md} to #{obj} copy #{inv_obj.qp}..." }
|
482
459
|
ref.send(inv_md.writer, inv_obj)
|
483
460
|
end
|
484
461
|
end
|
@@ -547,53 +524,6 @@ module CaRuby
|
|
547
524
|
logger.debug { "Search reference parameter #{attribute} for #{template.qp} set to #{ref} copied from #{source.qp}" }
|
548
525
|
ref
|
549
526
|
end
|
550
|
-
|
551
|
-
# Takes a {Persistable#snapshot} of obj to track changes and adds a lazy loader.
|
552
|
-
# If obj already has a snapshot, then this method is a no-op.
|
553
|
-
# If the other fetched source object is given, then the obj snapshot is updated
|
554
|
-
# with values from other which were not previously in obj.
|
555
|
-
#
|
556
|
-
# @param [Resource] obj the domain object to make persistable
|
557
|
-
# @param [Resource] other the domain object with the snapshot content
|
558
|
-
# @return [Resource] obj
|
559
|
-
# @raise [ArgumentError] if obj is a collection and other is not nil
|
560
|
-
def persistify(obj, other=nil)
|
561
|
-
if obj.collection? then
|
562
|
-
if other then raise ArgumentError.new("Database reader persistify other argument not supported") end
|
563
|
-
obj.each { |ref| persistify(ref) }
|
564
|
-
return obj
|
565
|
-
end
|
566
|
-
# merge or take a snapshot if necessary
|
567
|
-
snapshot(obj, other) unless obj.snapshot_taken?
|
568
|
-
# recurse to dependents before adding lazy loader to owner
|
569
|
-
obj.dependents.each { |dep| persistify(dep) if dep.identifier }
|
570
|
-
# add lazy loader to the unfetched attributes
|
571
|
-
add_lazy_loader(obj)
|
572
|
-
obj
|
573
|
-
end
|
574
|
-
|
575
|
-
# Adds this database's lazy loader to the given fetched domain object obj.
|
576
|
-
def add_lazy_loader(obj)
|
577
|
-
obj.add_lazy_loader(@lazy_loader, &@matcher)
|
578
|
-
end
|
579
|
-
|
580
|
-
# If obj has a snapshot and other is given, then merge any new fetched attribute values into the obj snapshot
|
581
|
-
# which does not yet have a value for the fetched attribute.
|
582
|
-
# Otherwise, take an obj snapshot.
|
583
|
-
#
|
584
|
-
# @param [Resource] obj the domain object to snapshot
|
585
|
-
# @param [Resource] the source domain object
|
586
|
-
# @return [Resource] the obj snapshot, updated with source content if necessary
|
587
|
-
def snapshot(obj, other=nil)
|
588
|
-
if obj.snapshot_taken? then
|
589
|
-
if other then
|
590
|
-
ovh = other.value_hash(other.class.fetched_attributes)
|
591
|
-
obj.snapshot.merge(ovh) { |v, ov| v.nil? ? ov : v }
|
592
|
-
end
|
593
|
-
else
|
594
|
-
obj.take_snapshot
|
595
|
-
end
|
596
|
-
end
|
597
527
|
end
|
598
528
|
end
|
599
529
|
end
|