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.
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