Models & Relationships

Learn how to create models and define relationships between them in Khadem Dart

Eloquent-like ORM
Relationship Management
Query Building
dart
// app/models/User.dart
import 'package:khadem/khadem_dart.dart';

class User extends KhademModel<User> with Timestamps, HasRelationships {
  User({
    this.name,
    this.email,
    this.password,
    int? id,
  }) {
    this.id = id;
  }

  String? name;
  String? email;
  String? password;

  @override
  List<String> get fillable => [
    'name',
    'email',
    'password',
  ];

  @override
  List<String> get initialHidden => [
    'password',
    'remember_token',
  ];

  @override
  Map<String, Type> get casts => {
    'email_verified_at': DateTime,
    'is_active': bool,
    'settings': Map,
  };

  @override
  Map<String, RelationDefinition> get relations => {
    'posts': hasMany<Post>(
      foreignKey: 'user_id',
      relatedTable: 'posts',
      factory: () => Post(),
    ),
  };

  @override
  Object? getField(String key) {
    return switch (key) {
      'id' => id,
      'name' => name,
      'email' => email,
      'password' => password,
      'created_at' => createdAt,
      'updated_at' => updatedAt,
      _ => null
    };
  }

  @override
  void setField(String key, dynamic value) {
    return switch (key) {
      'id' => id = value,
      'name' => name = value,
      'email' => email = value,
      'password' => password = value,
      'created_at' => createdAt = value,
      'updated_at' => updatedAt = value,
      _ => null
    };
  }

  @override
  User newFactory(Map<String, dynamic> data) {
    return User()..fromJson(data);
  }
}
dart
// Create a new user
final user = User();
user.name = 'John Doe';
user.email = 'john@example.com';
user.password = 'password'; // Note: Hashing should be done separately
await user.save();

// Find user by ID
final user = await User().query.where('id', '=', 1).first();

// Find by attributes
final user = await User().query.where('email', '=', 'john@example.com').first();

// Get all users
final users = await User().query.get();

// Get users with conditions
final activeUsers = await User().query.where('is_active', '=', true).get();

// Update user
user.name = 'Updated Name';
await user.save();

// Delete user
await user.delete();

// Refresh from database
await user.refresh();
dart
// app/models/User.dart
class User extends KhademModel<User> with Timestamps, HasRelationships {
  // Local query methods
  QueryBuilderInterface<User> active() {
    return query.where('is_active', '=', true);
  }

  QueryBuilderInterface<User> verified() {
    return query.whereNotNull('email_verified_at');
  }

  QueryBuilderInterface<User> recent() {
    return query.where('created_at', '>', DateTime.now().subtract(Duration(days: 7)));
  }
}

// Usage
final activeUsers = await User().active().get();
final verifiedUsers = await User().verified().get();
final recentUsers = await User().recent().get();

// Combine scopes
final activeVerifiedUsers = await User().active().verified().get();

// Chain with other query methods
final recentActiveUsers = await User()
    .recent()
    .active()
    .orderBy('created_at', direction: 'desc')
    .limit(10)
    .get();

Creating Models

Models represent database tables and provide an easy way to interact with your data.

dart
// app/models/User.dart
class User extends KhademModel<User> with Timestamps, HasRelationships {
  // CRUD operations
  Future<void> updateProfile(Map<String, dynamic> data) async {
    fromJson(data);
    await save();
  }

  Future<void> deleteAccount() async {
    await delete();
  }

  // Check if model exists
  bool get exists => id != null;

  // Check if model was recently created
  bool get wasRecentlyCreated => createdAt != null &&
    createdAt!.difference(DateTime.now()).inMinutes < 1;

  // Get specific field value
  dynamic getField(String key) {
    return switch (key) {
      'id' => id,
      'name' => name,
      'email' => email,
      'password' => password,
      'created_at' => createdAt,
      'updated_at' => updatedAt,
      _ => null
    };
  }

  // Set specific field value
  void setField(String key, dynamic value) {
    return switch (key) {
      'id' => id = value,
      'name' => name = value,
      'email' => email = value,
      'password' => password = value,
      'created_at' => createdAt = value,
      'updated_at' => updatedAt = value,
      _ => null
    };
  }
}
dart
// app/models/User.dart
class User extends KhademModel<User> with Timestamps, HasRelationships {
  // Accessors
  String get fullName => '${name?.split(' ').first ?? ''} ${name?.split(' ').last ?? ''}';

  String get avatarUrl => 'https://ui-avatars.com/api/?name=${Uri.encodeComponent(name ?? '')}';

