Module: Familia::Horreum::Persistence
- Included in:
- Familia::Horreum
- Defined in:
- lib/familia/horreum/persistence.rb
Overview
Serialization - Instance-level methods for object persistence and retrieval Handles conversion between Ruby objects and Valkey hash storage
Instance Method Summary collapse
-
#apply_fields(**fields) ⇒ self
Updates the object by applying multiple field values.
-
#batch_update(**kwargs) ⇒ MultiResult
Updates multiple fields atomically in a Database transaction.
-
#clear_fields! ⇒ void
Clears all fields by setting them to nil.
-
#commit_fields(update_expiration: true) ⇒ Object
Commits object fields to the DB storage.
-
#destroy! ⇒ void
Permanently removes this object and its related fields from the DB.
-
#refresh ⇒ self
Refreshes object state from the DB and returns self for method chaining.
-
#refresh! ⇒ void
Refreshes the object state from the DB storage.
-
#save(update_expiration: true) ⇒ Boolean
Persists the object to Valkey storage with automatic timestamping.
-
#save_if_not_exists(update_expiration: true) ⇒ Boolean
Saves the object to Valkey storage only if it doesn't already exist.
Instance Method Details
#apply_fields(**fields) ⇒ self
Updates the object by applying multiple field values.
Sets multiple attributes on the object instance using their corresponding setter methods. Only fields that have defined setter methods will be updated.
249 250 251 252 253 254 255 |
# File 'lib/familia/horreum/persistence.rb', line 249 def apply_fields(**fields) fields.each do |field, value| # Apply the field value if the setter method exists send("#{field}=", value) if respond_to?("#{field}=") end self end |
#batch_update(**kwargs) ⇒ MultiResult
Updates multiple fields atomically in a Database transaction.
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/familia/horreum/persistence.rb', line 213 def batch_update(**kwargs) update_expiration = kwargs.delete(:update_expiration) { true } fields = kwargs Familia.trace :BATCH_UPDATE, nil, fields.keys if Familia.debug? transaction_result = transaction do |conn| fields.each do |field, value| prepared_value = serialize_value(value) conn.hset dbkey, field, prepared_value # Update instance variable to keep object in sync send("#{field}=", value) if respond_to?("#{field}=") end end # Update expiration if requested and supported self.update_expiration(default_expiration: nil) if update_expiration && respond_to?(:update_expiration) # Return the MultiResult directly (transaction already returns MultiResult) transaction_result end |
#clear_fields! ⇒ void
This operation does not persist the changes to the DB. Call save after clear_fields! if you want to persist the cleared state.
This method returns an undefined value.
Clears all fields by setting them to nil.
Resets all object fields to nil values, effectively clearing the object's state. This operation affects all fields defined on the object's class, setting each one to nil through their corresponding setter methods.
320 321 322 |
# File 'lib/familia/horreum/persistence.rb', line 320 def clear_fields! self.class.field_method_map.each_value { |method_name| send("#{method_name}=", nil) } end |
#commit_fields(update_expiration: true) ⇒ Object
The expiration update is only performed for classes that have the expiration feature enabled. For others, it's a no-op.
This method performs debug logging of the object's class, dbkey, and current state before committing to the DB.
Commits object fields to the DB storage.
Persists the current state of all object fields to the DB using HMSET. Optionally updates the key's expiration time if the feature is enabled for the object's class.
187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/familia/horreum/persistence.rb', line 187 def commit_fields(update_expiration: true) prepared_value = to_h_for_storage Familia.ld "[commit_fields] Begin #{self.class} #{dbkey} #{prepared_value} (exp: #{update_expiration})" result = hmset(prepared_value) # Only classes that have the expiration ferature enabled will # actually set an expiration time on their keys. Otherwise # this will be a no-op that simply logs the attempt. update_expiration(default_expiration: nil) if update_expiration result end |
#destroy! ⇒ void
This method provides high-level object lifecycle management.
It operates at the object level for ORM-style operations, while
delete! operates directly on database keys. Use destroy! when
removing complete objects from the system.
When debugging is enabled, this method will trace the deletion operation for diagnostic purposes.
This method returns an undefined value.
Permanently removes this object and its related fields from the DB.
Deletes the object's database key and all associated data. This operation is irreversible and will permanently destroy all stored information for this object instance and the additional list, set, hash, string etc fields defined for this class.
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/familia/horreum/persistence.rb', line 281 def destroy! Familia.trace :DESTROY, dbkey, uri # Execute all deletion operations within a transaction transaction do |conn| # Delete the main object key conn.del(dbkey) # Delete all related fields if present if self.class.relations? Familia.trace :DELETE_RELATED_FIELDS!, nil, "#{self.class} has relations: #{self.class..keys}" self.class..each do |name, _definition| obj = send(name) Familia.trace :DELETE_RELATED_FIELD, name, "Deleting related field #{name} (#{obj.dbkey})" conn.del(obj.dbkey) end end end end |
#refresh ⇒ self
Refreshes object state from the DB and returns self for method chaining.
Loads the current state of the object from the DB storage, updating all field values to match their persisted state. This method provides a chainable interface to the refresh! operation.
376 377 378 379 |
# File 'lib/familia/horreum/persistence.rb', line 376 def refresh refresh! self end |
#refresh! ⇒ void
This method discards any unsaved changes to the object. Use with caution when the object has been modified but not yet persisted.
Transient fields are reset to nil during refresh since they have no authoritative source in Valkey storage.
This method returns an undefined value.
Refreshes the object state from the DB storage.
Reloads all persistent field values from the DB, overwriting any unsaved changes in the current object instance. This operation synchronizes the object with its stored state in the database.
345 346 347 348 349 350 351 352 353 354 355 356 357 358 |
# File 'lib/familia/horreum/persistence.rb', line 345 def refresh! Familia.trace :REFRESH, nil, uri if Familia.debug? raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey) fields = hgetall Familia.ld "[refresh!] #{self.class} #{dbkey} fields:#{fields.keys}" # Reset transient fields to nil for semantic clarity and ORM consistency # Transient fields have no authoritative source, so they should return to # their uninitialized state during refresh operations reset_transient_fields! naive_refresh(**fields) end |
#save(update_expiration: true) ⇒ Boolean
When Familia.debug? is enabled, this method will trace the save operation for debugging purposes.
Persists the object to Valkey storage with automatic timestamping.
Saves the current object state to Valkey storage, automatically setting created and updated timestamps if the object supports them. The method commits all persistent fields and optionally updates the key's expiration.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/familia/horreum/persistence.rb', line 63 def save(update_expiration: true) Familia.trace :SAVE, nil, uri if Familia.debug? # No longer need to sync computed identifier with a cache field self.created ||= Familia.now.to_i if respond_to?(:created) self.updated = Familia.now.to_i if respond_to?(:updated) # Commit our tale to the Database chronicles # Wrap in transaction for atomicity between save and indexing ret = commit_fields(update_expiration: update_expiration) # Auto-index for class-level indexes after successful save # Use transaction to ensure atomicity with the save operation if ret transaction do |conn| auto_update_class_indexes # Add to class-level instances collection after successful save self.class.instances.add(identifier, Familia.now) if self.class.respond_to?(:instances) end end Familia.ld "[save] #{self.class} #{dbkey} #{ret} (update_expiration: #{update_expiration})" # Did Database accept our offering? !ret.nil? end |
#save_if_not_exists(update_expiration: true) ⇒ Boolean
This method uses HSETNX to atomically check and set the identifier field, ensuring race-condition-free conditional creation.
Saves the object to Valkey storage only if it doesn't already exist.
Conditionally persists the object to Valkey storage by first checking if the identifier field already exists. If the object already exists in storage, raises an error. Otherwise, proceeds with a normal save operation including automatic timestamping.
This method provides atomic conditional creation to prevent duplicate objects from being saved when uniqueness is required based on the identifier field.
Check if save_if_not_exists is implemented correctly. It should:
Check if record exists If exists, raise Familia::RecordExistsError If not exists, save
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/familia/horreum/persistence.rb', line 132 def save_if_not_exists(update_expiration: true) identifier_field = self.class.identifier_field Familia.ld "[save_if_not_exists]: #{self.class} #{identifier_field}=#{identifier}" Familia.trace :SAVE_IF_NOT_EXISTS, nil, uri if Familia.debug? success = dbclient.watch(dbkey) do if dbclient.exists(dbkey).positive? dbclient.unwatch raise Familia::RecordExistsError, dbkey end result = dbclient.multi do |multi| multi.hmset(dbkey, to_h_for_storage) end result.is_a?(Array) # transaction succeeded end # Auto-index for class-level indexes after successful save # Use transaction to ensure atomicity with the save operation if success transaction do |conn| auto_update_class_indexes end end success end |