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!
|
save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
# Cascaded dependent destroys run inside the same transaction as the
|
||||||
self.class.store.delete(self.class.table_name, id) if persisted?
|
# 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
|
@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
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -336,6 +404,16 @@ module Prism
|
|||||||
@attributes.dup
|
@attributes.dup
|
||||||
end
|
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)
|
def ==(other)
|
||||||
other.is_a?(self.class) && !id.nil? && id == other.id
|
other.is_a?(self.class) && !id.nil? && id == other.id
|
||||||
end
|
end
|
||||||
@@ -351,14 +429,34 @@ module Prism
|
|||||||
@attributes = row.dup
|
@attributes = row.dup
|
||||||
@new_record = false
|
@new_record = false
|
||||||
@destroyed = false
|
@destroyed = false
|
||||||
|
clear_changes_information
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def touch_timestamps
|
def touch_timestamps
|
||||||
now = Time.now.utc
|
now = Time.now.utc
|
||||||
columns = self.class.columns
|
columns = self.class.columns
|
||||||
@attributes['created_at'] ||= now if columns.include?('created_at') && new_record?
|
self['created_at'] = now if columns.include?('created_at') && new_record? && self['created_at'].nil?
|
||||||
@attributes['updated_at'] = now if columns.include?('updated_at')
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in new issue
Block a user