knirirr
knirirr

Reputation: 2807

Modifying ActiveRecord models before preventing deletion

Some records in my application have a DOI assigned to them and in that case they should not be deleted. Instead, they should have their description changed and be flagged when a user triggers their deletion. A way to do this, I thought, would be as follows in the relevant model:

before_destroy :destroy_validation

private

def destroy_validation
  if metadata['doi'].blank?
     # Delete as normal...     
     nil
  else
    # This is a JSON field. 
    modified_metadata = Marshal.load(Marshal.dump(metadata))
    description = "Record does not exist anymore: #{name}. The record with identifier content #{doi} was invalid."
    modified_metadata['description'] = description
    modified_metadata['tombstone'] = true
    update_column :metadata, modified_metadata
    raise ActiveRecord::RecordNotDestroyed, 'Records with DOIs cannot be deleted'
  end
end

This does indeed prevent deletion, but the record appears unchanged afterwards rather than having a modified description. Here's an example of a test:

test "records with dois are not deleted" do
  record = Record.new(metadata: metadata)
  record.metadata['doi'] = 'this_is_a_doi'
  assert record.save
  assert_raises(ActiveRecord::RecordNotDestroyed) { record.destroy! }
  assert Record.exists?(record.id)
  modified_record = Record.find(record.id)
  puts "#{record.description}" # This is correctly modified as per the callback code.
  puts "#{modified_record.description}" # This is the same as when the record was created.
end

I can only guess that Rails is rolling back the update_column due to an exception having been raised, though I may be mistaken. Is there anything I can do to prevent this?

Upvotes: 1

Views: 132

Answers (1)

stolarz
stolarz

Reputation: 576

save and destroy are automatically wrapped in a transaction https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

So destroy fails, transactions is rolled back and you can't see updated column in tests.

You could try with after_rollback callback https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_rollback

or do record.destroy check for record.errors, if found update record with method manually record.update_doi if record.errors.any?.

before_destroy :destroyable?
...
def destroyable?
  unless metadata['doi'].blank?
    errors.add('Doi is not empty.')
    throw :abort
  end
end

def update_doi
  modified_metadata = Marshal.load(Marshal.dump(metadata))
  description = "Record does not exist anymore: #{name}. The record with identifier content #{doi} was invalid."
  modified_metadata['description'] = description
  modified_metadata['tombstone'] = true
  update_column :metadata, modified_metadata
end

Tip: use record.reload instead of Record.find(record.id).

Upvotes: 1

Related Questions