caruby-core 1.4.2 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +10 -0
- data/lib/caruby/cli/command.rb +10 -8
- data/lib/caruby/database/fetched_matcher.rb +28 -39
- data/lib/caruby/database/lazy_loader.rb +101 -0
- data/lib/caruby/database/persistable.rb +190 -167
- data/lib/caruby/database/persistence_service.rb +21 -7
- data/lib/caruby/database/persistifier.rb +185 -0
- data/lib/caruby/database/reader.rb +106 -176
- data/lib/caruby/database/saved_matcher.rb +56 -0
- data/lib/caruby/database/search_template_builder.rb +1 -1
- data/lib/caruby/database/sql_executor.rb +8 -7
- data/lib/caruby/database/store_template_builder.rb +134 -61
- data/lib/caruby/database/writer.rb +252 -52
- data/lib/caruby/database.rb +88 -67
- data/lib/caruby/domain/attribute_initializer.rb +16 -0
- data/lib/caruby/domain/attribute_metadata.rb +161 -72
- data/lib/caruby/domain/id_alias.rb +22 -0
- data/lib/caruby/domain/inversible.rb +91 -0
- data/lib/caruby/domain/merge.rb +116 -35
- data/lib/caruby/domain/properties.rb +1 -1
- data/lib/caruby/domain/reference_visitor.rb +207 -71
- data/lib/caruby/domain/resource_attributes.rb +93 -80
- data/lib/caruby/domain/resource_dependency.rb +22 -97
- data/lib/caruby/domain/resource_introspection.rb +21 -28
- data/lib/caruby/domain/resource_inverse.rb +134 -0
- data/lib/caruby/domain/resource_metadata.rb +41 -19
- data/lib/caruby/domain/resource_module.rb +42 -33
- data/lib/caruby/import/java.rb +8 -9
- data/lib/caruby/migration/migrator.rb +20 -7
- data/lib/caruby/migration/resource_module.rb +0 -2
- data/lib/caruby/resource.rb +132 -351
- data/lib/caruby/util/cache.rb +4 -1
- data/lib/caruby/util/class.rb +48 -1
- data/lib/caruby/util/collection.rb +54 -18
- data/lib/caruby/util/inflector.rb +7 -0
- data/lib/caruby/util/options.rb +35 -31
- data/lib/caruby/util/partial_order.rb +1 -1
- data/lib/caruby/util/properties.rb +2 -2
- data/lib/caruby/util/stopwatch.rb +16 -8
- data/lib/caruby/util/transitive_closure.rb +1 -1
- data/lib/caruby/util/visitor.rb +342 -328
- data/lib/caruby/version.rb +1 -1
- data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
- data/lib/caruby.rb +2 -0
- metadata +10 -9
- data/lib/caruby/database/saved_merger.rb +0 -131
- data/lib/caruby/domain/annotatable.rb +0 -25
- data/lib/caruby/domain/annotation.rb +0 -23
- data/lib/caruby/import/annotatable_class.rb +0 -28
- data/lib/caruby/import/annotation_class.rb +0 -27
- data/lib/caruby/import/annotation_module.rb +0 -67
- data/lib/caruby/migration/resource.rb +0 -8
@@ -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
|