Module: CaRuby::Persistable

Included in:
Resource
Defined in:
lib/caruby/database/persistable.rb

Overview

The Persistable mixin adds persistance capability. Every instance which includes Persistable must respond to an overrided #database method.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#snapshot{Symbol => Object} (readonly)

Returns the content value hash at the point of the last snapshot.

Returns:

  • ({Symbol => Object})

    the content value hash at the point of the last snapshot



11
12
13
# File 'lib/caruby/database/persistable.rb', line 11

def snapshot
  @snapshot
end

Class Method Details

.saved?(obj) ⇒ Boolean

Returns whether the given object(s) have an identifier.

Parameters:

  • obj (Jinx::Resource, <Jinx::Resource>, nil)

    the object(s) to check

Returns:

  • (Boolean)

    whether the given object(s) have an identifier



15
16
17
18
19
20
21
22
23
# File 'lib/caruby/database/persistable.rb', line 15

def self.saved?(obj)
  if obj.nil_or_empty? then
    false
  elsif obj.collection? then
    obj.all? { |ref| saved?(ref) }
  else
    !!obj.identifier
  end
end

.unsaved?(obj) ⇒ Boolean

Returns whether at least one of the given object(s) does not have an identifier.

Parameters:

  • obj (Jinx::Resource, <Jinx::Resource>, nil)

    the object(s) to check

Returns:

  • (Boolean)

    whether at least one of the given object(s) does not have an identifier



27
28
29
# File 'lib/caruby/database/persistable.rb', line 27

def self.unsaved?(obj)
  not (obj.nil_or_empty? or saved?(obj))
end

Instance Method Details

#add_defaults_autogeneratedObject

Sets the default attribute values for this auto-generated domain object.



281
282
283
# File 'lib/caruby/database/persistable.rb', line 281

def add_defaults_autogenerated
  add_defaults_recursive
end

#add_lazy_loader(loader, attributes = nil) ⇒ Object

Lazy loads the attributes. If a block is given to this method, then the attributes are determined by calling the block with this Persistable as a parameter. Otherwise, the default attributes are the unfetched domain attributes.

Each of the attributes which does not already hold a non-nil or non-empty value will be loaded from the database on demand. This method injects attribute value initialization into each loadable attribute reader. The initializer is given by either the loader Proc argument. The loader takes two arguments, the target object and the attribute to load. If this Persistable already has a lazy loader, then this method is a no-op.

Lazy loading is disabled on an attribute after it is invoked on that attribute or when the attribute setter method is called.

Parameters:

  • loader (LazyLoader)

    the lazy loader to add



197
198
199
200
201
202
203
204
205
206
# File 'lib/caruby/database/persistable.rb', line 197

def add_lazy_loader(loader, attributes=nil)
  # guard against invalid call
  if identifier.nil? then raise ValidationError.new("Cannot add lazy loader to an unfetched domain object: #{self}") end
  # the attributes to lazy-load
  attributes ||= loadable_attributes
  return if attributes.empty?
  # define the reader and writer method overrides for the missing attributes
  pas = attributes.select { |pa| inject_lazy_loader(pa) }
  logger.debug { "Lazy loader added to #{qp} attributes #{pas.to_series}." } unless pas.empty?
end

#autogenerated?(operation) ⇒ <Symbol>

Relaxes the #saved_attributes_to_fetch condition for a SCG as follows:

  • If the SCG status was updated from Pending to Collected, then fetch the saved SCG event parameters.

Parameters:

Returns:

  • (<Symbol>)

    whether this domain object must be fetched to reflect the database state



343
344
345
# File 'lib/caruby/database/persistable.rb', line 343

def autogenerated?(operation)
  operation == :update && status_changed_to_complete? ? EVENT_PARAM_ATTRS : super
end

#changed?(attribute = nil) ⇒ Boolean

Returns whether this Persistable either doesn’t have a snapshot or has changed since the last snapshot. This is a conservative condition test that returns false if there is no snaphsot for this Persistable and therefore no basis to determine whether the content changed. If the attribute parameter is given, then only that attribute is checked for a change. Otherwise, all attributes are checked.

Parameters:

  • attribute (Symbol, nil) (defaults to: nil)

    the optional attribute to check.

Returns:

  • (Boolean)

    whether this Persistable’s content differs from its snapshot



