feat(orm): extend model with destroy and touch
This commit is contained in:
1 parent
13b4613023
commit
fc91e95b62
1 file changed
+102
-4
+102
-4
@@ -319,9 +319,77 @@ module Prism
|
||||
save!
|
||||
end
|
||||
|
||||
def destroy
|
||||
self.class.store.delete(self.class.table_name, id) if persisted?
|
||||
# Cascaded dependent destroys run inside the same transaction as the
|
||||
# parent's own delete, so a failure partway through the cascade (or
|
||||
# in after_destroy) rolls back every row it already removed - not
|
||||
# just the parent - instead of leaving orphaned deletes behind.
|
||||
# `force: true` (from Paranoia#really_destroy!) always removes the row
|
||||
# for real even on a paranoid model, running the full callback/cascade/
|
||||
# locking sequence again even if the record was already soft-deleted -
|
||||
# matches Rails paranoia's really_destroy!, which re-runs the destroy
|
||||
# callbacks rather than assuming a soft-deleted record is untouchable.
|
||||
def destroy(force: false)
|
||||
return self if destroyed? && !force
|
||||
# Set before before_destroy runs (not after the paranoid-delete
|
||||
# branch below decides) so a before_destroy hook can tell #soft_deleting?
|
||||
# apart from a real delete - e.g. a hook that removes an uploaded
|
||||
# file from disk should only do that on a hard delete, not one a
|
||||
# paranoid model will still let #restore bring back.
|
||||
@destroy_force = force
|
||||
if !new_record?
|
||||
self.class.transaction do
|
||||
enlist_in_transaction(:destroy)
|
||||
return self unless run_callbacks(:before_destroy)
|
||||
cascade_dependent_associations
|
||||
if paranoid_delete? && !force
|
||||
soft_delete_row
|
||||
elsif self.class.locking_enabled?
|
||||
begin
|
||||
self.class.store.delete(self.class.table_name, id, [self.class.locking_column, lock_version.to_i])
|
||||
rescue Store::StaleRowError
|
||||
raise StaleObjectError.new(self, :destroy)
|
||||
end
|
||||
else
|
||||
self.class.store.delete(self.class.table_name, id)
|
||||
end
|
||||
@destroyed = true
|
||||
run_callbacks(:after_destroy)
|
||||
end
|
||||
else
|
||||
@destroyed = true
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# ActiveRecord::Persistence#touch-lite: bumps updated_at (plus any
|
||||
# named extra attributes, e.g. `post.touch(:published_at)`) straight
|
||||
# on the stored row and skips validations and the save callback chain
|
||||
# entirely - only after_touch runs. This is what belongs_to ...,
|
||||
# touch: true rides on (see Associable#define_touch): touching a
|
||||
# comment's post whenever the comment is saved or destroyed changes
|
||||
# the post's #cache_key, which is exactly what busts a `cache post do
|
||||
# ... end` fragment that embeds the comment list.
|
||||
def touch(*names)
|
||||
raise RecordNotFound, "cannot touch a new record" if new_record?
|
||||
raise RecordNotFound, "cannot touch a destroyed record" if destroyed?
|
||||
now = Time.now.utc
|
||||
attributes = {}
|
||||
attributes['updated_at'] = now if self.class.columns.include?('updated_at')
|
||||
names.each { |name| attributes[name.to_s] = now }
|
||||
return self if attributes.empty?
|
||||
if self.class.locking_enabled?
|
||||
begin
|
||||
row = self.class.store.update(self.class.table_name, id, attributes,
|
||||
[self.class.locking_column, lock_version.to_i])
|
||||
rescue Store::StaleRowError
|
||||
raise StaleObjectError.new(self, :touch)
|
||||
end
|
||||
attributes[self.class.locking_column] = row[self.class.locking_column]
|
||||
else
|
||||
self.class.store.update(self.class.table_name, id, attributes)
|
||||
end
|
||||
attributes.each { |key, value| @attributes[key] = value }
|
||||
run_callbacks(:after_touch)
|
||||
self
|
||||
end
|
||||
|
||||
@@ -336,6 +404,16 @@ module Prism
|
||||
@attributes.dup
|
||||
end
|
||||
|
||||
# Fragment-cache key: changes whenever the record is touched, so a
|
||||
# `cache post do ... end` view fragment (Prism::CacheHelpers) keyed on
|
||||
# this automatically busts on update without anyone deleting the old
|
||||
# entry by hand.
|
||||
def cache_key
|
||||
base = "#{self.class.table_name}/#{id}"
|
||||
timestamp = self['updated_at'] || self['created_at']
|
||||
timestamp ? "#{base}-#{timestamp.to_i}" : base
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(self.class) && !id.nil? && id == other.id
|
||||
end
|
||||
@@ -351,14 +429,34 @@ module Prism
|
||||
@attributes = row.dup
|
||||
@new_record = false
|
||||
@destroyed = false
|
||||
clear_changes_information
|
||||
self
|
||||
end
|
||||
|
||||
def touch_timestamps
|
||||
now = Time.now.utc
|
||||
columns = self.class.columns
|
||||
@attributes['created_at'] ||= now if columns.include?('created_at') && new_record?
|
||||
@attributes['updated_at'] = now if columns.include?('updated_at')
|
||||
self['created_at'] = now if columns.include?('created_at') && new_record? && self['created_at'].nil?
|
||||
self['updated_at'] = now if columns.include?('updated_at')
|
||||
end
|
||||
|
||||
def initialize_lock_version
|
||||
return unless self.class.locking_enabled?
|
||||
@attributes[self.class.locking_column] = 0 if self[self.class.locking_column].nil?
|
||||
end
|
||||
|
||||
# Only the columns this instance actually changed since it was
|
||||
# loaded/created, never the full @attributes hash - an UPDATE that
|
||||
# sends every column (including ones this instance never touched)
|
||||
# would overwrite any value another code path mutated directly on
|
||||
# the stored row in the meantime, such as a counter_cache bump
|
||||
# (Store#increment_counter) from a cascade_dependent_associations
|
||||
# step that runs on this very record earlier in the same #destroy,
|
||||
# or simply this instance's attributes being stale relative to a
|
||||
# concurrent write. Matches real UPDATE semantics (only SET the
|
||||
# columns that changed) instead of a full-row REPLACE.
|
||||
def dirty_attributes
|
||||
@attributes.slice(*changed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in new issue
Block a user