Skip to main content

Introduction

Vania ORM is a powerful Object-Relational Mapping (ORM) system for Dart that provides an elegant, fluent interface for interacting with databases. Built on top of Vania's query builder, it offers a clean, intuitive API for managing database operations while maintaining type safety and performance.

Basic Usage

Defining Models

class User extends Model {
@override
String get tableName => 'users';

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

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

@override
void registerRelations() {
hasMany('posts', Post(), foreignKey: 'user_id');
hasOne('profile', Profile(), foreignKey: 'user_id');
belongsToMany('roles', Role(), foreignKey: 'user_id');
}
}

Model Properties

Core Properties

tableName

Specifies the database table name for the model.

@override
String get tableName => 'users';

Default: Pluralized, snake_case version of class name
Example: User class → users table

primaryKey

Defines the primary key column name.

@override
String get primaryKey => 'user_id';

Default: 'id'

keyType

Specifies the primary key data type.

@override
String get keyType => 'string';

Default: 'int'

incrementing

Indicates if the primary key is auto-incrementing.

@override
bool get incrementing => false;

Default: true

Mass Assignment Protection

fillable

Whitelist of attributes that can be mass assigned.

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

Usage: Only specified attributes can be set via create() or update() methods.
Security: Prevents unauthorized attribute modification.

guarded

Blacklist of attributes that cannot be mass assigned.

@override
List<String> get guarded => ['id', 'password', 'admin'];

Usage: All attributes except guarded ones can be mass assigned.
Note: fillable takes precedence over guarded.

Data Visibility

hidden

Attributes hidden from JSON serialization.

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

Usage: Sensitive data excluded from toJson() output.
Security: Prevents accidental exposure of sensitive information.

Timestamp Management

timestamps

Enables automatic timestamp management.

@override
bool get timestamps => true;

Default: true
Behavior: Automatically sets created_at and updated_at on insert/update.

createdAt

Defines the created timestamp column name.

@override
String get createdAt => 'created_date';

Default: 'created_at'

updatedAt

Defines the updated timestamp column name.

@override
String get updatedAt => 'modified_date';

Default: 'updated_at'

Soft Deletes

softDeletes

Enables soft delete functionality.

@override
bool get softDeletes => true;

Default: false
Behavior: Records are marked as deleted instead of being physically removed.

deletedAt

Defines the soft delete timestamp column name.

@override
String get deletedAt => 'deleted_date';

Default: 'deleted_at'

Database Connection

defaultConnection

Specifies the database connection to use.

@override
String get defaultConnection => 'postgres';

Default: 'mysql'

tablePrefix

Adds a prefix to the table name.

@override
String get tablePrefix => 'app_';

Result: app_users instead of users

CRUD Operations

// Create
final user = await User().create({
'name': 'John Doe',
'email': '[email protected]'
});

// Read
final users = await User().get();
final user = await User().find(1);

// Update
await User().where('id', '=', 1).update({'name': 'Jane Doe'});

// Delete
await User().where('id', '=', 1).delete();

Relationships

Vania ORM supports both standard and polymorphic relationships to model complex data structures. Use include() to eager‑load related records in a single query.


Standard Relationships

One‑to‑One (hasOne / belongsTo)

Link exactly one record in one table to one record in another.

class User extends Model {
@override
void registerRelations() {
hasOne('profile', Profile(), foreignKey: 'user_id');
}
}

class Profile extends Model {
@override
void registerRelations() {
belongsTo('user', User(), foreignKey: 'user_id');
}
}

Schema:

-- users
id | name | email | created_at

-- profiles
id | user_id | bio | avatar | created_at

Usage:

final user    = await User().include('profile').find(1);
final profile = await Profile().include('user').find(1);

print(user['profile']['bio']);
print(profile['user']['name']);

One‑to‑Many (hasMany / belongsTo)

One record relates to multiple records.

class User extends Model {
@override
void registerRelations() {
hasMany('posts', Post(), foreignKey: 'user_id');
}
}

class Post extends Model {
@override
void registerRelations() {
belongsTo('user', User(), foreignKey: 'user_id');
}
}

Schema:

-- users
id | name | email | created_at

-- posts
id | user_id | title | content | created_at

Usage:

final user  = await User().include('posts').find(1);
print('User has ${user['posts'].length} posts');

final posts = await Post().include('user').get();
for (var p in posts) {
print('${p['title']} by ${p['user']['name']}');
}

Many‑to‑Many (belongsToMany)

Link records through a pivot table.

class User extends Model {
@override
void registerRelations() {
belongsToMany('roles', Role(),
pivotTable: 'role_user',
parentPivotKey: 'user_id',
relatedPivotKey: 'role_id'
);
}
}

