cocomon

Relations

Practical examples for one-to-one, one-to-many, many-to-many, self-relations, and compound references.

Relations

The generated client is designed around relation-aware inputs and selectors. These examples focus on the generated-client surface rather than low-level query models.

Ownership rule

For any direct relation, exactly one side owns the foreign key.

The owning side is the side with:

  • fields: [...]
  • references: [...]

The non-owning side is usually the navigation field or list field.

One-to-one

One-to-one

model User {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  profile Profile?
}

model Profile {
  id     Int  @id @default(autoincrement())
  bio    String?
  userId Int  @unique
  user   User @relation(fields: [userId], references: [id])
}
final user = await db.user.create(
  data: const UserCreateInput(
    email: 'alice@example.com',
    profile: ProfileCreateNestedOneWithoutUserInput(
      create: ProfileCreateWithoutUserInput(
        bio: 'Ships docs and migrations',
      ),
    ),
  ),
  include: const UserInclude(profile: true),
);

In one-to-one relations, the owning side usually needs a unique local field or field set.

One-to-many

model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  posts Post[]
}

model Post {
  id      Int    @id @default(autoincrement())
  title   String
  userId  Int
  user    User   @relation(fields: [userId], references: [id])
}
await db.user.create(
  data: const UserCreateInput(
    email: 'alice@example.com',
    posts: PostCreateNestedManyWithoutUserInput(
      create: <PostCreateWithoutUserInput>[
        PostCreateWithoutUserInput(title: 'First post'),
        PostCreateWithoutUserInput(title: 'Second post'),
      ],
    ),
  ),
);

The foreign key lives on the Post side because Post owns userId and declares fields plus references.

Implicit many-to-many

model Post {
  id    Int    @id @default(autoincrement())
  title String
  tags  Tag[]
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}
await db.post.create(
  data: PostCreateInput(
    title: 'Ship relation docs',
    tags: TagCreateNestedManyWithoutPostsInput(
      connect: <TagWhereUniqueInput>[
        const TagWhereUniqueInput(name: 'docs'),
        const TagWhereUniqueInput(name: 'orm'),
      ],
    ),
  ),
);

Implicit many-to-many works when both sides are list relations. The storage table is managed by the provider adapters and migration flow.

Referential actions

comon_orm supports referential actions in @relation(...):

  • onDelete: Cascade
  • onDelete: Restrict
  • onDelete: NoAction
  • onDelete: SetNull
  • onDelete: SetDefault
  • the same set for onUpdate

Example:

model Post {
  id       Int  @id @default(autoincrement())
  authorId Int
  author   User @relation(fields: [authorId], references: [id], onDelete: Cascade)
}

Practical rule:

  • SetNull only makes sense when the local relation field is nullable
  • SetDefault only makes sense when the local relation field has a meaningful default

Explicit relation names

Add explicit relation names when:

  • the same two models are connected by more than one relation
  • a model relates to itself
model Employee {
  id        Int        @id @default(autoincrement())
  managerId Int?
  manager   Employee?  @relation("ManagementChain", fields: [managerId], references: [id])
  reports   Employee[] @relation("ManagementChain")
}

Self-relations and compound references

Self-relations require explicit naming when there is more than one endpoint on the same model pair. Compound references are supported end-to-end, including generated unique selectors for compound ids and compound uniques.

model Account {
  tenantId Int
  slug     String
  name     String

  @@id([tenantId, slug])
}

model Profile {
  id          Int     @id @default(autoincrement())
  tenantId    Int
  accountSlug String
  account     Account @relation(fields: [tenantId, accountSlug], references: [tenantId, slug])
}
await db.profile.create(
  data: ProfileCreateInput(
    account: AccountCreateNestedOneWithoutProfilesInput(
      connect: const AccountWhereUniqueInput.tenantIdSlug(
        tenantId: 7,
        slug: 'main',
      ),
    ),
  ),
);

Querying through relations

Relation filters compose into normal WhereInput trees:

final users = await db.user.findMany(
  where: UserWhereInput(
    posts: PostListRelationFilter(
      some: const PostWhereInput(
        title: StringFilter(contains: 'docs'),
      ),
    ),
  ),
  include: const UserInclude(posts: true),
);

On this page