167
168
169
# File 'lib/caruby/database/persistable.rb', line 167

def changed?(attribute=nil)
  @snapshot.nil? or not snapshot_equal_content?(attribute)
end

#changed_attributes<Symbol>

Returns the attributes which differ between the #snapshot and current content.

Returns:

  • (<Symbol>)

    the attributes which differ between the #snapshot and current content



172
173
174
175
176
177
178
179
180
# File 'lib/caruby/database/persistable.rb', line 172

def changed_attributes
 if @snapshot then
    ovh = value_hash(self.class.updatable_attributes)
    diff = @snapshot.diff(ovh) { |pa, v, ov| Jinx::Resource.value_equal?(v, ov) }
    diff.keys
  else
    self.class.updatable_attributes
  end
end

#copy_volatile_attributes(other) ⇒ Object

Sets the CaRuby::Propertied#volatile_nondomain_attributes to the other fetched value, if different.

Parameters:

  • other (Jinx::Resource)

    the fetched domain object reflecting the database state



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/caruby/database/persistable.rb', line 392

def copy_volatile_attributes(other)
  pas = self.class.volatile_nondomain_attributes
  return if pas.empty?
  pas.each do |pa|
    val = send(pa)
    oval = other.send(pa)
    if val.nil? then
      # Overwrite the current attribute value.
      set_property_value(pa, oval)
      logger.debug { "Set #{qp} volatile #{pa} to the fetched #{other.qp} database value #{oval.qp}." }
    elsif oval != val and pa == :identifier then
      # If this error occurs, then there is a serious match-merge flaw. 
      raise DatabaseError.new("Can't copy #{other} to #{self} with different identifier")
    end
  end
  logger.debug { "Merged auto-generated attribute values #{pas.to_series} from #{other.qp} into #{self}..." }
end

#createObject

Creates this domain object in the #database.

Raises:

  • (DatabaseError)

    if the subclass does not override this method

See Also:

  • Writer#create


74
75
76
# File 'lib/caruby/database/persistable.rb', line 74

def create
  database.create(self)
end

#databaseDatabase

Returns the data access mediator for this domain object. Application #Jinx::Resource modules are required to override this method.

Returns:

  • (Database)

    the data access mediator for this Persistable, if any

Raises:

  • (DatabaseError)

    if the subclass does not override this method



36
37
38
# File 'lib/caruby/database/persistable.rb', line 36

def database
  raise ValidationError.new("#{self} database is missing")
end

#deleteObject

Deletes this domain object from the #database.

Raises:

  • (DatabaseError)

    if the subclass does not override this method

See Also:

  • Writer#delete


113
114
115
# File 'lib/caruby/database/persistable.rb', line 113

def delete
  database.delete(self)
end

#do_without_lazy_loader { ... } ⇒ Object

Executes the given block with the database lazy loader disabled, if any.

Yields:

  • the block to execute



251
252
253
254
255
256
257
# File 'lib/caruby/database/persistable.rb', line 251

def do_without_lazy_loader(&block)
  if database then
    database.lazy_loader.disable(&block)
  else
    yield
  end
end

#dumpObject

Wrap Resource.dump to disable the lazy-loader while printing.



244
245
246
# File 'lib/caruby/database/persistable.rb', line 244

def dump
  do_without_lazy_loader { super }
end

#ensure_existsObject

Creates this domain object, if necessary.



81
82
83
# File 'lib/caruby/database/persistable.rb', line 81

def ensure_exists
  database.ensure_exists(self)
end

#fetch_autogenerated?(operation) ⇒ Boolean

Returns:

  • (Boolean)


347
348
349
350
351
352
353
# File 'lib/caruby/database/persistable.rb', line 347

def fetch_autogenerated?(operation)
  # only fetch a create, not an update (note that subclasses can override this condition)
  operation == :update
  # Check for an attribute with a value that might need to be changed in order to
  # reflect the auto-generated database content.
  self.class.autogenerated_logical_dependent_attributes.select { |pa| not send(pa).nil_or_empty? }
end

#fetch_saved?Boolean

Returns whether this domain object must be fetched to reflect the database state. This default implementation returns whether this domain object was created and there are any autogenerated attributes. Subclasses can override to relax or restrict the condition.

TODO - this method is no longeer used. Should it be? If not, remove here and in catissue subclasses.

Returns:

  • (Boolean)

    whether this domain object must be fetched to reflect the database state