class Role extends Model {
@override
void registerRelations() {
belongsToMany('users', User(),
pivotTable: 'role_user',
parentPivotKey: 'role_id',
relatedPivotKey: 'user_id'
);
}
}

Schema:


-- users, roles, and pivot role_user

-- users
id | name | email | created_at | updated_at

-- roles
id | title | created_at | updated_at

-- role_user (pivot)
user_id | role_id

Usage:

final user = await User().include('roles').find(1);
for (var r in user['roles']) {
print('User has role: ${r['title']}');
}

Polymorphic Relationships

Polymorphic relations let one model associate with multiple other models via a single table.

One‑to‑One (morphOne / morphTo)

class Post extends Model {
@override
void registerRelations() {
morphOne('image', Image(),
morphKey: 'imageable_id',
morphType: 'imageable_type',
type: 'post'
);
}
}

class User extends Model {
@override
void registerRelations() {
morphOne('avatar', Image(),
morphKey: 'imageable_id',
morphType: 'imageable_type',
type: 'user'
);
}
}

class Image extends Model {
@override
void registerRelations() {
morphTo('imageable', Post(),
morphKey: 'imageable_id',
morphType: 'imageable_type'
);
}
}

Schema:


-- posts, users, and images

-- posts
id | user_id | category_id | title | content | published | created_at | updated_at

-- users
id | name | email | created_at | updated_at

-- images (polymorphic)
id | imageable_id | imageable_type | filename | url | image_type | created_at | updated_at


Usage:

final post  = await Post().include('image').find(1);
final user = await User().include('avatar').find(1);
final image = await Image().include('imageable').find(1);

One‑to‑Many (morphMany)

class Post extends Model {
@override
void registerRelations() {
morphMany('comments', Comment(),
morphKey: 'commentable_id',
morphType: 'commentable_type',
type: 'post'
);
}
}

class Comment extends Model {
@override
void registerRelations() {
morphTo('commentable', Post(),
morphKey: 'commentable_id',
morphType: 'commentable_type'
);
belongsTo('user', User(), foreignKey: 'user_id');
}
}

Usage:

final post    = await Post().include('comments').find(1);
final comment = await Comment()
.include('commentable')
.include('user')
.find(1);

Many‑to‑Many (morphToMany / morphedByMany)

class Post extends Model {
@override
void registerRelations() {
morphToMany('tags', Tag(),
pivotTable: 'taggables',
morphKey: 'taggable_id',
morphType: 'taggable_type',
relatedMorphKey: 'tag_id',
type: 'post'
);
}
}

class Tag extends Model {
@override
void registerRelations() {
morphedByMany('posts', Post(),
pivotTable: 'taggables',
morphKey: 'tag_id',
morphType: 'taggable_type',
relatedMorphKey: 'taggable_id',
type: 'post'
);
}
}

Schema:

-- posts, tags, and pivot taggables

-- posts
id | user_id | category_id | title | content | published | created_at | updated_at

-- tags
id | name | slug | description | created_at | updated_at

-- taggables (polymorphic pivot)
id | tag_id | taggable_id | taggable_type | created_at

Usage:

final post = await Post().include('tags').find(1);
final tag = await Tag().include('posts').find(1);

Relationship Method Reference

Standard Relationships

MethodDescriptionUsage
hasOne()One-to-one relationshiphasOne('profile', Profile(), foreignKey: 'user_id')
hasMany()One-to-many relationshiphasMany('posts', Post(), foreignKey: 'user_id')
belongsTo()Inverse one-to-manybelongsTo('user', User(), foreignKey: 'user_id')
belongsToMany()Many-to-many relationshipbelongsToMany('roles', Role(), foreignKey: 'user_id')

Polymorphic Relationships

MethodDescriptionUsage
morphOne()Polymorphic one-to-onemorphOne('image', Image(), morphKey: 'imageable_id', morphType: 'imageable_type')
morphMany()Polymorphic one-to-manymorphMany('comments', Comment(), morphKey: 'commentable_id', morphType: 'commentable_type')
morphTo()Inverse polymorphicmorphTo('commentable', Post(), morphKey: 'commentable_id', morphType: 'commentable_type')
morphToMany()Polymorphic many-to-manymorphToMany('tags', Tag(), morphKey: 'taggable_id', morphType: 'taggable_type')
morphedByMany()Inverse polymorphic many-to-manymorphedByMany('posts', Post(), morphKey: 'taggable_id', morphType: 'taggable_type')

Relationship Parameters

Standard Relationship Parameters

  • foreignKey: The foreign key column name (e.g., user_id)
  • localKey: The local key column name (default: id)

Polymorphic Relationship Parameters

  • morphKey: The polymorphic ID column name (e.g., commentable_id)
  • morphType: The polymorphic type column name (e.g., commentable_type)
  • type: The type value stored in morphType column (auto-detected from model name)

✅ Correct Usage

