Soft Deletes
Soft deletes allow you to "delete" records by setting a timestamp rather than removing them from the database. This is useful for maintaining audit trails or implementing trash/restore functionality.
Enabling Soft Deletes
Add the SoftDeletes marker mixin to your model:
(table: 'posts')
class SoftDeletePost extends Model<SoftDeletePost> with SoftDeletes {
const SoftDeletePost({required this.id, required this.title});
(isPrimaryKey: true, autoIncrement: true)
final int id;
final String title;
}
The code generator detects this mixin and:
- Adds a virtual
deletedAtfield if not explicitly defined - Applies soft-delete implementation to the generated tracked class
- Enables soft-delete query scopes automatically
Timezone-Aware Soft Deletes
For deletion timestamps stored in UTC, use SoftDeletesTZ:
(table: 'articles')
class SoftDeleteArticleTz extends Model<SoftDeleteArticleTz>
with SoftDeletesTZ {
const SoftDeleteArticleTz({required this.id, required this.title});
(isPrimaryKey: true, autoIncrement: true)
final int id;
final String title;
}
Migration Setup
Add the soft delete column in your migration:
class AddSoftDeletesToPosts extends Migration {
const AddSoftDeletesToPosts();
void up(SchemaBuilder schema) {
schema.table('posts', (table) {
// Non-timezone aware
table.softDeletes();
// OR timezone aware (UTC storage)
// table.softDeletesTz();
});
}
void down(SchemaBuilder schema) {
schema.table('posts', (table) {
table.dropColumn('deleted_at');
});
}
}
Querying Soft Deleted Records
Default Behavior
By default, soft-deleted records are excluded from queries:
Future<void> softDeleteDefault(DataSource dataSource) async {
// Only returns non-deleted posts
final posts = await dataSource.query<$Post>().get();
}
Include Soft Deleted
Use withTrashed() to include soft-deleted records:
Future<void> softDeleteWithTrashed(DataSource dataSource) async {
// Returns all posts including deleted
final allPosts = await dataSource.query<$Post>().withTrashed().get();
}
Only Soft Deleted
Use onlyTrashed() to get only soft-deleted records:
Future<void> softDeleteOnlyTrashed(DataSource dataSource) async {
// Returns only deleted posts
final trashedPosts = await dataSource.query<$Post>().onlyTrashed().get();
}
Soft Delete Operations
Deleting Records
Standard delete operations soft-delete when the model has SoftDeletes:
Future<void> softDeleteOperations(
DataSource dataSource,
Post post,
int postId,
) async {
final repo = dataSource.repo<$Post>();
// Soft delete (sets deleted_at)
await repo.delete(post);
await repo.delete({'id': postId});
}
Restoring Records
Restore soft-deleted records:
Future<void> softDeleteRestore(
DataSource dataSource,
Post post,
int postId,
int userId,
) async {
final repo = dataSource.repo<$Post>();
// Restore a single record
await repo.restore(post);
await repo.restore({'id': postId});
// Restore using query callback
await repo.restore((Query<$Post> q) => q.whereEquals('author_id', userId));
}
Force Delete
Permanently remove a record (bypass soft delete):
Future<void> softDeleteForce(
DataSource dataSource,
Post post,
int postId,
) async {
final repo = dataSource.repo<$Post>();
// Permanently delete
await repo.forceDelete(post);
await repo.forceDelete({'id': postId});
}
Checking Soft Delete Status
The trashed getter returns true if the model has been soft deleted.
if (user.trashed) {
print('User is in the trash');
}
Carbon Integration
The deletedAt getter returns a CarbonInterface?, allowing for fluent date manipulation:
print(user.deletedAt?.format('Y-m-d H:i:s'));
print(user.deletedAt?.diffForHumans()); // "deleted 5 minutes ago"
Like other timestamp fields, deletedAt returns an immutable Carbon instance. You can safely chain methods without affecting the model's state:
final deletedYesterday = user.deletedAt?.subDay(); // Safe - returns new instance
Flexible Setters
The deletedAt setter accepts both DateTime and CarbonInterface:
user.deletedAt = DateTime.now();
// or
user.deletedAt = Carbon.now();
Combining with Timestamps
You can use both Timestamps and SoftDeletes:
(table: 'posts')
class CombinedPost extends Model<CombinedPost> with Timestamps, SoftDeletes {
const CombinedPost({required this.id, required this.title});
(isPrimaryKey: true, autoIncrement: true)
final int id;
final String title;
}
// Or timezone-aware versions
(table: 'tz_posts')
class CombinedPostTz extends Model<CombinedPostTz>
with TimestampsTZ, SoftDeletesTZ {
const CombinedPostTz({required this.id, required this.title});
(isPrimaryKey: true, autoIncrement: true)
final int id;
final String title;
}
Migration:
void softDeleteMigrationCombined(SchemaBuilder schema) {
schema.create('posts', (table) {
table.id();
table.string('title');
table.timestampsTz(); // created_at, updated_at
table.softDeletesTz(); // deleted_at
});
}