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;
}

Choosing A Relation Type

  • belongsTo: this model stores the foreign key.
  • hasOne/hasMany: related model stores the foreign key.
  • belongsToMany: join/pivot table between both models.
  • Polymorphic (morph*): one association can point to multiple model types.

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;
}

:::note 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);
}
}

Use eager loading for list/detail endpoints that always need related data. Use lazy loading for optional or rarely accessed relations.

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

:::note 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');
}
}

Verify Relation Behavior

  1. Add tests for at least one eager-load path.
  2. Add tests for one relation mutation path (associate / attach / sync).
  3. Confirm no N+1 behavior in hot list endpoints.

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.

Read This Next