// Direct on model
final users = await User().include('posts').get();

// After initial query setup
final users = await User().query.include('posts').get();

// Multiple includes
final users = await User()
.include('posts')
.include('profile')
.include('roles')
.get();

❌ Incorrect Usage

// Don't use include after query constraints
final users = await User()
.where('active', '=', true)
.include('posts') // Wrong placement
.get();

// Don't use include after limit
final users = await User()
.limit(10)
.include('posts') // Wrong placement
.get();

✅ Correct Pattern

// Apply includes first, then constraints
final users = await User()
.include('posts')
.include('profile')
.where('active', '=', true)
.orderBy('created_at', 'desc')
.limit(10)
.get();

Nested Eager Loading

String-based Nested Loading

// Load nested relationships
final users = await User().include('posts.comments.user').get();

// Multiple nested relationships
final users = await User()
.include('posts.comments')
.include('profile.avatar')
.get();

Callback-based Loading with Constraints

// Apply constraints to related models
final users = await User().include('posts', (query) {
return query.where('published', '=', true)
.orderBy('created_at', 'desc')
.limit(5);
}).get();

// Nested with constraints
final users = await User().include('posts', (query) {
return query.where('published', '=', true)
.include('comments', (commentQuery) {
return commentQuery.where('approved', '=', true)
.include('user')
.limit(3);
});
}).get();

Advanced Include Patterns

// Complex nested loading
final posts = await Post()
.include('user.profile')
.include('comments', (query) =>
query.where('approved', '=', true)
.include('user')
.orderBy('created_at', 'desc')
)
.include('tags')
.where('published', '=', true)
.get();

// Polymorphic relationships
final posts = await Post()
.include('comments.commentable')
.get();

Query Building

Basic Queries

// Simple select
final users = await User().get();

// With conditions
final users = await User()
.where('active', '=', true)
.where('age', '>', 18)
.get();

// Ordering and limiting
final users = await User()
.orderBy('created_at', 'desc')
.limit(10)
.get();

// Specific columns
final users = await User()
.select(['id', 'name', 'email'])
.get();

Advanced Queries

// Multiple conditions
final users = await User()
.where('active', '=', true)
.where('age', '>', 18)
.where('city', '=', 'New York')
.get();

// OR conditions
final users = await User()
.where('active', '=', true)
.orWhere('admin', '=', true)
.get();

// WHERE IN
final users = await User()
.whereIn('id', [1, 2, 3, 4, 5])
.get();

// WHERE BETWEEN
final users = await User()
.whereBetween('age', [18, 65])
.get();

// WHERE NULL
final users = await User()
.whereNull('deleted_at')
.get();

// WHERE NOT NULL
final users = await User()
.whereNotNull('email_verified_at')
.get();

Aggregation

// Count
final count = await User().count();

// Count with conditions
final activeCount = await User()
.where('active', '=', true)
.count();

// Sum
final totalSales = await Order().sum('total');

// Average
final avgPrice = await Product().avg('price');

// Min/Max
final minPrice = await Product().min('price');
final maxPrice = await Product().max('price');

Pagination

// Standard pagination
final result = await User().paginate(perPage: 15, page: 1);

// Access pagination data
print('Current page: ${result['current_page']}');
print('Total records: ${result['total']}');
print('Per page: ${result['per_page']}');
print('Last page: ${result['last_page']}');
print('Has more pages: ${result['has_more']}');

// Data
final users = result['data'];

// Simple pagination
final result = await User().simplePaginate(perPage: 10);

Chunking

// Process large datasets in chunks
await User().chunk(100, (List<Map<String, dynamic>> users) {
for (var user in users) {
print('Processing user: ${user['name']}');
}
});

// Chunk by ID
await User().chunkById(100, (List<Map<String, dynamic>> users) {
for (var user in users) {
print('Processing user: ${user['name']}');
}
});

Advanced Features

Soft Deletes

class User extends Model {
@override
bool get softDeletes => true;

@override
String get deletedAt => 'deleted_at';
}

// Soft delete (marks as deleted)
await User().where('id', '=', 1).delete();

// Force delete (permanently removes)
await User().where('id', '=', 1).forceDelete();

// Include soft deleted records
final users = await User().withTrashed().get();

// Only soft deleted records
final users = await User().onlyTrashed().get();

// Restore soft deleted record
await User().where('id', '=', 1).restore();

// Check if record is soft deleted
final user = await User().find(1);
if (user != null && user['deleted_at'] != null) {
print('User is soft deleted');
}

Mass Assignment

// Safe mass assignment
final user = await User().create({
'name': 'John',
'email': '[email protected]',
'password': 'secret' // Will be blocked if in guarded
});

// Update with mass assignment
await User().where('id', '=', 1).update({
'name': 'Jane',
'email': '[email protected]'
});

