Relation writes
Attach, detach, and replace related rows from a single create, update, or upsert with connect, disconnect, and set.
Use relation commands when a single-row create, update, or upsert should also attach, detach, or replace related rows. You describe the link and better-drizzle resolves the foreign keys and junction rows inside one transaction.
The examples use author (a to-one relation on posts) and labels (a many-to-many relation from posts to tags through a postTags junction).
const post = await client.posts.create({
data: {
title: 'Relations without the glue',
author: { connect: { email: 'alice@example.com' } },
labels: { connect: [{ slug: 'drizzle' }, { slug: 'typescript' }] },
},
include: { author: true, labels: true },
});No authorId in sight, no junction rows to insert by hand, and post comes
back typed with both relations attached.
Attach existing rows with connect
connect links rows that already exist. On a to-one relation it takes a
single selector; on a to-many relation it takes one selector or an array.
// to-one: resolve the author and write the foreign key
await client.posts.create({
data: {
title: 'Hello',
author: { connect: { email: 'alice@example.com' } },
},
});
// to-many: attach three existing posts to a user
await client.users.update({
where: { id: 1 },
data: {
posts: { connect: [{ id: 2 }, { id: 3 }, { id: 9 }] },
},
});For a many-to-many relation declared with .through() in defineRelations, connect inserts the junction rows for you, as in the labels example at the top: you never name the junction table.
Detach rows with disconnect
disconnect breaks the link and leaves both rows in place. The shape differs
by cardinality, because a to-one relation has nothing to select:
// to-one: `true`, not a selector
await client.posts.update({
where: { id: 10 },
data: {
author: { disconnect: true },
},
});
// to-many: the rows to detach
await client.users.update({
where: { id: 1 },
data: {
posts: { disconnect: [{ id: 2 }, { id: 3 }] },
},
});A to-many disconnect selector must match an existing row, but a row that is
linked to a different parent is skipped rather than rejected.
Replace a relation with set
set makes the relation exactly what you pass, connecting what is missing and
disconnecting what is no longer listed. It is the right command for "these are
the tags now" semantics:
// the post ends up with exactly these two tags
await client.posts.update({
where: { id: 10 },
data: {
labels: { set: [{ slug: 'drizzle' }, { slug: 'orm' }] },
},
});
// an empty array clears the relation
await client.posts.update({
where: { id: 10 },
data: { labels: { set: [] } },
});
// to-one: repoint it, or null it out
await client.posts.update({
where: { id: 10 },
data: { author: { set: { email: 'bob@example.com' } } },
});
await client.posts.update({
where: { id: 10 },
data: { author: { set: null } },
});Reaching for set where connect would do means re-resolving rows that were
already linked. Prefer connect / disconnect for incremental edits and keep
set for replace-everything flows.
upsert follows its branch
upsert applies the rules of whichever branch runs. The create branch
accepts connect only; the update branch accepts all three commands:
await client.posts.upsert({
where: { slug: 'hello-world' },
create: {
slug: 'hello-world',
title: 'Hello world',
author: { connect: { email: 'alice@example.com' } },
},
update: {
title: 'Hello world (revised)',
labels: { set: [{ slug: 'drizzle' }] },
},
});Edge cases
Reference
Which commands a relation accepts depends on its cardinality and on whether you are creating or updating.
| Command | To-one on create | To-one on update | To-many on create | To-many on update |
|---|---|---|---|---|
connect | Yes - one selector | Yes - one selector | Yes - one or many selectors | Yes - one or many selectors |
disconnect | - | true | - | Yes - one or many selectors |
set | - | one selector or null | - | an array of selectors |
set is exclusive
set replaces the entire relation, so it cannot be combined with connect
or disconnect for that same relation. The type system enforces this -
mixing them is a compile error, not a runtime surprise.