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;
// #endregion relation-belongs-to-many-pivot
}
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'];
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;
}
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']);
}
}
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
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]);
}
}
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.