Drizzle
04 / 06

Relations, Joins & Transactions

Drizzle: Relations, Joins & Transactions

Defining Relations

// src/db/schema.ts — define relations for the relational query API
import { relations } from 'drizzle-orm';

export const usersRelations = relations(users, ({ many }) => ({
  folders: many(folders),
}));

export const foldersRelations = relations(folders, ({ one, many }) => ({
  user: one(users, {
    fields: [folders.userId],
    references: [users.id],
  }),
  parent: one(folders, {
    fields: [folders.parentId],
    references: [folders.id],
    relationName: 'parentFolder',
  }),
  children: many(folders, { relationName: 'parentFolder' }),
  pages: many(pages),
}));

export const pagesRelations = relations(pages, ({ one }) => ({
  folder: one(folders, {
    fields: [pages.folderId],
    references: [folders.id],
  }),
}));

Relational Query API (with relations)

// Requires schema with relations passed to drizzle()
// const db = drizzle(sql, { schema });

// Get user with all their folders
const userWithFolders = await db.query.users.findFirst({
  where: eq(users.id, userId),
  with: {
    folders: {
      orderBy: [asc(folders.name)],
    },
  },
});

// Nested relations
const folderWithPages = await db.query.folders.findFirst({
  where: eq(folders.id, folderId),
  with: {
    pages: {
      where: eq(pages.status, 'proficient'),
      orderBy: [desc(pages.updatedAt)],
      limit: 10,
      columns: {
        id: true,
        title: true,
        status: true,
        updatedAt: true,
        // Exclude heavy content field
        content: false,
      },
    },
    user: {
      columns: { id: true, name: true },
    },
  },
});

// Find many with relations
const allFolders = await db.query.folders.findMany({
  where: eq(folders.userId, userId),
  with: { pages: true },
});

Manual Joins

// Left join — includes rows without matching pages
const foldersWithPageCount = await db.select({
  folder: folders,
  pageCount: count(pages.id),
})
  .from(folders)
  .leftJoin(pages, eq(pages.folderId, folders.id))
  .where(eq(folders.userId, userId))
  .groupBy(folders.id)
  .orderBy(desc(count(pages.id)));

// Inner join — only rows with matching data
const pagesWithFolder = await db.select({
  page: pages,
  folderName: folders.name,
  folderSlug: folders.slug,
})
  .from(pages)
  .innerJoin(folders, eq(pages.folderId, folders.id))
  .where(eq(folders.userId, userId));

// Multiple joins
const fullData = await db.select()
  .from(pages)
  .innerJoin(folders, eq(pages.folderId, folders.id))
  .innerJoin(users, eq(folders.userId, users.id))
  .where(eq(users.clerkId, clerkId));

Transactions

// All operations in transaction are atomic
const result = await db.transaction(async (tx) => {
  // Create folder
  const [folder] = await tx.insert(folders)
    .values({ userId, name: 'New Project', slug: 'new-project' })
    .returning();

  // Create initial pages inside it
  const createdPages = await tx.insert(pages)
    .values([
      { folderId: folder.id, title: 'Overview', slug: 'overview' },
      { folderId: folder.id, title: 'Notes', slug: 'notes' },
    ])
    .returning();

  return { folder, pages: createdPages };
});

// Rollback by throwing inside the callback
const safeTransfer = await db.transaction(async (tx) => {
  const [source] = await tx.select().from(folders).where(eq(folders.id, sourceId)).for('update');

  if (!source) {
    tx.rollback(); // explicit rollback
    return;
  }

  await tx.update(folders).set({ parentId: targetId }).where(eq(folders.id, sourceId));
  return source;
});

Batch Operations

// Execute multiple queries in a single round-trip (Neon/SQLite)
const [userResult, foldersResult, pagesResult] = await db.batch([
  db.select().from(users).where(eq(users.id, userId)),
  db.select().from(folders).where(eq(folders.userId, userId)),
  db.select().from(pages).where(inArray(pages.folderId, folderIds)),
]);

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free