Similar to the SaveOperation
, Avram comes with a DeleteOperation
that’s generated with each model.
This allows you to write more complex logic around deleteing records. (e.g. delete confirmations, etc…)
If you just want to delete a record without any validations or callbacks, the simplest way is to use the generated {ModelName}::DeleteOperation
For example, if we have a Server
model, Lucky will generate a Server::DeleteOperation
that you can use:
server = ServerQuery.find(123)
Server::DeleteOperation.delete!(server)
If the record fails to be deleted, an Avram::InvalidOperationError
will be raised.
You can customize DeleteOperations with callbacks and validations. These
classes go in your src/operations/
directory, and will inherit from
{ModelName}::DeleteOperation
.
# src/operations/delete_server.cr
class DeleteServer < Server::DeleteOperation
end
DeleteOperation
in actions
The interface should feel pretty familiar. The object being deleted is passed in to the delete
method, and a block will
return the operation instance, and the object being deleted.
# src/actions/servers/delete.cr
class Servers::Delete < BrowserAction
delete "/servers/:server_id" do
server = ServerQuery.find(server_id)
DeleteServer.delete(server) do |operation, deleted_server|
if operation.deleted?
redirect to: Servers::Index
else
flash.failure = "Could not delete"
html Servers::EditPage, server: deleted_server
end
end
end
end
You can also pass in params or named args for use with attributes, or needs
.
DeleteServer.delete(server, params, secret_codes: [23_u16, 94_u16]) do |operation, deleted_server|
if operation.deleted?
redirect to: Servers::Index
else
flash.failure = "Could not delete"
html Servers::EditPage, server: deleted_server
end
end
You can also use the delete!
method if you don’t need validations and expect deletes to work every time:
DeleteServer.delete!(server)
This is helpful when your operation only has callbacks or needs and is expected to work every time.
DeleteOperation
Callbacks and Validations
DeleteOperations come with before_delete
and after_delete
callbacks that allow you to either validate
some code before performing the delete, or perform some action after deleteing. (i.e. Send a “Goodbye” email, etc…)
Along with the callbacks, you also have access to attribute
, needs
, and all of the columns related to a model.
You even have file_attribute
for those times you need to use biometric scans to authorize deleting a record!
# src/operations/delete_server.cr
class DeleteServer < Server::DeleteOperation
attribute confirmation : String
before_delete do
validate_required confirmation
# `record` is the object to be deleted
if confirmation.value != record.server_name
confirmation.add_error("Confirmation must match the server name")
end
end
end
# src/operations/delete_server.cr
class DeleteServer < Server::DeleteOperation
needs secret_codes : Array(UInt16)
after_delete do |deleted_server|
decrypted_server_data = DecryptServer.new(deleted_server, with: secret_codes)
DecryptedServerDataEmail.new(decrypted_server_data).deliver
end
end
Currently bulk deletes with DeleteOperation are not supported.
If you need to bulk delete a group of records based on a where query, you can use delete
at the end of your query.
This returns the number of records deleted.
# DELETE FROM users WHERE banned_at IS NOT NULL
UserQuery.new.banned_at.is_not_nil.delete
A “soft delete” is when you want to hide a record as if it were deleted, but you want to keep the actual record in your database. This allows you to restore the record without losing any previous data or associations.
Avram comes with some built-in modules to help make working with soft deleted records a lot easier. Let’s add it
to an existing Article
model.
soft_deleted_at : Time?
column to the table that needs soft deletes.# Run this in your terminal
lucky gen.migration AddSoftDeleteToArticles
db/migrations/20241224180650_add_soft_delete_to_articles.cr
file.def migrate
alter table_for(Article) do
add soft_deleted_at : Time?, index: true
end
end
src/models/article.cr
file.class Article < BaseModel
# Include this module to add methods for
# soft deleting and restoring
include Avram::SoftDelete::Model
table do
# Add the new column to your model
column soft_deleted_at : Time?
end
end
src/queries/article_query.cr
.class ArticleQuery < Article::BaseQuery
# Include this module to add methods for
# querying and soft deleting records
include Avram::SoftDelete::Query
end
Once a model includes the Avram::SoftDelete::Model
, the associated DeleteOperation will handle the soft delete for you.
# src/operations/delete_article.cr
class DeleteArticle < Article::DeleteOperation
end
and in your action
# src/actions/articles/delete.cr
class Articles::Delete < BrowserAction
delete "/articles/:article_id" do
article = ArticleQuery.find(article_id)
deleted_article = DeleteArticle.delete!(article)
# This returns `true`
deleted_article.soft_deleted?
redirect to: Articles::Index
end
end
Currently bulk soft deletes with DeleteOperation are not supported.
You can bulk update a group of records as soft deleted with the soft_delete
method on your Query object.
articles_to_delete = ArticleQuery.new.created_at.gt(3.years.ago)
# Marks the articles created over 3 years ago as soft deleted
articles_to_delete.soft_delete
If you need to restore a soft deleted record, you can use the restore
method on the model instance.
# Set the `soft_deleted_at` back to `nil`
article.restore
The same as we can bulk soft delete records, we can also bulk update to restore them with
the restore
method on your Query object.
articles_to_restore = ArticleQuery.new.published_at.lt(1.week.ago)
# Restore recently published articles
articles_to_restore.restore
# Return all articles that are not soft deleted
ArticleQuery.new.only_kept
# Return all articles that are soft deleted
ArticleQuery.new.only_soft_deleted
If you want to filter out soft deleted records by default, it’s really easy to do.
Just add the only_kept
method as the default query in the initialize
method.
class ArticleQuery < Article::BaseQuery
include Avram::SoftDelete::Query
# All queries will scope to only_kept
def initialize
defaults &.only_kept
end
end
# Return all articles that are not soft deleted
ArticleQuery.new
Even with your default scope, you can still return soft deleted records when you need.
# Return all articles, both `kept` and soft deleted
ArticleQuery.new.with_soft_deleted
If you need to delete every record in the entire table, you can use truncate
.
TRUNCATE TABLE users
UserQuery.truncate
Running the
truncate
method may raise an error similar to the following:
Error message cannot truncate a table referenced in a foreign key constraint.
If that’s the case, call the same method with the
cascade
option set totrue
:
UserQuery.truncate(cascade: true)
This will automatically delete or update matching records in a child table where a foreign key relationship is in place.
You can also truncate your entire database by calling truncate
on your database class.
AppDatabase.truncate
This method is great for tests; horrible for production. Also note this method is not chainable.