378
379
380
381
382
383
384
385
386
# File 'lib/caruby/database/persistable.rb', line 378

def fetch_saved?
  # only fetch a create, not an update (note that subclasses can override this condition)
  return false if identifier
  # Check for an attribute with a value that might need to be changed in order to
  # reflect the auto-generated database content.
  ag_attrs = self.class.autogenerated_attributes
  return false if ag_attrs.empty?
  ag_attrs.any? { |pa| not send(pa).nil_or_empty? }
end

#fetched?Boolean

Returns whether this Persistable has a #snapshot.

Returns:

  • (Boolean)

    whether this Persistable has a #snapshot



136
137
138
# File 'lib/caruby/database/persistable.rb', line 136

def fetched?
  !!@snapshot
end

#find(opts = nil) ⇒ Object

Fetches this domain object from the #database.

Raises:

  • (DatabaseError)

    if the subclass does not override this method

See Also:

  • Reader#find


64
65
66
# File 'lib/caruby/database/persistable.rb', line 64

def find(opts=nil)
  database.find(self, opts)
end

#loadable_attributes<Symbol>

Returns the attributes to load on demand. The base attribute list is given by the CaRuby::Propertied#loadable_attributes whose value is nil or empty. In addition, if this Persistable has more than one Domain::Dependency#owner_attributes and one is non-nil, then none of the owner attributes are loaded on demand, since there can be at most one owner and ownership cannot change.

Returns:

  • (<Symbol>)

    the attributes to load on demand



215
216
217
218
219
220
221
222
223
224
# File 'lib/caruby/database/persistable.rb', line 215

def loadable_attributes
  pas = self.class.loadable_attributes.select { |pa| send(pa).nil_or_empty? }
  ownr_attrs = self.class.owner_attributes
  # If there is an owner, then variant owners are not loaded.
  if ownr_attrs.size > 1 and ownr_attrs.any? { |pa| not send(pa).nil_or_empty? } then
    pas - ownr_attrs
  else
    pas
  end
end

#merge_into_snapshot(other) ⇒ Object

Merges the other domain object non-domain attribute values into this domain object’s snapshot, An existing snapshot value is replaced by the corresponding other attribute value.

Parameters:

  • other (Jinx::Resource)

    the source domain object

Raises:

  • (ValidationError)

    if this domain object does not have a snapshot



145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/caruby/database/persistable.rb', line 145

def merge_into_snapshot(other)
  if @snapshot.nil? then
    raise ValidationError.new("Cannot merge #{other.qp} content into #{qp} snapshot, since #{qp} does not have a snapshot.")
  end
  # the non-domain attribute => [target value, other value] difference hash
  delta = diff(other)
  # the difference attribute => other value hash, excluding nil other values
  dvh = delta.transform_value { |d| d.last }
  return if dvh.empty?
  logger.debug { "#{qp} differs from database content #{other.qp} as follows: #{delta.filter_on_key { |pa| dvh.has_key?(pa) }.qp}" }
  logger.debug { "Setting #{qp} snapshot values from other #{other.qp} values to reflect the database state: #{dvh.qp}..." }
  # update the snapshot from the other value to reflect the database state
  @snapshot.merge!(dvh)
end

#persistence_servicePersistenceService

Returns the database application service for this Persistable.

Returns:



41
42
43
# File 'lib/caruby/database/persistable.rb', line 41

def persistence_service
  database.persistence_service(self.class)
end

#query(*path) ⇒ Object

Fetches the domain objects which match this template from the #database.

Raises:

  • (DatabaseError)

    if the subclass does not override this method

See Also:

  • Reader#query


52
53
54
# File 'lib/caruby/database/persistable.rb', line 52

def query(*path)
  path.empty? ? database.query(self) : database.query(self, *path)
end

#remove_lazy_loader(attribute = nil) ⇒ Object

Disables lazy loading of the specified attribute. Lazy loaded is disabled for all attributes if no attribute is specified. This method is a no-op if this Persistable does not have a lazy loader.

Parameters:

  • the (Symbol)

    attribute to remove from the load list, or nil if to remove all attributes



231
232
233
234
235
236
237
238
239
240
241
# File 'lib/caruby/database/persistable.rb', line 231

