Skip to main content

Relations

Load related data efficiently using the include clause.

Including Relations

One-to-Many

Load a parent's children:

const author = await db.findOne("author", {
select: ["id", "name"],
include: {
books: {
select: ["id", "title"],
},
},
where: { name: "Jane Austen" },
});

// author.books is an array of { id, title }

Many-to-One

Load a child's parent:

const books = await db.findMany("book", {
select: ["id", "title"],
include: {
author: {
select: ["id", "name"],
},
},
});

// Each book has an author object: { id, name }

Many-to-Many

Load through a junction table:

const books = await db.findMany("book", {
select: ["id", "title"],
include: {
tags: {
select: ["id", "name"],
},
},
});

// Each book has a tags array: [{ id, name }, ...]

Nested Includes

Include relations multiple levels deep:

const author = await db.findOne("author", {
select: ["id", "name"],
include: {
books: {
select: ["id", "title"],
include: {
tags: {
select: ["id", "name"],
},
},
},
},
where: { name: "Jane Austen" },
});

// author.books[0].tags is available

Filtering Relations

Apply WHERE clauses to included relations:

const author = await db.findOne("author", {
select: ["id", "name"],
include: {
books: {
select: ["id", "title", "published"],
where: { published: true }, // Only published books
},
},
where: { name: "Jane Austen" },
});

With operators:

import { $gte } from "@casekit/orm";

const author = await db.findOne("author", {
select: ["id", "name"],
include: {
books: {
select: ["id", "title", "publishedAt"],
where: {
publishedAt: { [$gte]: new Date("2024-01-01") },
},
},
},
where: { id: 1 },
});

Ordering Relations

Sort included records:

const author = await db.findOne("author", {
select: ["id", "name"],
include: {
books: {
select: ["id", "title"],
orderBy: [["title", "asc"]],
},
},
where: { name: "Jane Austen" },
});

Limiting Relations

Limit the number of included records:

const author = await db.findOne("author", {
select: ["id", "name"],
include: {
books: {
select: ["id", "title"],
limit: 5, // Only first 5 books
},
},
where: { name: "Jane Austen" },
});

Combining Options

All include options can be combined:

const author = await db.findOne("author", {
select: ["id", "name"],
include: {
books: {
select: ["id", "title", "publishedAt"],
where: { published: true },
orderBy: [["publishedAt", "desc"]],
limit: 10,
},
},
where: { id: 1 },
});

How It Works

The ORM uses PostgreSQL LATERAL JOINs to efficiently load related data:

  • Many-to-One: Uses standard JOIN, so related records are fetched in a single query
  • One-to-Many / Many-to-Many: Uses LATERAL subqueries to batch-load related records

This means complex nested queries are efficient and avoid the N+1 problem.

Example: Blog with Authors, Posts, and Comments

const authors = await db.findMany("author", {
select: ["id", "name"],
include: {
posts: {
select: ["id", "title", "createdAt"],
include: {
comments: {
select: ["id", "content", "createdAt"],
orderBy: [["createdAt", "desc"]],
limit: 5,
},
},
where: { published: true },
orderBy: [["createdAt", "desc"]],
limit: 10,
},
},
where: { active: true },
orderBy: [["name", "asc"]],
});

This executes as a single optimized query, returning:

  • All active authors, sorted by name
  • Each author's 10 most recent published posts
  • Each post's 5 most recent comments