  bool get isAdmin => role == 'admin';

  // Mutators
  set fullName(String value) {
    name = value;
  }

  set password(String value) {
    // Note: Hashing should be done before setting
    this.password = value;
  }

  // Custom methods
  Future<bool> sendPasswordReset() async {
    // Send password reset email logic
    return true;
  }

  Future<List<Post>> getPublishedPosts() async {
    return await loadRelation('posts').then((_) {
      final posts = getRelation('posts') as List<Post>? ?? [];
      return posts.where((post) => post.published == true).toList();
    });
  }
}

Model Relationships

Define relationships between your models to easily access related data.

dart
// app/models/User.dart
class User extends KhademModel<User> with Timestamps, HasRelationships {
  @override
  Map<String, RelationDefinition> get relations => {
    'profile': hasOne<Profile>(
      foreignKey: 'user_id',
      relatedTable: 'profiles',
      factory: () => Profile(),
    ),
  };
}

// app/models/Profile.dart
class Profile extends KhademModel<Profile> with Timestamps, HasRelationships {
  @override
  Map<String, RelationDefinition> get relations => {
    'user': belongsTo<User>(
      localKey: 'user_id',
      relatedTable: 'users',
      factory: () => User(),
    ),
  };
}

// Usage
final user = await User().query.where('id', '=', 1).first();
await user.load('profile');
final profile = user.getRelation('profile');

// Reverse relationship
final profile = await Profile().query.where('id', '=', 1).first();
await profile.load('user');
final user = profile.getRelation('user');
dart
// app/models/User.dart
class User extends KhademModel<User> with Timestamps, HasRelationships {
  @override
  Map<String, RelationDefinition> get relations => {
    'posts': hasMany<Post>(
      foreignKey: 'user_id',
      relatedTable: 'posts',
      factory: () => Post(),
    ),
  };
}

// app/models/Post.dart
class Post extends KhademModel<Post> with Timestamps, HasRelationships {
  @override
  Map<String, RelationDefinition> get relations => {
    'user': belongsTo<User>(
      localKey: 'user_id',
      relatedTable: 'users',
      factory: () => User(),
    ),
  };
}

// Usage
final user = await User().query.where('id', '=', 1).first();
await user.load('posts');
final posts = user.getRelation('posts') as List<Post>;

// Get posts count
final postsCount = await user.query
    .where('user_id', '=', user.id)
    .count();

// Create post for user
final post = Post();
post.userId = user.id;
post.title = 'New Post';
post.content = 'Post content';
await post.save();

// Get user from post
final post = await Post().query.where('id', '=', 1).first();
await post.load('user');
final user = post.getRelation('user') as User;
dart
// app/models/User.dart
class User extends KhademModel<User> with Timestamps, HasRelationships {
  @override
  Map<String, RelationDefinition> get relations => {
    'roles': belongsToMany<Role>(
      pivotTable: 'user_roles',
      foreignPivotKey: 'user_id',
      relatedPivotKey: 'role_id',
      relatedTable: 'roles',
      localKey: 'id',
      factory: () => Role(),
    ),
  };
}

// app/models/Role.dart
class Role extends KhademModel<Role> with Timestamps, HasRelationships {
  @override
  Map<String, RelationDefinition> get relations => {
    'users': belongsToMany<User>(
      pivotTable: 'user_roles',
      foreignPivotKey: 'role_id',
      relatedPivotKey: 'user_id',
      relatedTable: 'users',
      localKey: 'id',
      factory: () => User(),
    ),
  };
}

// Usage
final user = await User().query.where('id', '=', 1).first();
await user.load('roles');
final roles = user.getRelation('roles') as List<Role>;

// Note: Attach/detach operations would need to be implemented
// as custom methods since the framework doesn't provide them directly
dart
// app/models/Post.dart
class Post extends KhademModel<Post> with Timestamps, HasRelationships {
  @override
  Map<String, RelationDefinition> get relations => {
    'comments': morphMany<Comment>(
      morphTypeField: 'commentable_type',
      morphIdField: 'commentable_id',
      relatedTable: 'comments',
      factory: () => Comment(),
    ),
    'images': morphMany<Image>(
      morphTypeField: 'imageable_type',
      morphIdField: 'imageable_id',
      relatedTable: 'images',
      factory: () => Image(),
    ),
  };
}

// app/models/Comment.dart
class Comment extends KhademModel<Comment> with Timestamps, HasRelationships {
  @override
  Map<String, RelationDefinition> get relations => {
    'commentable': morphTo<Comment>(
      morphTypeField: 'commentable_type',
      morphIdField: 'commentable_id',
      relatedTable: '', // Will be determined dynamically
      factory: () => Comment(), // This would need to be dynamic
    ),
  };
}

