ORM Features & Traits
Advanced ORM features including Model Observers, Timestamps, Soft Deletes, Slugs, UUID Primary Keys, Query Scopes, and more.
Model Observers
Observers provide a clean way to separate event handling logic from your models. Listen to model lifecycle events and execute code before/after operations.
dart
import 'package:khadem/khadem.dart';
class UserObserver extends ModelObserver<User> {
@override
void creating(User user) {
// Set UUID before creating
user.ensureUuidGenerated();
user.createdBy = getCurrentUserId();
print('Creating user: ${user.email}');
}
@override
void created(User user) {
// Send welcome email after creation
print('User created: ${user.email}');
sendWelcomeEmail(user);
}
@override
void updating(User user) {
print('Updating user: ${user.id}');
user.updatedBy = getCurrentUserId();
}
@override
void updated(User user) {
print('User updated: ${user.id}');
clearUserCache(user.id);
}
@override
bool deleting(User user) {
// Prevent deletion if user has active posts
if (user.postsCount > 0) {
print('Cannot delete user with active posts');
return false; // Cancel deletion
}
return true; // Allow deletion
}
@override
void deleted(User user) {
print('User deleted: ${user.id}');
logUserDeletion(user);
}
}dart
// Register observer globally (in service provider or bootstrap)
User.observe(UserObserver());
// Or register in model
class User extends KhademModel<User> {
static void boot() {
observe(UserObserver());
}
}
// Now all User operations will trigger observer methods
final user = User()
..name = 'John Doe'
..email = 'john@example.com';
await user.save(); // Triggers creating() and created()
user.name = 'Jane Doe';
await user.save(); // Triggers updating() and updated()
await user.delete(); // Triggers deleting() (can be cancelled)
Observer Lifecycle Methods:
| Method | When Called | Can Cancel? |
|---|---|---|
| creating() | Before INSERT query | No |
| created() | After INSERT succeeds | No |
| updating() | Before UPDATE query | No |
| updated() | After UPDATE succeeds | No |
| saving() | Before INSERT or UPDATE | No |
| saved() | After INSERT or UPDATE | No |
| deleting() | Before DELETE query | Yes (return false) |
| deleted() | After DELETE succeeds | No |
| retrieving() | Before SELECT query | No |
| retrieved() | After SELECT succeeds | No |
| restoring() | Before soft delete restore | Yes (return false) |
| restored() | After restore succeeds | No |
| forceDeleting() | Before permanent delete | Yes (return false) |
| forceDeleted() | After permanent delete | No |
dart
class PostObserver extends ModelObserver<Post> {
@override
void creating(Post post) {
// Generate slug from title
post.ensureSlugGenerated();
// Set defaults
post.viewCount ??= 0;
post.status ??= 'draft';
}
@override
void created(Post post) {
// Dispatch event
Event.dispatch(PostCreatedEvent(post));
// Cache invalidation
Cache.forget('posts:recent');
}
@override
void updating(Post post) {
// Track changes
if (post.isDirty('status')) {
print('Status changed: ${post.getOriginal('status')} -> ${post.status}');
}
}
@override
bool deleting(Post post) {
// Move to archive instead of deleting
if (post.status == 'published') {
post.status = 'archived';
post.save();
return false; // Cancel actual deletion
}
return true;
}
@override
void retrieved(Post post) {
// Increment view count (consider using queues for better performance)
post.increment('view_count');
}
}Timestamps Trait
Automatically manage `created_at` and `updated_at` timestamps when creating or updating models.
dart
import 'package:khadem/khadem.dart';
class Post extends KhademModel<Post> with Timestamps {
int? id;
String? title;
String? content;
// created_at and updated_at are automatically managed
}
// Creating a record
final post = Post()
..title = 'Hello World'
..content = 'First post';
await post.save();
// created_at and updated_at are set automatically
print(post.createdAt); // DateTime
print(post.updatedAt); // DateTime
// Updating a record
post.title = 'Updated Title';
await post.save();
// updated_at is updated automatically, created_at stays the same
// Accessing timestamp info
print('Age: ${post.age?.inDays} days');
print('Was recently created: ${post.wasRecentlyCreated(hours: 24)}');
print('Was recently updated: ${post.wasRecentlyUpdated(minutes: 30)}');
dart
// Disable timestamps
class Session extends KhademModel<Session> with Timestamps {
@override
bool get timestamps => false; // No timestamp management
}
// Custom column names
class Article extends KhademModel<Article> with Timestamps {
@override
String get createdAtColumn => 'published_at';
@override
String get updatedAtColumn => 'modified_at';
}
// Manual timestamp manipulation
final post = Post();
post.setTimestamps(
createdAt: DateTime(2024, 1, 1),
updatedAt: DateTime(2024, 1, 15),
);
dart
// Touch - update only updated_at without changing data
await post.touch();
// Get age of record
final age = post.age;
print('Post is ${age?.inDays} days old');
// Get time since last update
final timeSinceUpdate = post.timeSinceUpdate;
print('Last updated ${timeSinceUpdate?.inHours} hours ago');
// Check if recently created/updated
if (post.wasRecentlyCreated(hours: 24)) {
print('New post!');
}
if (post.wasRecentlyUpdated(minutes: 30)) {
print('Recently modified');
}
Soft Deletes Trait
Soft delete records by setting a `deleted_at` timestamp instead of permanently removing them.
dart
import 'package:khadem/khadem.dart';
class Post extends KhademModel<Post> with SoftDeletes {
int? id;
String? title;
String? content;
// deleted_at is managed automatically
}
// Database migration - add deleted_at column
await createTable('posts', (table) {
table.id();
table.string('title');
table.text('content');
table.timestamp('deleted_at').nullable();
table.timestamps();
});
// Soft delete (sets deleted_at)
final post = await Post().query.find(1);
await post.delete(); // Soft delete
// Check if deleted
print(post.trashed); // true
print(post.isTrashed); // true
print(post.deletedAt); // DateTime
dart
// Default query excludes soft-deleted records
final posts = await Post().query.get(); // Only active posts
// Include soft-deleted records
final allPosts = await Post().query.withTrashed().get();
// Only soft-deleted records
final trashedPosts = await Post().query.onlyTrashed().get();
// Explicitly exclude soft-deleted (default behavior)
final activePosts = await Post().query.withoutTrashed().get();
dart
// Restore a soft-deleted record
final post = await Post().query.onlyTrashed().find(1);
await post.restore(); // Sets deleted_at to null
print(post.trashed); // false
// Force delete (permanently remove)
await post.forceDelete(); // Actually deletes from database
// With observer control
class PostObserver extends ModelObserver<Post> {
@override
bool restoring(Post post) {
// Check if restoration is allowed
if (!canRestorePost(post)) {
return false; // Cancel restoration
}
return true;
}
@override
bool forceDeleting(Post post) {
// Warn before permanent deletion
logPermanentDeletion(post);
return true;
}
}
HasSlug Trait
Automatically generate URL-friendly slugs from text fields (e.g., titles).
dart
import 'package:khadem/khadem.dart';
class Post extends KhademModel<Post> with HasSlug {
String? title;
@override
String get slugSource => title ?? '';
}
// Use with observer for auto-generation
class PostObserver extends ModelObserver<Post> {
@override
void creating(Post post) {
post.ensureSlugGenerated();
}
}
// Manual generation
final post = Post()..title = 'Hello World!';
post.generateSlug();
print(post.slug); // "hello-world"
// Custom source
post.generateSlugFrom('Custom Title 123');
print(post.slug); // "custom-title-123"
dart
// Check if slug exists
if (!post.hasSlug) {
post.generateSlug();
}
// Ensure slug is generated (doesn't overwrite)
post.ensureSlugGenerated();
// Force regenerate
post.regenerateSlug();
// Add suffix for uniqueness
final uniqueSlug = post.getSlugWithSuffix(2);
print(uniqueSlug); // "my-post-2"
// Find by slug
final post = await Post().query
.where('slug', '=', 'hello-world')
.first();
// URL routing
Route.get('/blog/{slug}', (req, res) async {
final slug = req.param('slug');
final post = await Post().query
.where('slug', '=', slug)
.first();
if (post == null) {
return res.status(404).sendJson({'error': 'Post not found'});
}
res.sendJson(post.toJson());
});
UuidPrimaryKey Trait
Use UUIDs as primary keys instead of auto-incrementing integers.
dart
import 'package:khadem/khadem.dart';
class User extends KhademModel<User> with UuidPrimaryKey {
String? name;
String? email;
// uuid is managed automatically
}
// Manual generation
final user = User();
user.generateUuid();
print(user.uuid); // "550e8400-e29b-41d4-a716-446655440000"
// Ensure UUID exists
user.ensureUuidGenerated();
// Get or generate
final uuid = user.getOrGenerateUuid();
dart
// Use with observer
class UserObserver extends ModelObserver<User> {
@override
void creating(User user) {
user.ensureUuidGenerated();
}
}
// Database migration - use uuid as primary key
await createTable('users', (table) {
table.uuid('uuid').primary();
table.string('name');
table.string('email').unique();
table.timestamps();
});
// Or as secondary identifier
await createTable('users', (table) {
table.id(); // Integer primary key
table.uuid('uuid').unique().index();
table.string('name');
table.timestamps();
});
// Find by UUID
final user = await User().query
.where('uuid', '=', '550e8400-e29b-41d4-a716-446655440000')
.first();
Query Scopes
Define reusable query constraints that can be chained for cleaner, more maintainable code.
dart
import 'package:khadem/khadem.dart';
class User extends KhademModel<User> with QueryScopes {
// Simple scope
QueryBuilderInterface<User> scopeActive(QueryBuilderInterface<User> query) {
return query.where('active', '=', true);
}
// Scope with parameter
QueryBuilderInterface<User> scopeRole(
QueryBuilderInterface<User> query,
String role,
) {
return query.where('role', '=', role);
}
// Scope with multiple conditions
QueryBuilderInterface<User> scopeVerified(QueryBuilderInterface<User> query) {
return query
.whereNotNull('email_verified_at')
.where('status', '=', 'active');
}
}
// Usage - chain scopes manually
final user = User();
final activeUsers = await user.scopeActive(user.query).get();
final admins = await user.scopeRole(user.query, 'admin').get();
// Combine scopes
final verifiedAdmins = await user
.scopeVerified(
user.scopeRole(user.query, 'admin')
)
.get();
dart
class User extends KhademModel<User> with QueryScopes {
// Helper to combine multiple scopes
QueryBuilderInterface<User> activeVerifiedAdmins() {
return applyScopes([
(q) => scopeActive(q),
(q) => scopeVerified(q),
(q) => scopeRole(q, 'admin'),
]);
}
// Date range scope
QueryBuilderInterface<User> scopeCreatedBetween(
QueryBuilderInterface<User> query,
DateTime start,
DateTime end,
) {
return query
.where('created_at', '>=', start)
.where('created_at', '<=', end);
}
// Search scope
QueryBuilderInterface<User> scopeSearch(
QueryBuilderInterface<User> query,
String term,
) {
return query.where((q) {
return q
.where('name', 'LIKE', '%$term%')
.orWhere('email', 'LIKE', '%$term%');
});
}
}
// Usage
final users = await User().activeVerifiedAdmins().get();
final recentUsers = await User()
.scopeCreatedBetween(
User().query,
DateTime.now().subtract(Duration(days: 7)),
DateTime.now(),
)
.get();
dart
class User extends KhademModel<User> with QueryScopes {
// Dynamic filtering with conditional scopes
QueryBuilderInterface<User> filteredUsers({
String? role,
bool? active,
String? searchTerm,
}) {
var q = query;
// Apply scope only if condition is met
q = when(active != null, q, (q) => scopeActive(q));
// Apply scope only if value is not null
q = whenNotNull(role, q, (q, value) => scopeRole(q, value));
q = whenNotNull(searchTerm, q, (q, value) => scopeSearch(q, value));
return q;
}
// Using pipe for functional style
QueryBuilderInterface<User> complexQuery(Map<String, dynamic> filters) {
return pipe([
(q) => filters['active'] == true ? scopeActive(q) : q,
(q) => filters.containsKey('role') ? scopeRole(q, filters['role']) : q,
(q) => q.orderBy('created_at', direction: 'DESC'),
(q) => filters.containsKey('limit') ? q.limit(filters['limit']) : q,
]);
}
// Tap for debugging
QueryBuilderInterface<User> debugQuery() {
return tap(query, (q) => print('Current query: $q'));
}
}
// Usage
final users = await User()
.filteredUsers(
role: 'admin',
active: true,
searchTerm: 'john',
)
.get();
final filtered = await User()
.complexQuery({
'active': true,
'role': 'user',
'limit': 10,
})
.get();
Combining Multiple Traits
Models can use multiple traits together for powerful functionality.
dart
import 'package:khadem/khadem.dart';
class Post extends KhademModel<Post>
with Timestamps, SoftDeletes, HasSlug, QueryScopes {
int? id;
String? title;
String? content;
String? status;
@override
String get slugSource => title ?? '';
// Query scopes
QueryBuilderInterface<Post> scopePublished(QueryBuilderInterface<Post> query) {
return query.where('status', '=', 'published');
}
QueryBuilderInterface<Post> scopeRecent(QueryBuilderInterface<Post> query) {
return query
.orderBy('created_at', direction: 'DESC')
.limit(10);
}
}
class PostObserver extends ModelObserver<Post> {
@override
void creating(Post post) {
// Generate slug from title
post.ensureSlugGenerated();
// Set default status
post.status ??= 'draft';
}
@override
void created(Post post) {
// Log creation
print('Post created: ${post.title} (${post.slug})');
// Send notification
Event.dispatch(PostCreatedEvent(post));
}
@override
bool deleting(Post post) {
// Archive published posts instead of deleting
if (post.status == 'published') {
post.status = 'archived';
post.save();
return false; // Cancel deletion
}
return true;
}
}
// Register observer
Post.observe(PostObserver());
// Usage
final post = Post()
..title = 'Advanced ORM Features'
..content = 'Learn about traits...';
await post.save();
// Triggers: creating(), sets slug, sets created_at/updated_at, triggers created()
print(post.slug); // "advanced-orm-features"
print(post.createdAt); // DateTime
print(post.age?.inDays); // Days since creation
// Query with scopes
final recentPublished = await Post()
.scopePublished(Post().query)
.scopeRecent(Post().query)
.get();
// Soft delete
await post.delete(); // Sets deleted_at
print(post.trashed); // true
// Restore
await post.restore(); // Clears deleted_at
// Force delete (permanent)
await post.forceDelete();
Best Practices
Follow these guidelines when using ORM features and traits.
✅ Do:
- Use observers for cross-cutting concerns
- Keep observer logic focused and single-purpose
- Use soft deletes for audit trails
- Generate slugs in observers for consistency
- Use query scopes for reusable queries
- Combine traits for rich model functionality
- Document custom timestamp columns
- Use UUIDs for distributed systems
❌ Don't:
- Put heavy logic in observers (use jobs)
- Create circular observer dependencies
- Force delete unless necessary
- Forget to add deleted_at index
- Regenerate slugs after publication
- Use too many traits on one model
- Disable timestamps without good reason
- Mix UUID and integer primary keys
