Relationships
Ormed supports common relationship types between models using the @OrmRelation annotation.
Relationship Types
- Has One
- Has Many
- Has Many Through
- Belongs To
- Belongs To Many
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;
}
A one-to-many relationship.
(table: 'users')
class UserWithPosts extends Model<UserWithPosts> {
const UserWithPosts({required this.id, this.posts});
(isPrimaryKey: true)
final int id;
.hasMany(target: UserPost, foreignKey: 'author_id')
final List<UserPost>? posts;
}
(table: 'posts')
class UserPost extends Model<UserPost> {
const UserPost({
required this.id,
required this.authorId,
required this.title,
});
(isPrimaryKey: true)
final int id;
final int authorId;
final String title;
}
Load related models through an intermediate model.
(table: 'authors')
class AuthorWithComments extends Model<AuthorWithComments> {
const AuthorWithComments({required this.id, this.comments});
(isPrimaryKey: true)
final int id;
.hasManyThrough(
target: PostComment,
throughModel: AuthorPost,
foreignKey: 'post_id',
throughForeignKey: 'author_id',
localKey: 'id',
)
final List<PostComment>? comments;
}
The inverse of hasOne/hasMany: this model owns the foreign key.
class PostWithAuthor extends Model<PostWithAuthor> {
const PostWithAuthor({
required this.id,
required this.authorId,
required this.title,
this.author,
});
(isPrimaryKey: true)
final int id;
final int authorId;
final String title;
.belongsTo(target: PostAuthor, foreignKey: 'author_id')
final PostAuthor? author;
}
class PostAuthor extends Model<PostAuthor> {
const PostAuthor({required this.id, required this.name});
(isPrimaryKey: true)
final int id;
final String name;
}
A many-to-many relationship using a pivot table. Ormed automatically handles the pivot table joins for efficient querying.
class PostWithTags extends Model<PostWithTags> {
const PostWithTags({required this.id, this.tags});
(isPrimaryKey: true)
final int id;
// #region relation-belongs-to-many-pivot
.belongsToMany(
Tag,
pivotTable: 'post_tags',
foreignKey: 'post_id',
relatedKey: 'tag_id',
withPivot: ['sort_order', 'note'],
)
final List<Tag>? tags;
class Tag extends Model<Tag> {
const Tag({required this.id, required this.name});
(isPrimaryKey: true)
final int id;
final String name;
}
Use withPivot to include pivot columns when loading the related models. The
selected pivot values are attached as a pivot relation on each related model.
.belongsToMany(
Tag,
pivotTable: 'post_tags',
foreignKey: 'post_id',
relatedKey: 'tag_id',
withPivot: ['sort_order', 'note'],
)
final List<Tag>? tags;
final pivot = tag.getRelation<Map<String, Object?>>('pivot');
final sortOrder = pivot?['sort_order'];
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.
- Morph One
- Morph Many
- Morph To
- Morph To Many
- Morphed By Many
(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;
}
(table: 'posts')
class MorphPostPhotos extends Model<MorphPostPhotos> {
const MorphPostPhotos({required this.id, this.photos = const []});
(isPrimaryKey: true)
final int id;
(ignore: true)
.morphMany(
target: MorphPhoto,
foreignKey: 'imageable_id',
morphType: 'imageable_type',
morphClass: 'Post',
)
final List<MorphPhoto> photos;
}
(table: 'photos')
class MorphPhoto extends Model<MorphPhoto> {
const MorphPhoto({
required this.id,
this.imageableId,
this.imageableType,
this.imageable,
});
(isPrimaryKey: true)
final int id;
(columnName: 'imageable_id')
final int? imageableId;
(columnName: 'imageable_type')
final String? imageableType;
(ignore: true)
.morphTo(foreignKey: 'imageable_id', morphType: 'imageable_type')
final OrmEntity? imageable;
}
(table: 'posts')
class MorphPostTags extends Model<MorphPostTags> {
const MorphPostTags({required this.id, this.tags = const []});
(isPrimaryKey: true)
final int id;
(ignore: true)
.morphToMany(
target: MorphTag,
through: 'taggables',
pivotForeignKey: 'taggable_id',
pivotRelatedKey: 'tag_id',
morphType: 'taggable_type',
morphClass: 'Post',
)
final List<MorphTag> tags;
}
(table: 'tags')
class MorphTag extends Model<MorphTag> {
const MorphTag({required this.id, this.posts = const []});
(isPrimaryKey: true)
final int id;
(ignore: true)
.morphedByMany(
target: MorphPostTags,
through: 'taggables',
pivotForeignKey: 'tag_id',
pivotRelatedKey: 'taggable_id',
morphType: 'taggable_type',
morphClass: 'Post',
)
final List<MorphPostTags> posts;
}
:::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:
- Basic
- Multiple
- Nested
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
}
}
Future<void> multipleRelationsLoading(DataSource dataSource) async {
final posts = await dataSource.query<$Post>().with_([
'author',
'tags',
'comments',
]).get();
}
Future<void> nestedRelationsLoading(DataSource dataSource) async {
// Load author's profile along with author
final posts = await dataSource.query<$Post>().with_(['author.profile']).get();
// Multiple levels
final deepPosts = await dataSource.query<$Post>().with_([
'author.profile',
'comments.user.profile',
]).get();
}
Lazy Loading
Load relations on-demand:
- Load
- Missing
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);
}
}
Future<void> loadMissingExample(DataSource dataSource) async {
final post = await dataSource.query<$Post>().find(1);
if (post != null) {
// Only loads relations that haven't been loaded yet
await post.loadMissing(['author', 'comments']);
}
}
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']);
}
}
}
Touching Related Models
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);
}
}
Creating Related Models
Create new related models through a parent model's hasMany/hasOne relationship:
- Basic
- With DTOs
- Create Many
- Mixed Input Types
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
}
}
Use type-safe DTOs instead of raw maps:
Future<void> createRelationWithDto(DataSource dataSource) async {
final user = await dataSource.query<$UserWithPosts>().first();
if (user != null) {
// Create using InsertDto - type-safe, only insert fields
final post = await user.createRelation<$UserPost>(
'posts',
UserPostInsertDto(title: 'DTO Post'),
);
// Create using UpdateDto - includes id if needed
final anotherPost = await user.createRelation<$UserPost>(
'posts',
UserPostUpdateDto(title: 'UpdateDto Post'),
);
print('Created: ${post.title}, ${anotherPost.title}');
}
}
Create multiple related models at once:
Future<void> createManyRelation(DataSource dataSource) async {
final user = await dataSource.query<$UserWithPosts>().first();
if (user != null) {
// Create multiple related posts at once
final posts = await user.createManyRelation<$UserPost>('posts', [
{'title': 'First Post'},
{'title': 'Second Post'},
{'title': 'Third Post'},
]);
print('Created ${posts.length} posts'); // 3
print(posts.every((p) => p.authorId == user.id)); // true
}
}
Mix different input types in a single call:
Future<void> createManyRelationMixed(DataSource dataSource) async {
final user = await dataSource.query<$UserWithPosts>().first();
if (user != null) {
// Mix different input types in the same call
final posts = await user.createManyRelation<$UserPost>('posts', [
// Map
{'title': 'Map Post'},
// InsertDto
UserPostInsertDto(title: 'InsertDto Post'),
// UpdateDto
UserPostUpdateDto(title: 'UpdateDto Post'),
]);
for (final post in posts) {
print('${post.title} - author: ${post.authorId}');
}
}
}
Event-Suppressed Creation
Create related models without triggering lifecycle events:
- Single
- Many
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}');
}
}
Future<void> createManyQuietlyRelation(DataSource dataSource) async {
final user = await dataSource.query<$UserWithPosts>().first();
if (user != null) {
// Create multiple without events
final posts = await user.createManyQuietlyRelation<$UserPost>('posts', [
{'title': 'Quiet Post 1'},
UserPostInsertDto(title: 'Quiet Post 2'),
UserPostUpdateDto(title: 'Quiet Post 3'),
]);
print('Quietly created ${posts.length} posts');
}
}
Many-to-Many Operations
- Attach
- Sync
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');
}
}
Future<void> syncToggleExample(DataSource dataSource) async {
final post = await dataSource.query<$Post>().first();
if (post != null) {
// Sync: replaces all related models with these
await post.sync('tags', [1, 2, 3]);
// Toggle: add if not present, remove if present
await post.toggle('tags', [1, 2]);
}
}
Verify Relation Behavior
- Add tests for at least one eager-load path.
- Add tests for one relation mutation path (
associate/attach/sync). - Confirm no N+1 behavior in hot list endpoints.
Aggregate Loading
Load aggregate values without fetching all related models:
- Count
- Sum
- Exists
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')}');
}
}
Future<void> sumAggregateExample(DataSource dataSource) async {
final user = await dataSource.query<$User>().first();
if (user != null) {
await user.loadSum(['posts'], 'views');
print('Total views: ${user.getAttribute<num>('posts_views_sum')}');
}
}
Future<void> existsAggregateExample(DataSource dataSource) async {
final user = await dataSource.query<$User>().first();
if (user != null) {
await user.loadExists(['posts']);
if (user.getAttribute<bool>('posts_exists') ?? false) {
print('User has posts');
}
}
}
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.