One row, many rows
hasMany reads the key off the other table, manyToMany goes through a junction collection, and multiple keeps a list of ids on the row itself.
A barber takes many appointments and offers many services. Neither list fits in a column on the barber's row, so one of these three methods holds it instead.
hasMany, when the other table holds the key
An appointment already points at a barber. hasMany is the same link read from
the other end.
barber: f.relation("barbers").required(),appointments: f.relation("appointments").hasMany({
foreignKey: "barber",
relationName: "barber",
}),Both options name that same barber field. Only foreignKey is required by the
signature, but relationName is the one that does the work: loading, filtering
and cascading each look it up among the target's field names, find the
belongsTo, and read the key column off it. Leave it out or name a field that is
not there, and the list comes back empty, its filters drop out and its cascade
is skipped. Nothing has to be declared on the belongsTo side for any of this.
.hasMany() also takes onDelete.
The field owns no column here. It is absent from the row until
with populates it as an array.
manyToMany, through a junction collection
A junction is an ordinary collection holding one belongsTo per side.
export const barberServices = collection("barber_services").fields(({ f }) => ({
barber: f.relation(() => barbers).required(),
service: f.relation(() => services).required(),
}));() => barbers is the lazy form of the target. Use it when two collection files
import each other, and the plain string name otherwise.
Each side then declares the same junction from its own point of view.
services: f.relation("services").manyToMany({
through: "barber_services",
sourceField: "barber",
targetField: "service",
}),barbers: f.relation("barbers").manyToMany({
through: "barber_services",
sourceField: "service",
targetField: "barber",
}),through names the junction collection. sourceField is the junction field
pointing back at this collection and targetField the one pointing at the
other. Both read as optional in the signature and both are needed at runtime.
Leave either out and the relation neither loads nor writes.
Like hasMany, neither side owns a column for this. The junction rows hold both
keys, and with is what turns them into an array of rows.
multiple, a list of ids on the row
.multiple() replaces the single foreign key with one jsonb column holding an
array of ids. It is the only to-many form that keeps the link on this row, so
the ids are the value and no second query expands them.
featured: f.relation("services").multiple(),`.multiple()` takes no writes
An array under a relation key is routed to the nested-mutation path, and that
path has no branch for this kind, so the column is never written. with skips
it in the same way. Use manyToMany for any set the API maintains.
Filtering the many side
hasMany and manyToMany take three quantifiers, each a where against the
target collection.
// At least one active service.
where: {
services: {
some: {
isActive: {
eq: true;
}
}
}
}
// No services at all.
where: {
services: {
none: {
}
}
}
// Every service active.
where: {
services: {
every: {
isActive: {
eq: true;
}
}
}
}some compiles to EXISTS, none to its negation, and every to "no related
row fails this test".
A .multiple() field is a jsonb array, so it filters as an array rather than
by joining: contains, containsAll, containsAny, isEmpty, isNotEmpty,
count, isNull and isNotNull.
where: {
featured: {
contains: serviceId;
}
}Order in the chain
These three methods declare a whole field rather than adding to one, so the inferred type keeps only what is chained after them. The runtime keeps everything, and that gap is what bites.
// The type says nullable. The column is emitted NOT NULL.
f.relation("services").required().multiple();
// The two agree.
f.relation("services").multiple().required();Settle the kind first, then refine. .required(), .label(), .localized(),
.onDelete() and .relationName() all carry state forward, so once the kind is
settled they compose in any order.
Related
- Relations for the belongsTo this page builds on.
- Loading related rows for
with. - Writing relations for
connect,createandset.
Temporal values
Three field types, two value shapes. An instant crosses every boundary as a `Date`. A calendar day and a clock reading cross as exact strings and never become one.
Writing relations
create and update take a nested mutation under the relation's own key, so one call can link an existing row, insert a new one, or replace a whole set.