Skip to main content

Relationships

Ormed supports common relationship types between models using the @OrmRelation annotation.

Relationship Types

A one-to-one relationship where the related model has the foreign key.

(table: 'users')
class UserWithProfile extends Model<UserWithProfile> {
const UserWithProfile({required this.id, this.profile});

(isPrimaryKey: true)
final int id;

.hasOne(target: Profile, foreignKey: 'user_id')
final Profile? profile;
}

(table: 'profiles')
class Profile extends Model<Profile> {
const Profile({required this.id, required this.userId, required this.bio});

(isPrimaryKey: true)
final int id;

final int userId;
final String bio;
}

Polymorphic Relationships

Polymorphic relationships let a model belong to more than one type of model on a single association.

(table: 'users')
class MorphUser extends Model<MorphUser> {
const MorphUser({required this.id, this.avatar});

(isPrimaryKey: true)
final int id;

(ignore: true)
.morphOne(
target: MorphPhoto,
foreignKey: 'imageable_id',
morphType: 'imageable_type',
morphClass: 'User',
)
final MorphPhoto? avatar;
}
Morph-to constraints

morphTo relations do not support constraint callbacks and cannot be used as intermediate segments in nested relation paths. Use morphTo as the final segment when eager loading.

Morph Map Aliases

If your morph types store aliases instead of full model names, register a map before loading relations:

dataSource.registry.registerMorphMap({
'post': Post,
'user': User,
});

Loading Relations

Eager Loading

Load relations upfront with the query:

Future<void> basicEagerLoading(DataSource dataSource) async {
// Load a single relation
final posts = await dataSource.query<$Post>().with_(['author']).get();

for (final post in posts) {
print(post.author?.name); // Already loaded, no additional query
}
}

Lazy Loading

Load relations on-demand:

Future<void> lazyLoading(DataSource dataSource) async {
final post = await dataSource.query<$Post>().find(1);

if (post != null) {
// Load after fetching
await post.load(['author', 'tags']);

print(post.author?.name);
}
}

Checking Relation Status

Future<void> checkLoadedExample(DataSource dataSource) async {
final post = await dataSource.query<$Post>().find(1);

if (post != null) {
if (post.relationLoaded('author')) {
print(post.author?.name);
} else {
await post.load(['author']);
}
}
}

To automatically update related models' updated_at when this model changes, declare the relations to touch:

(touches: ['author', 'tags'])
class Post extends Model<Post> with Timestamps {
.belongsTo(target: User, foreignKey: 'author_id')
User? author;

.belongsToMany(target: Tag, through: 'post_tags')
List<Tag>? tags;
}

When a touched model is saved or deleted, Ormed updates the related models' updated_at (for belongs-to, has-one/has-many, and many-to-many relations).

Manual control is also available:

await post.touch(); // update this model's updated_at
await post.touchOwners(); // update touched relations
Touching requires timestamps

Only models with timestamps enabled (and an updated_at column) are touched.

To suppress touching within a scope:

await Model.withoutTouchingOn(<Type>[Post], () async {
await post.save();
});

Relation Manipulation

Setting Relations

Future<void> associateExample(DataSource dataSource) async {
final postRepo = dataSource.repo<$Post>();
final post = await dataSource.query<$Post>().first();
final user = await dataSource.query<$User>().first();

if (post != null && user != null) {
// Set a belongs-to relationship
post.associate('author', user);
await postRepo.update(post);

// Remove a belongs-to relationship
post.dissociate('author');
await postRepo.update(post);
}
}

Create new related models through a parent model's hasMany/hasOne relationship:

Future<void> createRelationBasic(DataSource dataSource) async {
final user = await dataSource.query<$UserWithPosts>().first();

if (user != null) {
// Create a related post using a map
final post = await user.createRelation<$UserPost>('posts', {
'title': 'My First Post',
});

print(post.authorId); // Automatically set to user.id
}
}

Event-Suppressed Creation

Create related models without triggering lifecycle events:

Future<void> createQuietlyRelation(DataSource dataSource) async {
final user = await dataSource.query<$UserWithPosts>().first();

if (user != null) {
// Create without triggering model events (creating/created/saving/saved)
final post = await user.createQuietlyRelation<$UserPost>('posts', {
'title': 'Silent Post',
});

// Works with DTOs too
final dtoPost = await user.createQuietlyRelation<$UserPost>(
'posts',
UserPostInsertDto(title: 'Silent DTO Post'),
);

print('Quietly created: ${post.title}, ${dtoPost.title}');
}
}

Many-to-Many Operations

Future<void> attachDetachExample(DataSource dataSource) async {
final post = await dataSource.query<$Post>().first();

if (post != null) {
// Attach related models to a many-to-many relationship
await post.attach('tags', [1, 2]);

// With pivot data
await post.attach('tags', [3], pivot: {'added_by': 1});

// Detach related models
await post.detach('tags', [1]);

// Detach all
await post.detach('tags');
}
}

Aggregate Loading

Load aggregate values without fetching all related models:

Future<void> countAggregateExample(DataSource dataSource) async {
final user = await dataSource.query<$User>().first();

if (user != null) {
await user.loadCount(['posts', 'comments']);
// Access via getAttribute
print('Posts: ${user.getAttribute<int>('posts_count')}');
print('Comments: ${user.getAttribute<int>('comments_count')}');
}
}

Preventing N+1 Queries

Use Model.preventLazyLoading() in development to catch N+1 issues:

void preventNPlusOneExample() {
// Enable lazy loading prevention in development
// void main() {
// if (kDebugMode) {
// Model.preventLazyLoading();
// }
// runApp(MyApp());
// }
//
// This throws an exception when you try to access a relation
// that hasn't been eager-loaded, helping catch N+1 issues.
}

This throws an exception when accessing relations that haven't been eager-loaded, helping you identify performance issues early.