// Database migrations
// comments table
table.integer('commentable_id');
table.string('commentable_type'); // Post, Video, etc.

// Usage
final post = await Post().query.where('id', '=', 1).first();
await post.load('comments');
final comments = post.getRelation('comments') as List<Comment>;

// Note: morphTo implementation would need custom logic
// to determine the correct model type based on commentable_type
dart
// app/models/Country.dart
class Country extends Model {
  Collection<Post> posts() {
    return hasManyThrough(Post, User);
  }
}

// app/models/User.dart
class User extends Model {
  Country country() {
    return belongsTo<Country>(
      localKey: 'country_id',
      relatedTable: 'countries',
      factory: () => Country()
    );
  }

  Collection<Post> posts() {
    return hasMany<Post>(
      foreignKey: 'user_id',
      relatedTable: 'posts',
      factory: () => Post()
    );
  }
}

// app/models/Post.dart
class Post extends Model {
  User user() {
    return belongsTo<User>(
      localKey: 'user_id',
      relatedTable: 'users',
      factory: () => User()
    );
  }
}

// Usage
final country = await Country.find(1);
final posts = await country.posts; // Posts through users

Eager Loading

Load relationships efficiently to avoid N+1 query problems.

dart
// Lazy loading (N+1 problem)
final users = await User().query.get();
for (final user in users) {
  await user.load('posts'); // N queries
}

// Eager loading (2 queries)
final users = await User().query.withRelations(['posts']).get();
for (final user in users) {
  final posts = user.getRelation('posts'); // Already loaded
}

// Multiple relationships
final users = await User().query.withRelations(['posts', 'profile']).get();

// Nested relationships
final users = await User().query.withRelations([
  'posts',
  // Note: Nested relations would need custom implementation
]).get();

// Conditional eager loading
final users = await User().query
    .withRelations(['posts'])
    .where('is_active', '=', true)
    .get();
dart
// Load relationships after model retrieval
final user = await User().query.where('id', '=', 1).first();

// Load single relationship
await user.load('posts');

// Load multiple relationships
await user.load(['posts', 'profile']);

// Check if relationship is loaded
if (user.isRelationLoaded('posts')) {
  final posts = user.getRelation('posts');
}

// Load missing relationships only
await user.loadMissing(['posts', 'profile']);

// Load with constraints (would need custom implementation)
await user.load('posts');
// Then filter manually
final posts = user.getRelation('posts') as List<Post>;
final publishedPosts = posts.where((post) => post.published == true).toList();
dart
// Limit related records
final users = await User().query
    .withRelations(['posts'])
    .get();

// Then manually limit posts for each user
for (final user in users) {
  final posts = user.getRelation('posts') as List<Post>;
  final limitedPosts = posts.take(5).toList();
  user.setRelation('posts', limitedPosts);
}

// Order related records (would need custom query)
final users = await User().query
    .withRelations(['posts'])
    .get();

// Filter related records manually
for (final user in users) {
  final posts = user.getRelation('posts') as List<Post>;
  final publishedPosts = posts.where((post) => post.published == true).toList();
  user.setRelation('posts', publishedPosts);
}

// Count related records
final users = await User().query.get();
for (final user in users) {
  await user.load('posts');
  final postsCount = (user.getRelation('posts') as List).length;
  user.setAppended('posts_count', postsCount);
}

// Note: Advanced constraining would need custom query builder extensions

Model Events

Hook into model lifecycle events to perform additional actions.

dart
// app/models/User.dart
class User extends KhademModel<User> with Timestamps, HasRelationships {
  // Note: Event system would need to be implemented
  // The current framework doesn't have built-in events like Laravel

  // You could implement custom event handling
  @override
  Future<void> save() async {
    // Custom pre-save logic
    if (password != null && password!.isNotEmpty) {
      // Hash password before saving
      password = 'hashed_' + password!;
    }

    touchUpdated();
    if (id == null) {
      touchCreated();
    }

    await super.save();

    // Custom post-save logic
    // Send welcome email, etc.
  }

  @override
  Future<void> delete() async {
    // Custom pre-delete logic
    // Delete related records, etc.

    await super.delete();

    // Custom post-delete logic
  }
}

// Available operations that can be overridden:
// - save() - called on both create and update
// - delete() - called on delete
// - refresh() - called when refreshing from database
dart
// Note: Observer pattern is not built into the current Khadem framework
// You can implement a similar pattern manually

