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
Method | Description | Usage |
---|---|---|
hasOne() | One-to-one relationship | hasOne('profile', Profile(), foreignKey: 'user_id') |
hasMany() | One-to-many relationship | hasMany('posts', Post(), foreignKey: 'user_id') |
belongsTo() | Inverse one-to-many | belongsTo('user', User(), foreignKey: 'user_id') |
belongsToMany() | Many-to-many relationship | belongsToMany('roles', Role(), foreignKey: 'user_id') |
Polymorphic Relationships
Method | Description | Usage |
---|---|---|
morphOne() | Polymorphic one-to-one | morphOne('image', Image(), morphKey: 'imageable_id', morphType: 'imageable_type') |
morphMany() | Polymorphic one-to-many | morphMany('comments', Comment(), morphKey: 'commentable_id', morphType: 'commentable_type') |
morphTo() | Inverse polymorphic | morphTo('commentable', Post(), morphKey: 'commentable_id', morphType: 'commentable_type') |
morphToMany() | Polymorphic many-to-many | morphToMany('tags', Tag(), morphKey: 'taggable_id', morphType: 'taggable_type') |
morphedByMany() | Inverse polymorphic many-to-many | morphedByMany('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
- 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();
- 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');
}
}
- 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');
}