Skip to content

Relationships

Arkormˣ supports relationships with eager loading and constrained relationship querying.

Define relationships

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

ts
class Post extends Model<'posts'> {
  protected static override delegate = 'posts';

  tags() {
    return this.morphToMany(
      Tag,
      'taggable',
      'taggables',
      'taggableId',
      'tagId',
      'id',
      'id',
    );
  }
}

Single-result relationships support withDefault():

  • belongsTo
  • hasOne
  • hasOneThrough
  • morphOne

Use it when a missing related record should resolve to a fallback model instead of null.

ts
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
ts
user.profile().withDefault(new Profile({ bio: 'Not provided yet' }));

user.avatar().withDefault((parent) => ({
  url: `/images/default-${parent.getAttribute('id')}.png`,
}));

Eager loading

ts
await User.query().with('posts').get();

await User.query()
  .with({
    posts: (query) => query.latest().limit(5),
  })
  .get();

Relationship filters and aggregates

ts
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

ts
const user = await User.query().firstOrFail();

await user.posts().get();
await user.posts().first();
await user.posts().where({ published: true }).getResults();