// app/observers/UserObserver.dart
class UserObserver {
  void beforeCreate(User user) {
    // Generate UUID or other pre-create logic
    print('Creating user: ${user.name}');
  }

  void afterCreate(User user) {
    // Send welcome email, etc.
    print('User created: ${user.name}');
  }

  void beforeUpdate(User user) {
    print('Updating user: ${user.name}');
  }

  void afterUpdate(User user) {
    print('User updated: ${user.name}');
  }

  void beforeDelete(User user) {
    print('Deleting user: ${user.name}');
  }

  void afterDelete(User user) {
    print('User deleted: ${user.name}');
  }
}

// app/models/User.dart
class User extends KhademModel<User> with Timestamps, HasRelationships {
  UserObserver? observer;

  @override
  Future<void> save() async {
    if (observer != null) {
      if (id == null) {
        observer!.beforeCreate(this);
      } else {
        observer!.beforeUpdate(this);
      }
    }

    await super.save();

    if (observer != null) {
      if (id != null && createdAt == updatedAt) {
        observer!.afterCreate(this);
      } else {
        observer!.afterUpdate(this);
      }
    }
  }

  @override
  Future<void> delete() async {
    if (observer != null) {
      observer!.beforeDelete(this);
    }

    await super.delete();

    if (observer != null) {
      observer!.afterDelete(this);
    }
  }
}

// Usage
final user = User();
user.observer = UserObserver();
user.name = 'John Doe';
await user.save(); // Will trigger observer methods

Model Serialization

Convert models to JSON and arrays for API responses.

dart
// app/models/User.dart
class User extends KhademModel<User> with Timestamps, HasRelationships {
  @override
  List<String> get initialHidden => ['password'];

  @override
  Map<String, Type> get casts => {
    'created_at': DateTime,
    'is_active': bool,
  };

  // Custom toJson method
  @override
  Map<String, dynamic> toJson() {
    final json = super.toJson();

    // Add computed properties
    json['full_name'] = name;
    json['avatar_url'] = 'https://example.com/avatar/${id}';
    json['is_admin'] = false; // Add custom logic

    return json;
  }
}

// Usage
final user = await User().query.where('id', '=', 1).first();
final json = user.toJson();

// Serialize collection
final users = await User().query.get();
final usersJson = users.map((user) => user.toJson()).toList();

// Get only specific attributes
final userData = user.only(['id', 'name', 'email']);

// Get all except specific attributes
final userData = user.except(['password', 'remember_token']);

// Make attributes visible/hidden dynamically
user.makeVisible(['phone']);
user.makeHidden(['created_at', 'updated_at']);
dart
// app/resources/UserResource.dart
class UserResource {
  final User user;

  UserResource(this.user);

  Map<String, dynamic> toArray() {
    return {
      'id': user.id,
      'name': user.name,
      'email': user.email,
      'avatar_url': 'https://example.com/avatar/${user.id}',
      'is_admin': false, // Add custom logic
      'created_at': user.createdAt,
      'updated_at': user.updatedAt,
      'posts_count': user.getAppended('posts_count') ?? 0,
      'profile': user.getRelation('profile') != null
          ? ProfileResource(user.getRelation('profile')).toArray()
          : null,
    };
  }

  static List<Map<String, dynamic>> collection(List<User> users) {
    return users.map((user) => UserResource(user).toArray()).toList();
  }
}

// Usage in controller
final users = await User().query.get();
for (final user in users) {
  await user.load('posts');
  user.setAppended('posts_count', (user.getRelation('posts') as List).length);
}
return UserResource.collection(users);
dart
// app/resources/UserResource.dart
class UserResource {
  final User user;
  final bool isAdmin;

  UserResource(this.user, {this.isAdmin = false});

  Map<String, dynamic> toArray() {
    final result = {
      'id': user.id,
      'name': user.name,
      'email': user.email,
    };

    // Only include for admin users
    if (isAdmin) {
      result['email_verified_at'] = user.emailVerifiedAt;
      result['role'] = 'user'; // Add custom logic
      result['permissions'] = ['read']; // Add custom logic
    }

    // Only include if user owns the resource
    if (user.id == 1) { // Add ownership check logic
      result['phone'] = user.phone;
    }

    return result;
  }
}

// Helper methods
Map<String, dynamic> mergeWhen(bool condition, Map<String, dynamic> attributes) {
  return condition ? attributes : {};
}

// Usage
final user = await User().query.where('id', '=', 1).first();
final resource = UserResource(user, isAdmin: true);
final data = resource.toArray();

On this page