Cascading Delete Guide#
Cascading delete is a powerful feature in Datum that allows you to safely delete entities and all their related data in a controlled manner. This guide covers the comprehensive cascading delete functionality including dry-run mode, progress callbacks, cancellation support, timeout protection, and improved error handling.
Overview#
Cascading delete ensures data integrity by automatically handling related entities when deleting a parent entity. It supports different deletion behaviors and provides extensive control over the deletion process.
Cascade Delete Behaviors#
Datum supports three cascade delete behaviors that determine how related entities are handled:
Cascade#
relations: {
'posts': HasMany<Post>(this, 'userId', cascadeDeleteBehavior: CascadeDeleteBehavior.cascade),
}
Deletes all related entities along with the parent entity.
Restrict#
relations: {
'comments': HasMany<Comment>(this, 'userId', cascadeDeleteBehavior: CascadeDeleteBehavior.restrict),
}
Prevents deletion if any related entities exist, maintaining referential integrity.
None (Default)#
relations: {
'author': BelongsTo<User>(this, 'userId', cascadeDeleteBehavior: CascadeDeleteBehavior.none),
}
Leaves related entities untouched (traditional foreign key behavior).
Basic Usage#
Simple Cascade Delete#
// Delete a user and all related entities
final result = await userManager.cascadeDelete(id: 'user-123', userId: 'user-123');
if (result.success) {
print('Successfully deleted ${result.totalDeleted} entities');
} else {
print('Deletion failed: ${result.errors.join(', ')}');
}
Fluent API Builder#
// Use the fluent API for more control
final result = await userManager
.deleteCascade('user-123')
.forUser('user-123')
.execute();
Advanced Features#
Dry-Run Mode#
Preview what would be deleted without actually performing the deletion:
// Preview the deletion
final preview = await userManager
.deleteCascade('user-123')
.forUser('user-123')
.dryRun()
.execute();
if (preview.success) {
print('Would delete ${preview.totalDeleted} entities:');
final success = preview as CascadeSuccess<User>;
success.deletedEntities.forEach((type, entities) {
print('- ${entities.length} ${type.toString()} entities');
});
success.restrictedRelations.forEach((relation, entities) {
print('⚠️ ${entities.length} entities would prevent deletion in relation: $relation');
});
}
Progress Callbacks#
Monitor deletion progress in real-time:
final result = await userManager
.deleteCascade('user-123')
.forUser('user-123')
.withProgress((progress) {
print('Progress: ${progress.progressPercentage.toStringAsFixed(1)}% '
'(${progress.completed}/${progress.total}) - ${progress.currentEntityType}');
})
.execute();
Cancellation Support#
Cancel long-running deletions:
final token = CancellationToken();
final future = userManager
.deleteCascade('user-123')
.forUser('user-123')
.withCancellation(token)
.execute();
// Cancel after 5 seconds if still running
Future.delayed(Duration(seconds: 5), () {
token.cancel();
});
final result = await future;
if (result.success) {
print('Deletion completed');
} else if (result.errors.any((e) => e.contains('cancelled'))) {
print('Deletion was cancelled');
}
Timeout Protection#
Prevent hanging operations with timeouts:
final result = await userManager
.deleteCascade('user-123')
.forUser('user-123')
.withTimeout(Duration(seconds: 30))
.execute();
Partial Deletes#
Allow partial success when some deletions fail:
final result = await userManager
.deleteCascade('user-123')
.forUser('user-123')
.allowPartialDeletes()
.execute();
// Check what was actually deleted
if (result is CascadeSuccess<User>) {
print('Successfully deleted ${result.totalDeleted} entities');
result.deletedEntities.forEach((type, entities) {
print('- ${entities.length} ${type.toString()}');
});
}
Entity Relationships Example#
Here's a complete example showing how cascade delete works with complex relationships:
class User extends RelationalDatumEntity {
@override
final String id;
@override
final String userId;
final String name;
const User({required this.id, required this.name}) : userId = id;
@override
Map<String, Relation> get relations => {
// Cascade: Delete all posts when user is deleted
'posts': HasMany<Post>(this, 'userId', cascadeDeleteBehavior: CascadeDeleteBehavior.cascade),
// Cascade: Delete profile when user is deleted
'profile': HasOne<Profile>(this, 'userId', cascadeDeleteBehavior: CascadeDeleteBehavior.cascade),
// Restrict: Prevent deletion if user has comments
'comments': HasMany<Comment>(this, 'userId', cascadeDeleteBehavior: CascadeDeleteBehavior.restrict),
};
// ... other required methods
}
class Post extends RelationalDatumEntity {
@override
final String id;
@override
final String userId;
final String title;
const Post({required this.id, required this.userId, required this.title});
@override
Map<String, Relation> get relations => {
// None: Don't delete author when post is deleted
'author': BelongsTo<User>(this, 'userId', cascadeDeleteBehavior: CascadeDeleteBehavior.none),
// Cascade: Delete all comments when post is deleted
'comments': HasMany<Comment>(this, 'postId', cascadeDeleteBehavior: CascadeDeleteBehavior.cascade),
};
// ... other required methods
}
class Comment extends RelationalDatumEntity {
@override
final String id;
@override
final String userId;
final String postId;
final String content;
const Comment({required this.id, required this.userId, required this.postId, required this.content});
@override
Map<String, Relation> get relations => {
// None: Don't delete related entities
'post': BelongsTo<Post>(this, 'postId', cascadeDeleteBehavior: CascadeDeleteBehavior.none),
};
// ... other required methods
}
Analytics and Monitoring#
Get detailed analytics about the deletion process:
final result = await userManager
.deleteCascade('user-123')
.forUser('user-123')
.execute();
if (result is CascadeSuccess<User>) {
final analytics = result.analytics;
print('Deletion Analytics:');
print('- Duration: ${analytics.totalDuration}');
print('- Queries executed: ${analytics.queriesExecuted}');
print('- Relationships traversed: ${analytics.relationshipsTraversed}');
print('- Total entities processed: ${analytics.totalEntitiesProcessed}');
print('- Total entities deleted: ${analytics.totalEntitiesDeleted}');
print('- Success rate: ${analytics.successRate.toStringAsFixed(1)}%');
analytics.entitiesProcessedByType.forEach((type, count) {
print('- Processed $count ${type.toString()} entities');
});
}
Error Handling#
Cascade delete provides detailed error information:
final result = await userManager.cascadeDelete(id: 'user-123', userId: 'user-123');
if (!result.success) {
print('Deletion failed with ${result.errors.length} errors:');
result.errors.forEach((error) {
print('- $error');
});
// Check for restrict violations
if (result is CascadeDeleteResult<User>) {
result.restrictedRelations.forEach((relation, entities) {
print('Restrict violation in $relation: ${entities.length} entities');
});
}
}
Common Error Types#
- ENTITY_NOT_FOUND: The entity to delete doesn't exist
- RESTRICT_VIOLATION: Related entities prevent deletion
- DELETE_FAILED: Individual entity deletion failed
- TIMEOUT: Operation exceeded timeout duration
- CANCELLED: Operation was cancelled
Best Practices#
1. Use Dry-Run for Critical Operations#
// Always preview critical deletions
final preview = await manager.deleteCascade(entityId).forUser(userId).dryRun().execute();
if (preview.totalDeleted > 100) {
// Get user confirmation for large deletions
final confirmed = await showLargeDeletionDialog(preview);
if (!confirmed) return;
}
2. Handle Restrict Violations Gracefully#
final result = await manager.cascadeDelete(entityId).forUser(userId).execute();
if (!result.success && result.errors.any((e) => e.contains('restrict'))) {
// Show user which relations are preventing deletion
final violations = (result as CascadeDeleteResult).restrictedRelations;
await showRestrictViolationDialog(violations);
}
3. Use Progress Callbacks for Long Operations#
final result = await manager
.deleteCascade(entityId)
.forUser(userId)
.withProgress((progress) {
updateProgressIndicator(progress.progressPercentage);
if (progress.progressPercentage % 25 == 0) {
print('Deletion ${progress.progressPercentage}% complete');
}
})
.execute();
4. Set Appropriate Timeouts#
// Adjust timeout based on expected data size
final timeout = expectedEntityCount > 1000
? Duration(minutes: 5)
: Duration(seconds: 30);
final result = await manager
.deleteCascade(entityId)
.forUser(userId)
.withTimeout(timeout)
.execute();
5. Use Cancellation for User-Initiated Operations#
final token = CancellationToken();
// Allow user to cancel via UI
cancelButton.onPressed.listen((_) => token.cancel());
final result = await manager
.deleteCascade(entityId)
.forUser(userId)
.withCancellation(token)
.execute();
Production Considerations#
Performance Optimization#
- Use batch processing for large datasets
- Monitor analytics for performance bottlenecks
- Consider using
allowPartialDeletes()for resilient operations
Data Safety#
- Always use dry-run mode for critical data
- Implement proper backup strategies
- Log all cascade delete operations for audit trails
Error Recovery#
- Handle partial failures gracefully
- Provide clear error messages to users
- Implement retry logic for transient failures
Integration with Sync#
Cascade delete operations are automatically included in sync operations:
// Cascade delete with immediate sync
await manager.cascadeDelete(id: entityId, userId: userId, forceRemoteSync: true);
All deleted entities will be queued for remote synchronization, ensuring consistency across all data sources.
Testing#
Use the comprehensive test examples as reference for testing cascade delete functionality:
// Test restrict violations
test('prevents deletion with restrict relationships', () async {
// Create entity with restrict relationship
await manager.cascadeDelete(id: entityId, userId: userId);
// Verify deletion was blocked
});
// Test cascade behavior
test('deletes all related entities with cascade behavior', () async {
// Create entity with cascade relationships
final result = await manager.cascadeDelete(id: entityId, userId: userId);
// Verify all related entities were deleted
});
// Test dry-run mode
test('dry-run shows what would be deleted', () async {
final preview = await manager.deleteCascade(entityId).forUser(userId).dryRun().execute();
// Verify preview shows correct entities without deleting them
});
This guide covers the comprehensive cascading delete functionality in Datum. For more advanced usage patterns, refer to the API documentation and test examples.