Relationships
Arkormˣ supports relationships with eager loading and constrained relationship querying.
Define relationships
class User extends Model<'users'> {
protected static override delegate = 'users';
posts() {
return this.hasMany(Post, 'authorId', 'id');
}
}
class Post extends Model<'posts'> {
protected static override delegate = 'posts';
author() {
return this.belongsTo(User, 'authorId', 'id');
}
}Supported relationships:
Supported relationship patterns
hasOne
Use hasOne when the current model owns exactly one related record.
class User extends Model<'users'> {
protected static override delegate = 'users';
profile() {
return this.hasOne(Profile, 'userId', 'id');
}
}hasMany
Use hasMany when the current model owns many related records.
class User extends Model<'users'> {
protected static override delegate = 'users';
posts() {
return this.hasMany(Post, 'authorId', 'id');
}
}belongsTo
Use belongsTo on the child side that contains the foreign key.
class Post extends Model<'posts'> {
protected static override delegate = 'posts';
author() {
return this.belongsTo(User, 'authorId', 'id');
}
}belongsToMany
Use belongsToMany for many-to-many relations through a pivot table.
class User extends Model<'users'> {
protected static override delegate = 'users';
roles() {
return this.belongsToMany(
Role,
'roleUsers',
'userId',
'roleId',
'id',
'id',
);
}
}hasOneThrough
Use hasOneThrough to access one distant relation via an intermediate model.
class Mechanic extends Model<'mechanics'> {
protected static override delegate = 'mechanics';
carOwner() {
return this.hasOneThrough(Owner, Car, 'mechanicId', 'carId', 'id', 'id');
}
}hasManyThrough
Use hasManyThrough to access many distant relations via an intermediate model.
class Country extends Model<'countries'> {
protected static override delegate = 'countries';
posts() {
return this.hasManyThrough(Post, User, 'countryId', 'authorId', 'id', 'id');
}
}morphOne
Use morphOne for one polymorphic relation.
class User extends Model<'users'> {
protected static override delegate = 'users';
avatar() {
return this.morphOne(Image, 'imageable', 'id');
}
}morphMany
Use morphMany for many polymorphic related records.
class Post extends Model<'posts'> {
protected static override delegate = 'posts';
comments() {
return this.morphMany(Comment, 'commentable', 'id');
}
}morphToMany
Use morphToMany for polymorphic many-to-many relation through a pivot table.
class Post extends Model<'posts'> {
protected static override delegate = 'posts';
tags() {
return this.morphToMany(
Tag,
'taggable',
'taggables',
'taggableId',
'tagId',
'id',
'id',
);
}
}Default related models
Single-result relationships support withDefault():
belongsTohasOnehasOneThroughmorphOne
Use it when a missing related record should resolve to a fallback model instead of null.
class Profile extends Model<'profiles'> {
protected static override delegate = 'profiles';
user() {
return this.belongsTo(User, 'userId').withDefault({
name: 'Guest User',
email: 'guest@example.com',
});
}
}withDefault() accepts:
- A plain object of related model attributes
- An instance of the related model
- A callback that returns either of the above
user.profile().withDefault(new Profile({ bio: 'Not provided yet' }));
user.avatar().withDefault((parent) => ({
url: `/images/default-${parent.getAttribute('id')}.png`,
}));Eager loading
await User.query().with('posts').get();
await User.query()
.with({
posts: (query) => query.latest().limit(5),
})
.get();Relationship filters and aggregates
await User.query().has('posts').get();
await User.query()
.whereHas('posts', (q) => q.whereKey('published', true))
.get();
await User.query().withCount('posts').get();
await User.query().withExists('posts').get();
await User.query().withSum('posts', 'views').get();Direct relation execution
const user = await User.query().firstOrFail();
await user.posts().get();
await user.posts().first();
await user.posts().where({ published: true }).getResults();