def remove_lazy_loader(attribute=nil)
  if attribute.nil? then
    return self.class.domain_attributes.each { |pa| remove_lazy_loader(pa) }
  end
  # the modified accessor method
  reader, writer = self.class.property(attribute).accessors
  # remove the reader override
  disable_singleton_method(reader)
  # remove the writer override
  disable_singleton_method(writer)
end

#saveObject Also known as: store

Saves this domain object in the #database.

Raises:

  • (DatabaseError)

    if the subclass does not override this method

See Also:

  • Writer#save


91
92
93
# File 'lib/caruby/database/persistable.rb', line 91

def save
  database.save(self)
end

#saved_attributes_to_fetch(operation) ⇒ <Symbol>

Returns this domain object’s attributes which must be fetched to reflect the database state. This default implementation returns the CaRuby::Propertied#autogenerated_logical_dependent_attributes if this domain object does not have an identifier, or an empty array otherwise. Subclasses can override to relax or restrict the condition.

Parameters:

Returns:

  • (<Symbol>)

    whether this domain object must be fetched to reflect the database state



328
329
330
331
332
333
334
335
336
# File 'lib/caruby/database/persistable.rb', line 328

def saved_attributes_to_fetch(operation)
  # only fetch a create, not an update (note that subclasses can override this condition)
  if operation.type == :create or operation.autogenerated? then
    # Filter the class saved fetch attributes for content.
    self.class.saved_attributes_to_fetch.select { |pa| not send(pa).nil_or_empty? }
  else
    Array::EMPTY_ARRAY
  end
end

#searchable?Boolean

Returns whether this domain object has #searchable_attributes.

Returns:



286
287
288
# File 'lib/caruby/database/persistable.rb', line 286

def searchable?
  not searchable_attributes.nil?
end

#searchable_attributes<Symbol>

Returns the attributes to use for a search using this domain object as a template, determined as follows:

  • If this domain object has a non-nil primary key, then the primary key is the search criterion.

  • Otherwise, if this domain object has a secondary key and each key attribute value is not nil, then the secondary key is the search criterion.

  • Otherwise, if this domain object has an alternate key and each key attribute value is not nil, then the aklternate key is the search criterion.

Returns:

  • (<Symbol>)

    the attributes to use for a search on this domain object



299
300
301
302
303
304
305
306
# File 'lib/caruby/database/persistable.rb', line 299

def searchable_attributes
  key_props = self.class.primary_key_attributes
  return key_props if key_searchable?(key_props)
  key_props = self.class.secondary_key_attributes
  return key_props if key_searchable?(key_props)
  key_props = self.class.alternate_key_attributes
  return key_props if key_searchable?(key_props)
end

#take_snapshot{Symbol => Object}

Captures the Persistable’s updatable attribute base values. The snapshot is subsequently accessible using the #snapshot method.

Returns:

  • ({Symbol => Object})

    the snapshot value hash



131
132
133
# File 'lib/caruby/database/persistable.rb', line 131

def take_snapshot
  @snapshot = value_hash(self.class.updatable_attributes)
end

#updatable?Boolean

Returns whether this domain object can be updated (default is true, subclasses can override).

Returns:

  • (Boolean)

    whether this domain object can be updated (default is true, subclasses can override)



119
120
121
# File 'lib/caruby/database/persistable.rb', line 119

def updatable?
  true
end

#updateObject

Updates this domain object in the #database.

Raises:

  • (DatabaseError)

    if the subclass does not override this method

See Also:

  • Writer#update


103
104
105
# File 'lib/caruby/database/persistable.rb', line 103

def update
  database.update(self)
end

#validate(autogenerated = false) ⇒ Persistable

Validates this domain object and its #CaRuby::Propertied#unproxied_savable_template_attributes for consistency and completeness prior to a database create operation. An object is valid if it contains a non-nil value for each mandatory attribute. Objects which have already been validated are skipped.

A Persistable class should not override this method, but override the private #validate_local method instead.

Returns:

Raises:

  • (Jinx::ValidationError)

    if the object state is invalid



269
270
271
272
273
274
275
276
277
278
# File 'lib/caruby/database/persistable.rb', line 269

def validate(autogenerated=false)
  if (identifier.nil? or autogenerated) and not @validated then
    validate_local
    @validated = true
  end
  self.class.unproxied_savable_template_attributes.each do |pa|
    send(pa).enumerate { |dep| dep.validate }
  end
  self
end