Relations and transactions

Define one-to-many relations

Use the current Drizzle 1.0 defineRelations API to connect users and notes in one type-safe relation map.

Drizzle 1.0 defines all relations in one place, with a single defineRelations() call. Older tutorials use a relations() helper per table. 1.0 replaced that API, so check the installed version before copying code from a blog post.

Create src/db/relations.ts:

import { defineRelations } from 'drizzle-orm'
import * as schema from './schema'

export const relations = defineRelations(schema, r => ({
  users: {
    notes: r.many.notes({
      from: r.users.id,
      to: r.notes.authorId,
    }),
  },
  notes: {
    author: r.one.users({
      from: r.notes.authorId,
      to: r.users.id,
      optional: false,
    }),
  },
}))

The first argument is the schema object, so r knows every table and column. The callback returns a map: for each table, the relations you want to be able to query.

Both directions, explicitly

A user has many notes, so under users we add notes: r.many.notes(...). A note has one author, so under notes we add author: r.one.users(...).

from and to name the columns that connect them. From the user side, users.id matches notes.authorId. From the note side, it’s the reverse. Drizzle can infer one side from the other in simple cases, but I write both. When you read this file in a year, you want to see the join columns, not guess them.

The property names, notes and author, are what you’ll type in queries. Pick names that read well: user.notes, note.author.

What optional: false means

By default a one relation is typed as possibly null, because the related row might not exist. optional: false tells TypeScript that note.author is always a user.

That’s a promise, and we can make it because authorId is notNull() with a foreign key. The database guarantees an author exists. If your column were nullable, or the foreign key were missing, leave the option out. TypeScript would believe you, and the runtime null would crash something later.

Connect it to the client

The relations map goes into the Drizzle client. Update src/db/index.ts:

import { relations } from './relations'

export const db = drizzle(process.env.DB_FILE_NAME, { relations })

Now db.query.users and db.query.notes exist, with with options typed from the map. The next lesson uses them.

Check the mapping

Swap the two columns in one relation on purpose, so from: r.users.id becomes from: r.notes.authorId under users. Then run a query like db.query.users.findFirst({ with: { notes: true } }). Depending on the swap, you get a type error or a user whose notes is empty, because the join now compares the wrong columns. Put it back. A relation is only as good as its two column references, which is why I like seeing them written out.

Lesson completed