// Manual attribute setting
final user = User();
user.setAttribute('name', 'John');
user.setAttribute('email', '[email protected]');

// Get attribute
final name = user.getAttribute('name');

// Check if attribute exists
if (user.hasAttribute('email')) {
print('Email is set');
}

JSON Serialization

final user = await User().find(1);
final json = user.toJson(); // Excludes hidden attributes

// Custom serialization
class User extends Model {
@override
List<String> get hidden => ['password', 'remember_token'];

@override
Map<String, dynamic> toJson() {
final json = super.toJson();
json['full_name'] = '${getAttribute('first_name')} ${getAttribute('last_name')}';
json['avatar_url'] = 'https://example.com/avatars/${getAttribute('id')}.jpg';
return json;
}
}

Attribute Accessors & Mutators

class User extends Model {
// Accessor - modify value when retrieving
@override
dynamic getAttribute(String key) {
switch (key) {
case 'full_name':
return '${super.getAttribute('first_name')} ${super.getAttribute('last_name')}';
case 'email':
return super.getAttribute('email')?.toLowerCase();
default:
return super.getAttribute(key);
}
}

// Mutator - modify value when setting
@override
void setAttribute(String key, dynamic value) {
switch (key) {
case 'email':
super.setAttribute('email', value?.toLowerCase());
break;
case 'password':
super.setAttribute('password', _hashPassword(value));
break;
default:
super.setAttribute(key, value);
}
}

String _hashPassword(String password) {
// Hash password logic
return password; // Simplified
}
}

Database Transactions

// Using database transactions
await DB.transaction((transaction) async {
final user = await User().create({
'name': 'John',
'email': '[email protected]'
});

await Profile().create({
'user_id': user['id'],
'bio': 'Hello world'
});

await Post().create({
'user_id': user['id'],
'title': 'First post',
'content': 'This is my first post'
});
});

Best Practices

1. Always define relationships in registerRelations()

@override
void registerRelations() {
hasMany('posts', Post(), foreignKey: 'user_id');
hasOne('profile', Profile(), foreignKey: 'user_id');
belongsToMany('roles', Role(), foreignKey: 'user_id');
}

2. Use include() before query constraints

// Good
final users = await User()
.include('posts')
.include('profile')
.where('active', '=', true)
.get();

// Avoid
final users = await User()
.where('active', '=', true)
.include('posts') // May not work as expected
.get();

3. Protect sensitive attributes

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

@override
List<String> get guarded => ['id', 'admin', 'created_at', 'updated_at'];

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

4. Use appropriate relationship types

// One-to-Many: User has many Posts
hasMany('posts', Post(), foreignKey: 'user_id');

// One-to-One: User has one Profile
hasOne('profile', Profile(), foreignKey: 'user_id');

// Many-to-Many: User belongs to many Roles
belongsToMany('roles', Role(), foreignKey: 'user_id');

// Polymorphic: Post morphs many Comments
morphMany('comments', Comment(),
morphKey: 'commentable_id',
morphType: 'commentable_type'
);

5. Optimize with selective loading

// Load only needed relationships
final users = await User()
.include('posts', (query) =>
query.select(['id', 'title', 'created_at'])
.where('published', '=', true)
.limit(5)
)
.select(['id', 'name', 'email'])
.get();

6. Use chunking for large datasets

// Process large datasets efficiently
await User().chunk(1000, (List<Map<String, dynamic>> users) {
for (var user in users) {
// Process each user
processUser(user);
}
});

7. Handle soft deletes properly

class User extends Model {
@override
bool get softDeletes => true;

// Methods automatically respect soft deletes
final users = await User().get(); // Excludes deleted users
final allUsers = await User().withTrashed().get(); // Includes deleted users
}

Common Mistakes

  1. Wrong include placement
// ❌ Wrong
final users = await User()
.where('active', '=', true)
.include('posts') // May not work as expected
.get();

// ✅ Correct
final users = await User()
.include('posts')
.where('active', '=', true)
.get();
  1. Missing relations
// ❌ Missing registerRelations
class User extends Model {
// Relations not defined
}

// ✅ Proper relations
class User extends Model {
@override
void registerRelations() {
hasMany('posts', Post(), foreignKey: 'user_id');
}
}
  1. Mass assignment vulnerabilities
// ❌ Vulnerable to mass assignment
class User extends Model {
// No fillable or guarded defined
}

// ✅ Protected
class User extends Model {
@override
List<String> get fillable => ['name', 'email'];

@override
List<String> get guarded => ['id', 'admin'];
}

Error Handling

try {
final user = await User().findOrFail(1);
print(user['name']);
} catch (e) {
print('User not found: $e');
}

// Check if record exists
final user = await User().find(1);
if (user != null) {
print(user['name']);
} else {
print('User not found');
}