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.
Files changed (52) hide show
  1. data/History.txt +10 -0
  2. data/lib/caruby/cli/command.rb +10 -8
  3. data/lib/caruby/database/fetched_matcher.rb +28 -39
  4. data/lib/caruby/database/lazy_loader.rb +101 -0
  5. data/lib/caruby/database/persistable.rb +190 -167
  6. data/lib/caruby/database/persistence_service.rb +21 -7
  7. data/lib/caruby/database/persistifier.rb +185 -0
  8. data/lib/caruby/database/reader.rb +106 -176
  9. data/lib/caruby/database/saved_matcher.rb +56 -0
  10. data/lib/caruby/database/search_template_builder.rb +1 -1
  11. data/lib/caruby/database/sql_executor.rb +8 -7
  12. data/lib/caruby/database/store_template_builder.rb +134 -61
  13. data/lib/caruby/database/writer.rb +252 -52
  14. data/lib/caruby/database.rb +88 -67
  15. data/lib/caruby/domain/attribute_initializer.rb +16 -0
  16. data/lib/caruby/domain/attribute_metadata.rb +161 -72
  17. data/lib/caruby/domain/id_alias.rb +22 -0
  18. data/lib/caruby/domain/inversible.rb +91 -0
  19. data/lib/caruby/domain/merge.rb +116 -35
  20. data/lib/caruby/domain/properties.rb +1 -1
  21. data/lib/caruby/domain/reference_visitor.rb +207 -71
  22. data/lib/caruby/domain/resource_attributes.rb +93 -80
  23. data/lib/caruby/domain/resource_dependency.rb +22 -97
  24. data/lib/caruby/domain/resource_introspection.rb +21 -28
  25. data/lib/caruby/domain/resource_inverse.rb +134 -0
  26. data/lib/caruby/domain/resource_metadata.rb +41 -19
  27. data/lib/caruby/domain/resource_module.rb +42 -33
  28. data/lib/caruby/import/java.rb +8 -9
  29. data/lib/caruby/migration/migrator.rb +20 -7
  30. data/lib/caruby/migration/resource_module.rb +0 -2
  31. data/lib/caruby/resource.rb +132 -351
  32. data/lib/caruby/util/cache.rb +4 -1
  33. data/lib/caruby/util/class.rb +48 -1
  34. data/lib/caruby/util/collection.rb +54 -18
  35. data/lib/caruby/util/inflector.rb +7 -0
  36. data/lib/caruby/util/options.rb +35 -31
  37. data/lib/caruby/util/partial_order.rb +1 -1
  38. data/lib/caruby/util/properties.rb +2 -2
  39. data/lib/caruby/util/stopwatch.rb +16 -8
  40. data/lib/caruby/util/transitive_closure.rb +1 -1
  41. data/lib/caruby/util/visitor.rb +342 -328
  42. data/lib/caruby/version.rb +1 -1
  43. data/lib/caruby/yard/resource_metadata_handler.rb +8 -0
  44. data/lib/caruby.rb +2 -0
  45. metadata +10 -9
  46. data/lib/caruby/database/saved_merger.rb +0 -131
  47. data/lib/caruby/domain/annotatable.rb +0 -25
  48. data/lib/caruby/domain/annotation.rb +0 -23
  49. data/lib/caruby/import/annotatable_class.rb +0 -28
  50. data/lib/caruby/import/annotation_class.rb +0 -27
  51. data/lib/caruby/import/annotation_module.rb +0 -67
  52. 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
- # Returns the CaCORE ApplicationServiceProvider wrapped by this PersistenceService.
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
- # Returns the block result.
91
- def dispatch
95
+ #
96
+ # @return the block result
97
+ def time
92
98
  result = nil
93
- seconds = @timer.run { result = yield app_service }.elapsed
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 { "Calling application service query on target class #{target} with following HQL:\n#{hql}" }
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 { "Calling application service search with template #{template.qp}, target-first class path #{reverse_class_name_path.pp_s(:single_line)}, criterion:\n#{dump(template)}" }
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
- @ftchd_vstr = ReferenceVisitor.new { |tgt| tgt.class.fetched_domain_attributes }
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.qp }
149
-
150
- def lazy_load(obj, attribute)
151
- value = fetch_association(obj, attribute)
152
- # add a lazy loader and snapshot to each fetched reference
153
- persistify(value) if value
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 the hql query with an attribute query
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 { |parent| query_toxic(parent, attribute) }.flatten
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
- def merge_fetched(target, source)
225
- @ftchd_mrg_vstr.visit(target, source) { |src, tgt| tgt.copy_volatile_attributes(src) }
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
- if invertible_query?(obj, attribute) then
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 obj and attribute by issuing a HQL query with an identifier condition.
278
- def query_on_identifier(obj, attribute)
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.qp} #{attribute} using HQL #{hql}..." }
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 obj and attribute can be inverted as a query
297
- # on a template of type attribute which references obj. This condition holds if obj
298
- # has a key and attribute is a non-abstract reference with a searchable inverse.
299
- def invertible_query?(obj, attribute)
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 obj attribute by querying an attribute type template which references obj.
308
- def query_with_inverted_reference(obj, attribute)
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
- # an obj template
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 object matching the specified object obj from the database and merges
331
- # the matching database values into obj. The find uses the obj secondary or
332
- # alternate key for the search.
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 obj does not have a complete secondary or alternate key or if
335
- # there is no matching database object.
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 obj non-domain-valued attribute is set to the
338
- # fetched attribute value and this method returns obj.
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
- # Raises DatabaseError if more than object matches the obj attribute values or if
341
- # obj is a dependent entity that does not reference an owner.
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(obj, fetched)
329
+ merge_fetched(fetched, obj)
360
330
 
361
- # caCORE alert - see query method alerts
362
- # inject the lazy loader for loadable domain reference attributes
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.dump}")
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 fetched owner #{owner.qp} #{oattr} dependents..." }
414
- inverse = obj.class.attribute_metadata(oattr).inverse
415
- if inverse.nil? then
416
- raise DatabaseError.new("#{dep.class.qp} owner attribute #{oattr} does not have a #{obj.class.qp} inverse attribute")
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
- return obj if obj.identifier
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
- deps = query(owner, inverse)
426
- logger.debug { "Owner #{owner.qp} has #{deps.size} #{inverse} dependents: #{deps.qp}." }
427
- # If the dependent can be unambiguously matched to one of the results,
428
- # then return the matching result.
429
- obj.match_in_owner_scope(deps) unless deps.empty?
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 dependent
463
- # owners in the fetch result, as described in {#query_safe}.
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.qp}..." }
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.qp} copy #{inv_obj.qp}..." }
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