From d76ec045ae75bb6cfc8c865a77e7aa8c44a6380b Mon Sep 17 00:00:00 2001 From: Serhii Tatarintsev Date: Mon, 6 Jul 2026 16:52:52 +0000 Subject: [PATCH 1/6] docs(orm/next): add data modeling guides Add a data modeling section under Prisma Next docs with three pages: - Overview: models, primary keys (how to pick), scalar fields (how to pick a type), and relations - Relational databases: 1:1, 1:n, m:n (implicit + explicit), and polymorphic relations, plus foreign-key ownership guidance - MongoDB: documents, embed-vs-reference, references, and polymorphic collections Register the section in the Next nav. Note: relation snippets use the in-flight directional `@relation(from:, to:)` syntax and implicit m:n from the unmerged PSL relation-syntax stack (prisma/prisma-next #872-876); verify before publishing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../docs/orm/next/data-modeling/index.mdx | 156 +++++++++++++++ .../docs/orm/next/data-modeling/meta.json | 4 + .../docs/orm/next/data-modeling/mongodb.mdx | 176 +++++++++++++++++ .../data-modeling/relational-databases.mdx | 177 ++++++++++++++++++ apps/docs/content/docs/orm/next/meta.json | 5 +- 5 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 apps/docs/content/docs/orm/next/data-modeling/index.mdx create mode 100644 apps/docs/content/docs/orm/next/data-modeling/meta.json create mode 100644 apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx create mode 100644 apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx diff --git a/apps/docs/content/docs/orm/next/data-modeling/index.mdx b/apps/docs/content/docs/orm/next/data-modeling/index.mdx new file mode 100644 index 0000000000..39b88c44ad --- /dev/null +++ b/apps/docs/content/docs/orm/next/data-modeling/index.mdx @@ -0,0 +1,156 @@ +--- +title: Data modeling overview +description: How to model your application domain in Prisma Next — models, primary keys, scalar fields, and relations. +url: /orm/next/data-modeling +metaTitle: Data modeling in Prisma Next +metaDescription: Learn the building blocks of a Prisma Next data model — models, primary keys, scalar field types, and relations — and how they map to relational and document databases. +badge: early-access +--- + +Data modeling is the step where you describe the shape of your application's data: the entities it works with, the fields each entity carries, and how those entities connect. In Prisma Next you author that description as a **schema**, and Prisma Next compiles it into a versioned [contract](/orm/next) that your code, your migrations, and your tooling all read from. + +This page introduces the four building blocks you use in every Prisma Next schema: + +- [Models](#models) — the entities of your domain +- [Primary keys](#primary-keys) — how each record is identified +- [Scalar fields](#scalar-fields) — the individual values a model stores +- [Relations](#relations) — how models connect to each other + +The building blocks below apply to every database. Where they diverge is in how you model relations, which the database-specific guides linked at the end cover in depth. + +## Models + +A **model** describes one kind of entity in your domain: a user, an order, a blog post. Every model has a unique identity and its own lifecycle, which is what separates it from a plain bag of values. Two posts with the same title are still two different posts, and a user who changes their email address is still the same user. + +You declare a model with the `model` keyword and give it a set of fields: + +```prisma +model User { + id Int @id @default(autoincrement()) + email String + name String? +} +``` + +How a model maps to physical storage, and which models become queryable entry points, depends on the database and is covered in the database-specific guides. + +## Primary keys + +A **primary key** is the field (or fields) that uniquely identifies each record in a model. Every model needs one. You mark it with the `@id` attribute: + +```prisma +model User { + id Int @id @default(autoincrement()) + email String +} +``` + +The value can be generated by the database or by Prisma Next. Common choices: + +- **Auto-incrementing integer** — `Int @id @default(autoincrement())`. Compact and ordered, but sequential and predictable. +- **UUID** — `String @id @default(uuid())`. Globally unique and safe to generate on the client, at the cost of a wider key. +- **MongoDB ObjectId** — `ObjectId @id @map("_id")`. The idiomatic MongoDB identifier, embedding a creation timestamp. + +A key can also span multiple fields. A **composite primary key** uses the `@@id` block attribute, for when a record is identified by a combination of values rather than a single one: + +```prisma +model UserTag { + userId Int + tagId String + + @@id([userId, tagId]) +} +``` + +### How to pick a primary key + +The first decision is **natural vs. surrogate**. A natural key is an attribute that already identifies the record uniquely: a product's SKU, a book's ISBN, a country's ISO code. A surrogate key is a synthetic value with no business meaning: an auto-incrementing integer, a UUID, an ObjectId. + +A **natural key** is the right choice when the value is genuinely stable, unique, and externally assigned — a standardized code your application receives rather than invents. A `Country` keyed by its ISO code (`US`, `DE`), a `Currency` keyed by its ISO code (`USD`, `EUR`), or a `UsState` keyed by its two-letter abbreviation are good fits: the code is the identity, it never changes, and using it as the key means other records store a readable value (`US`) directly instead of an opaque number. Reference and lookup tables like these are where natural keys shine. + +A **surrogate key** is the better default for most entities you create yourself. The reason is stability: natural keys you'd be tempted to use — a user's email, a product's SKU — do change in practice, and changing a primary key is expensive because everything that already points at the old value has to be updated too. A surrogate never changes, so it stays a dependable identifier for the life of the record. Keep the natural attribute as well and enforce it with `@unique`: a `User` gets a surrogate `id` plus `email String @unique`, a `Product` gets a surrogate `id` plus `sku String @unique`. You get a stable key *and* the uniqueness guarantee. + +Once you've chosen a surrogate, pick its type by how the record is created and referenced: + +- **Auto-incrementing integer** (`Int @id @default(autoincrement())`) — the smallest and fastest key to index, and naturally ordered by insertion. Good for an internal `Order` or `Invoice` whose id never leaves your system. The trade-offs: the database assigns the value, so you don't know it until after the insert; it's sequential and therefore guessable, which leaks row counts and invites enumeration if you expose it; and separate sources can't generate ids without colliding. +- **UUID** (`String @id @default(uuid())`) — globally unique and generated in your application before the insert, so you know the id up front and independent sources never collide. A fit for a `User` or `ApiToken` whose id appears in URLs or is created across services, where you don't want to leak counts. The cost is size (a UUID is far wider than an integer) and, for random UUIDs, worse index locality than a sequential key. +- **MongoDB ObjectId** (`ObjectId @id @map("_id")`) — the idiomatic MongoDB key: generated without coordination, compact, and it embeds its own creation time. The default for any MongoDB model, for example a `Post` document. See the [MongoDB guide](/orm/next/data-modeling/mongodb). + +Use a **composite key** when the identity genuinely *is* the combination of two values. The clearest case is a model that links two others: a `UserTag` connecting a user and a tag is identified by the pair `(userId, tagId)`, and a duplicate pair would be meaningless. Avoid composite keys as the identity of ordinary entities, because everything that points at the model then has to carry all of the key's columns. + +## Scalar fields + +A **scalar field** holds a single value of a primitive type. The common scalar types are: + +| Type | Stores | +| ---------- | ----------------------------------------- | +| `String` | Text | +| `Int` | 32-bit integer | +| `BigInt` | 64-bit integer | +| `Float` | Floating-point number | +| `Boolean` | `true` / `false` | +| `DateTime` | Timestamp | +| `Json` | Arbitrary JSON value | +| `ObjectId` | MongoDB document identifier | + +This isn't the whole set. Prisma Next's type system is extensible, so more scalar types — a dedicated `Uuid`, or types contributed by extensions — are available beyond the common ones above. + +Two modifiers change a field's shape: + +- A trailing `?` makes the field **optional** (`name String?`). +- A trailing `[]` makes it a **list** (`tags String[]`). + +### How to pick a data type + +Match the type to the **meaning** of the value, not to how it happens to look: + +- **An identifier is a `String`, not an `Int`, unless you do arithmetic on it.** Zip codes, phone numbers, and order references look numeric but you never add them, they can have leading zeros, and they aren't ordered numerically. Store them as `String`. +- **Money is not a `Float`.** Floating-point types can't represent decimal amounts exactly, so sums drift by fractions of a cent. Store currency as an integer number of minor units (cents), or use a dedicated decimal type where one is available. +- **An instant is a `DateTime`, not a `String`.** A real timestamp type gives you correct comparison, sorting, and range queries; a string gives you none of those and invites format bugs. +- **A fixed set of values is an enum, not a free-form `String`.** When a field only ever holds one of a known set — a status, a role, a priority — an `enum` documents the allowed values and rejects anything else. (Enum authoring differs slightly by database; see the family guides.) + +Size a field for the data it will realistically hold, including future growth. A narrower type (`Int` over `BigInt`) documents intent and costs a little less to store, but the choice is asymmetric: widening a type later — when a counter overflows `Int`, say — is a migration, so when you're unsure, lean toward the type you won't have to change. Mark a field optional (`?`) only when "absent" is a meaningful state distinct from a sensible default; a required field with a default is often the clearer model. + +## Relations + +A **relation** is a connection between two models. Relations are the backbone of most schemas: a user *has* many orders, an order *belongs to* one user, a post *has* many tags. + +Two things describe any relation — its cardinality and which side stores it. + +### Cardinality + +Cardinality is how many records connect on each side. There are three shapes: + +- **One-to-one** — a record on each side links to at most one on the other. A user and their profile; an order and its shipment. +- **One-to-many** — one record links to many, and each of those links back to a single one. A user and their orders; a post and its comments. +- **Many-to-many** — records on both sides link to many. Posts and tags; students and courses. + +### Ownership + +A relation is physically stored by one model holding a link to the other. Which model holds that link, and in what form, is a modeling decision with real consequences — and it's where relational and document databases differ most, so the family guides cover it in depth. + +### Declaring a relation + +You declare a relation with two kinds of field: + +- A **relation field** — a virtual field typed as the related model (for example, `author User`). It has no storage of its own; it tells Prisma Next how to navigate the connection. +- A field that **stores the connection** — the scalar that actually holds the link (for example, `authorId`). It typically references the other model's primary key, though it can point at any unique field. + +The `@relation` attribute ties them together with a **directional** vocabulary: `from` names the local field that holds the link, and `to` names the field it points at. Omit `to` and it defaults to the target model's `@id`. + +```prisma +model Post { + id Int @id @default(autoincrement()) + authorId Int + author User @relation(from: [authorId], to: [id]) +} +``` + +Which model owns the relation, and how each cardinality is stored, is where the two database families diverge. + +## Next steps + +The same four building blocks — models, primary keys, scalar fields, and relations — apply to every Prisma Next schema. Where they diverge is in how you model relations, so continue with the guide for your database: + +- [Relational data modeling](/orm/next/data-modeling/relational-databases) — one-to-one, one-to-many, many-to-many, and polymorphic relations for PostgreSQL and other SQL targets, including how to pick which side stores the relation. +- [MongoDB data modeling](/orm/next/data-modeling/mongodb) — the same shapes in the document family, plus the central decision: embed or reference. diff --git a/apps/docs/content/docs/orm/next/data-modeling/meta.json b/apps/docs/content/docs/orm/next/data-modeling/meta.json new file mode 100644 index 0000000000..a8beab36d3 --- /dev/null +++ b/apps/docs/content/docs/orm/next/data-modeling/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Data modeling", + "pages": ["index", "relational-databases", "mongodb"] +} diff --git a/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx new file mode 100644 index 0000000000..ceadb8a2c3 --- /dev/null +++ b/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx @@ -0,0 +1,176 @@ +--- +title: MongoDB data modeling +description: Modeling data in Prisma Next for MongoDB — documents, embedded documents, references, and when to use each. +url: /orm/next/data-modeling/mongodb +metaTitle: MongoDB data modeling in Prisma Next +metaDescription: How to model data in Prisma Next for MongoDB — documents and collections, embedded documents vs. separate collections, relations, and polymorphic collections. +badge: early-access +--- + +MongoDB stores data as **documents** grouped into **collections**. A document is a nested structure, which gives you a real choice for related data: keep it *inside* a document (**embed** it) or store it in its own collection and link to it by reference. Making that choice well is the core of MongoDB data modeling, and it's what this page focuses on. + +If you haven't yet, start with the [data modeling overview](/orm/next/data-modeling) for models, primary keys, and scalar fields. + +## Documents, collections, and the `_id` key + +A Prisma Next model maps to a MongoDB **collection**, and each record is a **document**. Every document is identified by an `_id` field, which is the primary key. The idiomatic type for it is `ObjectId` — a 12-byte value that is globally unique without coordination and embeds its own creation timestamp. + +```prisma +model User { + id ObjectId @id @map("_id") + name String + email String +} +``` + +The `@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field. Documents can hold nested objects and arrays directly, which is what makes embedding possible. + +## Embed or reference + +This is the decision that shapes every MongoDB data model. Two pieces of related data can either live together in one document (**embedded**) or sit in separate collections linked by an id (**referenced**). + +**Embedding** nests the related data directly inside its parent document — an order and its line items in a single `orders` document, a user and their address in one `users` document. The related data is stored, loaded, and updated as part of the parent, so one read returns the whole thing with no second query, and a write to the document updates parent and children together, atomically. The trade-off is that embedded data has no independent existence: you can't query it on its own, and it rides along on every read of the parent whether you need it or not. + +**Referencing** keeps the related data in its own collection and stores a link — one document's `_id` — on the other. Loading both takes a second lookup, but each record stands on its own: it can be queried, listed, and updated independently, and it isn't duplicated when several parents point at it. + +So embed data that's read and written with its parent and stays bounded in size; reference data that grows without limit, is queried on its own, or is shared by many parents. A quick way to decide: + +| Signal | Example | Lean toward | +| ------ | ------- | ----------- | +| Always loaded with the parent | An order's line items, shown whenever the order is | Embed | +| Small and bounded in size | A user's single mailing address | Embed | +| Has no meaning outside the parent | A blog post's SEO metadata | Embed | +| Grows without limit | A user's activity events, appended forever | Reference | +| Queried, listed, or updated on its own | Products, browsed and filtered independently | Reference | +| Shared by many parents | A tag applied to thousands of posts | Reference | + +A blog post's comments make the trade-off concrete. If you always render them with the post and their count stays modest, embed them. If comments can grow into the thousands, or you need to list every comment by one author across posts, reference them in their own collection. + +## Embedded documents + +You describe the shape of embedded data with a `type` block, then use it as a field. An embedded type has no collection of its own — it exists only inside its parent document. + +```prisma +type Address { + street String + city String + zip String? + country String +} + +model User { + id ObjectId @id @map("_id") + name String + address Address? +} +``` + +The same mechanism covers embedded one-to-one (a single embedded value, `address Address?`) and embedded one-to-many (a list of embedded values): + +```prisma +type CartItem { + productId String + name String + amount Int +} + +model Cart { + id ObjectId @id @map("_id") + items CartItem[] +} +``` + +`Cart.items` stores the line items directly inside the cart document. There is no separate `items` collection and no join to load them. + +Embedding fits data that is *contained* by its parent and has no life of its own: a user's address, an order's line items, a document's metadata. These are value shapes, not entities — an embedded `Address` has no `_id` and can't be queried on its own; it's loaded, updated, and deleted with the document that holds it. + +The one caveat is growth. An embedded array is a good idea only while it stays bounded. Line items on an order are naturally capped; comments or activity events are not, and an unbounded array both slows every read of the parent and marches the document toward the 16 MB ceiling. When an embedded list has no natural limit, reference it instead. + +## References across collections + +When related records need their own collection, link them by storing one document's `_id` in the other and declaring a relation, the same directional `@relation(from:, to:)` used across Prisma Next. + +```prisma +model User { + id ObjectId @id @map("_id") + name String + posts Post[] +} + +model Post { + id ObjectId @id @map("_id") + title String + authorId ObjectId + author User @relation(from: [authorId], to: [id]) +} +``` + +This is a one-to-many by reference: many posts reference one user, and each is queryable on its own. Prisma Next resolves the reference at query time (via a `$lookup` aggregation) when you include the related data. + +**Many-to-many** has no dedicated form in MongoDB; you build it from the same references. The most explicit way is a **join collection** — one small document per pairing, each holding the two ids: + +```prisma +model Post { + id ObjectId @id @map("_id") + title String + taggings Tagging[] +} + +model Tag { + id ObjectId @id @map("_id") + label String + taggings Tagging[] +} + +model Tagging { + id ObjectId @id @map("_id") + postId ObjectId + tagId ObjectId + post Post @relation(from: [postId], to: [id]) + tag Tag @relation(from: [tagId], to: [id]) + + @@unique([postId, tagId]) +} +``` + +Resolving any reference means a `$lookup`, which isn't free — so document models often **denormalize** to skip the join on hot reads. The extended-reference pattern embeds a small copy of the fields a read needs (a comment stores its author's name and avatar) while the full record stays in its own collection. You trade a little duplication, and the work of keeping the copy in sync, for a read that touches one document instead of two collections. + +## Polymorphic collections + +MongoDB collections don't enforce a single shape, so it's idiomatic to store documents of different "types" in one collection and tell them apart with a discriminator field. Prisma Next models this with a base model and variant models, distinguished by that discriminator, with all variants sharing the one collection. + +```prisma +model Post { + id ObjectId @id @map("_id") + title String + kind String + authorId ObjectId + author User @relation(from: [authorId], to: [id]) + + @@discriminator(kind) +} + +model Article { + summary String + + @@base(Post, "article") +} + +model Tutorial { + difficulty String + duration Int + + @@base(Post, "tutorial") +} +``` + +`@@discriminator(kind)` names the field that records each document's variant; `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A query for posts returns articles and tutorials together; you can also narrow to one variant. + +Single-collection polymorphism fits when the variants are handled together far more than separately: a `notifications` collection of email, SMS, and push messages that you query as one stream; an `events` collection of many event types read together for a timeline. Storing them in one collection lets a single query return every variant, and MongoDB's flexible documents mean each variant simply carries its own fields. + +Prefer separate collections when the types rarely appear in the same query, share little structure, or need very different indexes. The test: reach for base + variants when there's a shared identity and shared queries, not merely a family resemblance. + +## Next steps + +- Indexing embedded fields, resolving references with `$lookup`, and querying polymorphic collections are covered in the MongoDB query material. +- Modeling for a relational database instead? See the [relational data modeling guide](/orm/next/data-modeling/relational-databases). diff --git a/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx new file mode 100644 index 0000000000..5439983635 --- /dev/null +++ b/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx @@ -0,0 +1,177 @@ +--- +title: Relational data modeling +description: Modeling relations in Prisma Next for PostgreSQL and other SQL databases — one-to-one, one-to-many, many-to-many, and polymorphic. +url: /orm/next/data-modeling/relational-databases +metaTitle: Relational data modeling in Prisma Next +metaDescription: How to model one-to-one, one-to-many, many-to-many, and polymorphic relations in Prisma Next for PostgreSQL and other SQL databases, including how to choose which side owns the foreign key. +badge: early-access +--- + +In a relational database, each model maps to a **table** and each scalar field to a **column**. Relations between tables are built on **foreign keys**: a column in one table that holds the primary key of a row in another. This page covers the four relation shapes you model in Prisma Next on SQL databases and, for each, when to reach for it and where the foreign key belongs. + +If you haven't yet, start with the [data modeling overview](/orm/next/data-modeling) for models, primary keys, and scalar fields. + +## How relations are declared + +In a relational database the connection is stored as a **foreign key** column. The model that owns the relation carries a scalar field for that column plus a relation field that navigates it, tied together with `@relation(from:, to:)` — see the [overview](/orm/next/data-modeling#relations) for the general mechanics. + +```prisma +model Post { + id Int @id @default(autoincrement()) + authorId Int + author User @relation(from: [authorId], to: [id]) +} +``` + +The side that carries the `from` field owns the foreign key. Which side that should be is the main modeling decision in the shapes below. + +## One-to-many + +A one-to-many (1:n) relation connects one record on one side to many on the other: one user writes many posts, one post belongs to one user. This is the most common relation shape. + +```prisma +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + authorId Int + author User @relation(from: [authorId], to: [id]) +} +``` + +The foreign key always lives on the **"many" side** — `Post.authorId` — because each post points at exactly one user, while a user points at an open-ended list of posts. The `posts Post[]` field on `User` is the back-relation; it stores nothing in the database and is inferred from the foreign key on `Post`. + +Reach for a one-to-many whenever a record "belongs to" one parent and a parent "has many" children: comments on a post, line items on an order, employees in a department. It's the default relation shape, and both one-to-one and many-to-many are refinements of it. + +Because the "one" side's list field is virtual, you never write to it to create the link — you set the foreign key on the "many" side. Deleting a parent raises the question of what happens to its children: block the delete, cascade it, or null the foreign key. That choice is a *referential action*, configured on the relation and covered in the relations reference. + +## One-to-one + +A one-to-one (1:1) relation connects at most one record on each side: a user has at most one profile, a profile belongs to exactly one user. You model it like a one-to-many, then add a `@unique` constraint to the foreign key so the "many" side collapses to "at most one". + +```prisma +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(from: [userId], to: [id]) +} +``` + +The `@unique` on `Profile.userId` is what makes this one-to-one rather than one-to-many: it guarantees no two profiles reference the same user. + +### Which side owns the foreign key? + +Both sides are queryable either way, so the choice is a modeling call. The rule of thumb: the foreign key belongs on the **dependent** side — the record that can't exist on its own. A `Profile` needs a `User`, so `Profile` carries `userId`. The other signals line up with that same side: it's the one you create second (insert the `User`, then its `Profile`), the one the other treats as optional (`profile Profile?`), and the one you'd cascade-delete when the parent goes away. Putting the key there keeps `User` clean, lets a user exist with no profile, and still guarantees every profile has exactly one user. + +If neither side is clearly dependent — a true peer-to-peer 1:1 — put the key on whichever side you query *from* less often, since that's the side you'll traverse across less frequently. + +## Many-to-many + +A many-to-many (m:n) relation connects many records on each side: a post has many tags, a tag applies to many posts. Relational databases can't express this with a single foreign key, so it's always implemented with a **junction table** (also called a join table) that holds one row per connected pair. Prisma Next offers two ways to model it. + +### Implicit many-to-many + +Declare a list field on each side and no junction model. Prisma Next synthesizes the junction table for you (named `_PostToTag`, with two foreign-key columns and a composite primary key) and manages it behind the scenes. + +```prisma +model Post { + id Int @id @default(autoincrement()) + title String + tags Tag[] +} + +model Tag { + id Int @id @default(autoincrement()) + label String @unique + posts Post[] +} +``` + +Use the implicit form when the connection is *just* a connection — you don't need to store anything about the pairing itself. + +### Explicit many-to-many + +When the relationship carries its own data — a timestamp for when the tag was added, who added it, an ordering — model the junction as a real model. Declare `@relation(through:)` on one list field to name the junction; the other side's bare list is inferred. + +```prisma +model User { + id Int @id @default(autoincrement()) + name String + tags Tag[] @relation(through: UserTag) +} + +model Tag { + id Int @id @default(autoincrement()) + name String + users User[] +} + +model UserTag { + userId Int + tagId Int + user User @relation(from: [userId], to: [id]) + tag Tag @relation(from: [tagId], to: [id]) + + @@id([userId, tagId]) +} +``` + +The junction model uses a composite primary key over its two foreign keys (`@@id([userId, tagId])`), which is what enforces "one row per pair". Add any extra fields (for example, `addedAt DateTime @default(now())`) to `UserTag` directly. + +Start with the **implicit** form. It's the least to maintain, and both forms compile to the same junction table underneath, so you lose nothing by starting simple. Switch to the **explicit** form the moment the pairing needs to carry data of its own — when the tag was added, who added it, a sort order — or when you want to query the junction directly, for example "the 20 most recently tagged posts". Once the junction is a model it's an ordinary model: give it fields, indexes, and its own relations. + +Two related cases need explicit disambiguation and are covered in the relations reference: a **self-referential** many-to-many (users following users, both ends pointing at the same model), and **two** many-to-many relations between the same pair of models. Both are resolved by pointing at the junction's fields (`through: Junction.field`, `inverse:`) rather than by relying on inference. + +## Polymorphic relations + +Sometimes several kinds of record share a common base but each carries its own extra fields: every `Task` has a title and a type, but a `Bug` also has a severity and a `Feature` has a target release. Prisma Next models this with a **base model** plus **variant models**, distinguished by a discriminator field. + +```prisma +model Task { + id Int @id @default(autoincrement()) + title String + type String + + @@discriminator(type) +} + +model Bug { + severity String + + @@base(Task, "bug") +} + +model Feature { + targetRelease String? + + @@base(Task, "feature") + + @@map("features") +} +``` + +`@@discriminator(type)` marks the field that records which variant a row is; `@@base(Task, "bug")` declares that `Bug` is a variant of `Task` for the discriminator value `"bug"`. There are two storage layouts, and you pick between them per variant with `@@map`: + +- **Single-table inheritance** — a variant with no `@@map` (like `Bug`) shares the base table. Its variant-specific columns live alongside the base columns. Simple, but variant columns must be nullable at the storage level. +- **Multi-table inheritance** — a variant with its own `@@map` (like `Feature`) gets its own table holding only its extra columns, linked back to the base table's primary key with a cascading foreign key. + +Polymorphism earns its keep when several types are genuinely variations on one thing: you query them together — a single feed of tasks, one stream of events — and each row shares a common core while carrying a few type-specific fields. If you never query the types together, plain separate models are simpler. If the variants differ by only a nullable field or two, a single model with nullable columns is simpler still. Reserve polymorphism for the middle ground: shared identity and shared queries, with real per-variant structure. + +Between the two layouts, **single-table inheritance** keeps every variant in one table, so reads never join — but every variant-specific column has to be nullable (a `Bug`'s columns are null on a `Feature` row), and the table widens as variants accumulate. **Multi-table inheritance** keeps each variant's columns in its own table with their non-null constraints intact, at the cost of a join to read a full variant. Favor single-table when variants are many and thin, multi-table when they're few and each carries substantial structure. Throughout, treat it as a base with variants, not a class hierarchy: there's no inherited behavior, just a shared core and per-variant fields. + +## Next steps + +- Referential actions (what happens to related rows on delete or update) and relation disambiguation (`inverse:`, `through: Junction.field`) are covered in the relations reference. +- Modeling for MongoDB instead? See the [MongoDB data modeling guide](/orm/next/data-modeling/mongodb). diff --git a/apps/docs/content/docs/orm/next/meta.json b/apps/docs/content/docs/orm/next/meta.json index ae5f3be609..0880c7b034 100644 --- a/apps/docs/content/docs/orm/next/meta.json +++ b/apps/docs/content/docs/orm/next/meta.json @@ -4,6 +4,9 @@ "root": true, "pages": [ "---Introduction---", - "index" + "index", + + "---Guides---", + "data-modeling" ] } From 8878659e70327bd82e3efb0ac3ca52590148e9ec Mon Sep 17 00:00:00 2001 From: Serhii Tatarintsev Date: Mon, 6 Jul 2026 16:57:39 +0000 Subject: [PATCH 2/6] docs(orm/next): drop MongoDB many-to-many section Remove the many-to-many modeling content from the MongoDB guide entirely. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../docs/orm/next/data-modeling/mongodb.mdx | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx index ceadb8a2c3..ea6ca073d1 100644 --- a/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx @@ -107,33 +107,7 @@ model Post { This is a one-to-many by reference: many posts reference one user, and each is queryable on its own. Prisma Next resolves the reference at query time (via a `$lookup` aggregation) when you include the related data. -**Many-to-many** has no dedicated form in MongoDB; you build it from the same references. The most explicit way is a **join collection** — one small document per pairing, each holding the two ids: - -```prisma -model Post { - id ObjectId @id @map("_id") - title String - taggings Tagging[] -} - -model Tag { - id ObjectId @id @map("_id") - label String - taggings Tagging[] -} - -model Tagging { - id ObjectId @id @map("_id") - postId ObjectId - tagId ObjectId - post Post @relation(from: [postId], to: [id]) - tag Tag @relation(from: [tagId], to: [id]) - - @@unique([postId, tagId]) -} -``` - -Resolving any reference means a `$lookup`, which isn't free — so document models often **denormalize** to skip the join on hot reads. The extended-reference pattern embeds a small copy of the fields a read needs (a comment stores its author's name and avatar) while the full record stays in its own collection. You trade a little duplication, and the work of keeping the copy in sync, for a read that touches one document instead of two collections. +Resolving a reference means a `$lookup`, which isn't free — so document models often **denormalize** to skip the join on hot reads. The extended-reference pattern embeds a small copy of the fields a read needs (a comment stores its author's name and avatar) while the full record stays in its own collection. You trade a little duplication, and the work of keeping the copy in sync, for a read that touches one document instead of two collections. ## Polymorphic collections From b436d39f09a0b08d7ff1083954261e9cc11ae3fd Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:47:14 +0200 Subject: [PATCH 3/6] docs(data-modeling): plain language, tested syntax, structured key guidance Revision per review feedback, with every schema snippet emit-tested against @prisma-next 0.14.0 (PostgreSQL app + MongoDB app): - Rewrite intros in plain language: idea first, blog example second, terms last. Remove all em dashes. The docs-writer skill gains a "Teach in plain language" section codifying this for other PRs. - Fix syntax that the published packages reject: @relation(from:, to:) does not exist (unsupported argument on 0.14.0 and on main, which accepts fields/references/map/onDelete/onUpdate), so all examples use @relation(fields:, references:). Implicit many-to-many and @relation(through:) are also rejected ("use an explicit join model"), so the m:n section now teaches the explicit junction model as the working pattern and names the implicit form as not supported yet. Verified as PASS: composite @@id junctions, @@discriminator/@@base (both layouts, SQL and Mongo), Mongo type blocks (single + list), uuid()/autoincrement defaults. - Fix broken rendering: bare (1:1) / (1:n) / (m:n) were parsed as remark directives and rendered as "(1\n)". Now backticked; verified intact in rendered HTML. - Break dense guidance into scannable subsections with code blocks: natural vs surrogate keys (Country ISO example, User/Product with @unique), surrogate type choice, data type choice (zip/priceCents/ publishedAt examples), and "Which side owns the foreign key" as an assertive checklist. - MongoDB shown explicitly with tab switchers where it differs (primary keys), cross-links to Fundamentals for querying each shape, "Prompt your coding agent" sections, direct link phrasing. - Park the DR-8679 redirect map, commented out, in next.config.mjs under the shared "Prisma Next URL cutover (DR-8687)" block. Co-Authored-By: Claude Fable 5 --- .claude/skills/docs-writer/SKILL.md | 20 ++ .../docs/orm/next/data-modeling/index.mdx | 242 ++++++++++++------ .../docs/orm/next/data-modeling/mongodb.mdx | 98 ++++--- .../data-modeling/relational-databases.mdx | 166 +++++++----- apps/docs/next.config.mjs | 12 + 5 files changed, 358 insertions(+), 180 deletions(-) diff --git a/.claude/skills/docs-writer/SKILL.md b/.claude/skills/docs-writer/SKILL.md index cac0ebe62a..be885fc132 100644 --- a/.claude/skills/docs-writer/SKILL.md +++ b/.claude/skills/docs-writer/SKILL.md @@ -118,6 +118,26 @@ Better: - Use exact product names: Prisma Postgres, Prisma Compute, Prisma Next. Don't shorten "Prisma Postgres" to "the database" or "Prisma Next" to "the ORM" once a page covers more than one product. - When you tell the reader something is automatic, show the trigger that makes it happen and how to confirm it did. "Compute injects `DATABASE_URL` automatically" needs a follow-up: "Run `prisma compute env` to confirm it's set." +## Teach in plain language + +Open every concept with the plain-words version a newcomer can repeat, then a concrete everyday example, and only then the precise terms. Jargon may appear after the reader has the idea, never as the introduction to it. + +Weak (jargon-first): + +> Data modeling is the step where you describe the shape of your application's data: the entities it works with, the fields each entity carries, and how those entities connect. You author that description as a schema, and it compiles into a versioned contract that your code, migrations, and tooling all read from. + +Better (idea first, example second, terms last): + +> Data modeling is the process of describing the data your application needs and how that data is connected. +> +> For example, a blog has users, posts, and comments. A user has fields like an email and a name. These models also relate to each other: a user can write many posts, and a post can have many comments. +> +> In Prisma Next, you define this structure in a `contract.prisma` file. This file becomes the shared contract between your application code, database migrations, and developer tools. + +The same rule applies inside sections: when a paragraph packs several decisions together, split it into short subsections, one decision each, and show a code block for every option you name. Guidance that lives only in inline code (`Int @id @default(autoincrement())` mid-sentence) belongs in a fenced block with a sentence of its own. + +Never lean on internal vocabulary ("runtime family", "lowering", "execution stack") without a one-line plain definition at first use. + ## Cut the slop Delete these on sight. They add length, not clarity. diff --git a/apps/docs/content/docs/orm/next/data-modeling/index.mdx b/apps/docs/content/docs/orm/next/data-modeling/index.mdx index 39b88c44ad..8096178e6d 100644 --- a/apps/docs/content/docs/orm/next/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/next/data-modeling/index.mdx @@ -1,28 +1,32 @@ --- title: Data modeling overview -description: How to model your application domain in Prisma Next — models, primary keys, scalar fields, and relations. +description: Describe the data your application needs with models, primary keys, scalar fields, and relations. url: /orm/next/data-modeling metaTitle: Data modeling in Prisma Next -metaDescription: Learn the building blocks of a Prisma Next data model — models, primary keys, scalar field types, and relations — and how they map to relational and document databases. +metaDescription: Learn the building blocks of a Prisma Next data model, models, primary keys, scalar field types, and relations, and how they map to relational and document databases. badge: early-access --- -Data modeling is the step where you describe the shape of your application's data: the entities it works with, the fields each entity carries, and how those entities connect. In Prisma Next you author that description as a **schema**, and Prisma Next compiles it into a versioned [contract](/orm/next) that your code, your migrations, and your tooling all read from. +Data modeling is the process of describing the data your application needs and how that data is connected. -This page introduces the four building blocks you use in every Prisma Next schema: +For example, a blog has users, posts, and comments. A user has fields like an email and a name. A post has fields like a title and content. These models also relate to each other: a user can write many posts, and a post can have many comments. -- [Models](#models) — the entities of your domain -- [Primary keys](#primary-keys) — how each record is identified -- [Scalar fields](#scalar-fields) — the individual values a model stores -- [Relations](#relations) — how models connect to each other +In Prisma Next, you define this structure in a `contract.prisma` file. This file becomes the shared contract between your application code, your database migrations, and your developer tools. -The building blocks below apply to every database. Where they diverge is in how you model relations, which the database-specific guides linked at the end cover in depth. +If you are coming from Prisma 7, `contract.prisma` plays a similar role to the `schema.prisma` file you used before. -## Models +This page introduces the four building blocks of every Prisma Next schema: + +- [Models](#models): the things your application works with +- [Primary keys](#primary-keys): how each record is identified +- [Scalar fields](#scalar-fields): the values a model stores +- [Relations](#relations): how models connect to each other -A **model** describes one kind of entity in your domain: a user, an order, a blog post. Every model has a unique identity and its own lifecycle, which is what separates it from a plain bag of values. Two posts with the same title are still two different posts, and a user who changes their email address is still the same user. +These building blocks apply to every database. Where databases differ is in how you model relations, and the [relational](/orm/next/data-modeling/relational-databases) and [MongoDB](/orm/next/data-modeling/mongodb) guides cover that in depth. -You declare a model with the `model` keyword and give it a set of fields: +## Models + +A model describes one kind of record: a user, an order, a blog post. Declare a model with the `model` keyword and give it fields: ```prisma model User { @@ -32,125 +36,213 @@ model User { } ``` -How a model maps to physical storage, and which models become queryable entry points, depends on the database and is covered in the database-specific guides. +Every record of a model has its own identity and its own lifecycle. Two posts with the same title are still two different posts, and a user who changes their email address is still the same user. + +On a relational database, a model becomes a table. On MongoDB, it becomes a collection. ## Primary keys -A **primary key** is the field (or fields) that uniquely identifies each record in a model. Every model needs one. You mark it with the `@id` attribute: +A primary key is the field that uniquely identifies each record. Every model needs one. Mark it with `@id`: -```prisma +```prisma tab="PostgreSQL" model User { id Int @id @default(autoincrement()) email String } ``` -The value can be generated by the database or by Prisma Next. Common choices: +```prisma tab="MongoDB" +model User { + id ObjectId @id @map("_id") + email String + @@map("users") +} +``` + +On MongoDB, the primary key maps to the document's mandatory `_id` field, and `ObjectId` is the idiomatic type for it. -- **Auto-incrementing integer** — `Int @id @default(autoincrement())`. Compact and ordered, but sequential and predictable. -- **UUID** — `String @id @default(uuid())`. Globally unique and safe to generate on the client, at the cost of a wider key. -- **MongoDB ObjectId** — `ObjectId @id @map("_id")`. The idiomatic MongoDB identifier, embedding a creation timestamp. +### Natural keys -A key can also span multiple fields. A **composite primary key** uses the `@@id` block attribute, for when a record is identified by a combination of values rather than a single one: +A natural key is a value that already identifies the record in the real world. Use one when the value is stable, unique, and assigned outside your application: a standardized code you receive rather than invent. + +Reference tables are the classic fit. A country is its ISO code, and the code never changes: ```prisma -model UserTag { - userId Int - tagId String +model Country { + code String @id + name String +} +``` - @@id([userId, tagId]) +Records that point at `Country` now store a readable value (`US`, `DE`) instead of an opaque number. Currencies (`USD`, `EUR`) and similar lookup tables work the same way. + +### Surrogate keys + +A surrogate key is a generated value with no business meaning: an auto-incrementing integer, a UUID, an ObjectId. It is the better default for records your application creates. + +The reason is stability. Values you might be tempted to use as a key, like an email address or a product SKU, change in practice. Changing a primary key is expensive because every record that points at the old value must be updated too. A surrogate key never changes. + +Keep the natural value as a regular field and enforce its uniqueness with `@unique`. You get a stable key and the uniqueness guarantee: + +```prisma +model User { + id Int @id @default(autoincrement()) + email String @unique +} + +model Product { + id Int @id @default(autoincrement()) + sku String @unique +} +``` + +### Which surrogate type to pick + +Pick by how the record is created and where its id travels: + +```prisma +// Auto-incrementing integer: smallest and fastest to index, +// ordered by insertion. Good for internal records whose id +// never leaves your system. +model Invoice { + id Int @id @default(autoincrement()) +} +``` + +The database assigns the value, so you only know it after the insert. It is also sequential, so it leaks row counts and invites guessing if you expose it in URLs. + +```prisma +// UUID: globally unique, generated before the insert. +// Good for ids that appear in URLs or are created across services. +model ApiToken { + id String @id @default(uuid()) } ``` -### How to pick a primary key +A UUID is wider than an integer and random UUIDs index a little worse, but independent services never collide and nothing is leaked. -The first decision is **natural vs. surrogate**. A natural key is an attribute that already identifies the record uniquely: a product's SKU, a book's ISBN, a country's ISO code. A surrogate key is a synthetic value with no business meaning: an auto-incrementing integer, a UUID, an ObjectId. +```prisma +// ObjectId: the idiomatic MongoDB key. Generated without +// coordination and embeds its own creation time. +model Post { + id ObjectId @id @map("_id") + @@map("posts") +} +``` -A **natural key** is the right choice when the value is genuinely stable, unique, and externally assigned — a standardized code your application receives rather than invents. A `Country` keyed by its ISO code (`US`, `DE`), a `Currency` keyed by its ISO code (`USD`, `EUR`), or a `UsState` keyed by its two-letter abbreviation are good fits: the code is the identity, it never changes, and using it as the key means other records store a readable value (`US`) directly instead of an opaque number. Reference and lookup tables like these are where natural keys shine. +### Composite keys -A **surrogate key** is the better default for most entities you create yourself. The reason is stability: natural keys you'd be tempted to use — a user's email, a product's SKU — do change in practice, and changing a primary key is expensive because everything that already points at the old value has to be updated too. A surrogate never changes, so it stays a dependable identifier for the life of the record. Keep the natural attribute as well and enforce it with `@unique`: a `User` gets a surrogate `id` plus `email String @unique`, a `Product` gets a surrogate `id` plus `sku String @unique`. You get a stable key *and* the uniqueness guarantee. +A composite key spans several fields, declared with `@@id`. Use one when the identity really is the combination, which usually means a model that links two others: -Once you've chosen a surrogate, pick its type by how the record is created and referenced: +```prisma +model UserTag { + userId Int + tagId Int -- **Auto-incrementing integer** (`Int @id @default(autoincrement())`) — the smallest and fastest key to index, and naturally ordered by insertion. Good for an internal `Order` or `Invoice` whose id never leaves your system. The trade-offs: the database assigns the value, so you don't know it until after the insert; it's sequential and therefore guessable, which leaks row counts and invites enumeration if you expose it; and separate sources can't generate ids without colliding. -- **UUID** (`String @id @default(uuid())`) — globally unique and generated in your application before the insert, so you know the id up front and independent sources never collide. A fit for a `User` or `ApiToken` whose id appears in URLs or is created across services, where you don't want to leak counts. The cost is size (a UUID is far wider than an integer) and, for random UUIDs, worse index locality than a sequential key. -- **MongoDB ObjectId** (`ObjectId @id @map("_id")`) — the idiomatic MongoDB key: generated without coordination, compact, and it embeds its own creation time. The default for any MongoDB model, for example a `Post` document. See the [MongoDB guide](/orm/next/data-modeling/mongodb). + @@id([userId, tagId]) +} +``` -Use a **composite key** when the identity genuinely *is* the combination of two values. The clearest case is a model that links two others: a `UserTag` connecting a user and a tag is identified by the pair `(userId, tagId)`, and a duplicate pair would be meaningless. Avoid composite keys as the identity of ordinary entities, because everything that points at the model then has to carry all of the key's columns. +A duplicate `(userId, tagId)` pair would be meaningless, so the pair is the key. Avoid composite keys on ordinary models: everything that points at the model then has to carry all of the key's fields. ## Scalar fields -A **scalar field** holds a single value of a primitive type. The common scalar types are: +A scalar field holds a single value. The common types: -| Type | Stores | -| ---------- | ----------------------------------------- | -| `String` | Text | -| `Int` | 32-bit integer | -| `BigInt` | 64-bit integer | -| `Float` | Floating-point number | -| `Boolean` | `true` / `false` | -| `DateTime` | Timestamp | -| `Json` | Arbitrary JSON value | -| `ObjectId` | MongoDB document identifier | +| Type | Stores | +| ---------- | --------------------------- | +| `String` | Text | +| `Int` | 32-bit integer | +| `BigInt` | 64-bit integer | +| `Float` | Floating-point number | +| `Boolean` | `true` / `false` | +| `DateTime` | Timestamp | +| `Json` | Arbitrary JSON value | +| `ObjectId` | MongoDB document identifier | -This isn't the whole set. Prisma Next's type system is extensible, so more scalar types — a dedicated `Uuid`, or types contributed by extensions — are available beyond the common ones above. +Prisma Next's type system is extensible, so extensions can add more types, such as vectors or geometry. Two modifiers change a field's shape: -- A trailing `?` makes the field **optional** (`name String?`). -- A trailing `[]` makes it a **list** (`tags String[]`). +- A trailing `?` makes the field optional: `name String?` +- A trailing `[]` makes it a list: `tags String[]` ### How to pick a data type -Match the type to the **meaning** of the value, not to how it happens to look: - -- **An identifier is a `String`, not an `Int`, unless you do arithmetic on it.** Zip codes, phone numbers, and order references look numeric but you never add them, they can have leading zeros, and they aren't ordered numerically. Store them as `String`. -- **Money is not a `Float`.** Floating-point types can't represent decimal amounts exactly, so sums drift by fractions of a cent. Store currency as an integer number of minor units (cents), or use a dedicated decimal type where one is available. -- **An instant is a `DateTime`, not a `String`.** A real timestamp type gives you correct comparison, sorting, and range queries; a string gives you none of those and invites format bugs. -- **A fixed set of values is an enum, not a free-form `String`.** When a field only ever holds one of a known set — a status, a role, a priority — an `enum` documents the allowed values and rejects anything else. (Enum authoring differs slightly by database; see the family guides.) +Match the type to what the value means, not to what it looks like. -Size a field for the data it will realistically hold, including future growth. A narrower type (`Int` over `BigInt`) documents intent and costs a little less to store, but the choice is asymmetric: widening a type later — when a counter overflows `Int`, say — is a migration, so when you're unsure, lean toward the type you won't have to change. Mark a field optional (`?`) only when "absent" is a meaningful state distinct from a sensible default; a required field with a default is often the clearer model. +An identifier is a `String`, even when it looks numeric. You never add zip codes or phone numbers together, they can have leading zeros, and they do not sort numerically: -## Relations +```prisma +model Address { + id Int @id @default(autoincrement()) + zip String +} +``` -A **relation** is a connection between two models. Relations are the backbone of most schemas: a user *has* many orders, an order *belongs to* one user, a post *has* many tags. +Money is not a `Float`. Floating-point numbers cannot represent decimal amounts exactly, so sums drift by fractions of a cent. Store an integer number of the smallest unit: -Two things describe any relation — its cardinality and which side stores it. +```prisma +model Product { + id Int @id @default(autoincrement()) + priceCents Int +} +``` -### Cardinality +A point in time is a `DateTime`, not a `String`. A real timestamp type gives you correct comparison, sorting, and range queries: -Cardinality is how many records connect on each side. There are three shapes: +```prisma +model Post { + id Int @id @default(autoincrement()) + publishedAt DateTime +} +``` -- **One-to-one** — a record on each side links to at most one on the other. A user and their profile; an order and its shipment. -- **One-to-many** — one record links to many, and each of those links back to a single one. A user and their orders; a post and its comments. -- **Many-to-many** — records on both sides link to many. Posts and tags; students and courses. +When you are unsure between two sizes, lean toward the wider one (`BigInt` over `Int` for a counter that could grow). Widening a type later is a migration; the wider type today is free. -### Ownership +Mark a field optional (`?`) only when "absent" means something different from a sensible default. A required field with a default is often the clearer model. -A relation is physically stored by one model holding a link to the other. Which model holds that link, and in what form, is a modeling decision with real consequences — and it's where relational and document databases differ most, so the family guides cover it in depth. +## Relations -### Declaring a relation +A relation connects two models: a user has many posts, a post belongs to one user. -You declare a relation with two kinds of field: +Two kinds of field describe a relation: -- A **relation field** — a virtual field typed as the related model (for example, `author User`). It has no storage of its own; it tells Prisma Next how to navigate the connection. -- A field that **stores the connection** — the scalar that actually holds the link (for example, `authorId`). It typically references the other model's primary key, though it can point at any unique field. +- A field that stores the connection. This is a real column or document field, like `authorId`, holding the other record's primary key. +- A relation field, typed as the other model, like `author User`. It stores nothing itself; it tells Prisma Next how to navigate the connection in queries. -The `@relation` attribute ties them together with a **directional** vocabulary: `from` names the local field that holds the link, and `to` names the field it points at. Omit `to` and it defaults to the target model's `@id`. +The `@relation` attribute ties them together: `fields` names the local field that stores the link, and `references` names the field it points at on the other model. ```prisma model Post { id Int @id @default(autoincrement()) authorId Int - author User @relation(from: [authorId], to: [id]) + author User @relation(fields: [authorId], references: [id]) } ``` -Which model owns the relation, and how each cardinality is stored, is where the two database families diverge. +Relations come in three shapes: -## Next steps +- One-to-one: a user has at most one profile. +- One-to-many: a user has many posts. +- Many-to-many: a post has many tags, and a tag appears on many posts. + +How each shape is stored, and which model should hold the connecting field, is where relational and document databases differ. Continue with the guide for your database: + +- [Relational data modeling](/orm/next/data-modeling/relational-databases) for PostgreSQL and other SQL databases: foreign keys, junction tables, and which side owns the key. +- [MongoDB data modeling](/orm/next/data-modeling/mongodb): embedding versus referencing, and polymorphic collections. -The same four building blocks — models, primary keys, scalar fields, and relations — apply to every Prisma Next schema. Where they diverge is in how you model relations, so continue with the guide for your database: +## Prompt your coding agent + +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-contract` skill covers schema authoring. Prompts that map to each section: + +- "Using the prisma-next-contract skill, add a Product model with a surrogate id and a unique sku field." +- "Add a Country reference table keyed by its ISO code." +- "Review my schema for fields that should be an enum or a DateTime instead of a String." +- "Connect Post to User with a foreign key and a relation field." + +## Next steps -- [Relational data modeling](/orm/next/data-modeling/relational-databases) — one-to-one, one-to-many, many-to-many, and polymorphic relations for PostgreSQL and other SQL targets, including how to pick which side stores the relation. -- [MongoDB data modeling](/orm/next/data-modeling/mongodb) — the same shapes in the document family, plus the central decision: embed or reference. +- [Model relational data](/orm/next/data-modeling/relational-databases): one-to-one, one-to-many, many-to-many, and polymorphic relations on SQL databases. +- [Model MongoDB data](/orm/next/data-modeling/mongodb): embed or reference, and single-collection polymorphism. +- [Query your models](/orm/next/fundamentals/reading-data) once the schema is in place. diff --git a/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx b/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx index ea6ca073d1..4217dce559 100644 --- a/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx +++ b/apps/docs/content/docs/orm/next/data-modeling/mongodb.mdx @@ -1,54 +1,55 @@ --- title: MongoDB data modeling -description: Modeling data in Prisma Next for MongoDB — documents, embedded documents, references, and when to use each. +description: Model documents, embedded documents, and references for MongoDB, and decide when to embed and when to reference. url: /orm/next/data-modeling/mongodb metaTitle: MongoDB data modeling in Prisma Next -metaDescription: How to model data in Prisma Next for MongoDB — documents and collections, embedded documents vs. separate collections, relations, and polymorphic collections. +metaDescription: How to model data in Prisma Next for MongoDB, including documents and collections, embedded documents versus references, and polymorphic collections. badge: early-access --- -MongoDB stores data as **documents** grouped into **collections**. A document is a nested structure, which gives you a real choice for related data: keep it *inside* a document (**embed** it) or store it in its own collection and link to it by reference. Making that choice well is the core of MongoDB data modeling, and it's what this page focuses on. +MongoDB stores data as documents grouped into collections. A document can nest objects and arrays directly, which gives you a real choice for related data: keep it inside the document (embed it) or store it in its own collection and link to it (reference it). -If you haven't yet, start with the [data modeling overview](/orm/next/data-modeling) for models, primary keys, and scalar fields. +Making that choice well is the core of MongoDB data modeling, and it is what this page focuses on. New to models and keys? Start with the [data modeling overview](/orm/next/data-modeling). -## Documents, collections, and the `_id` key +## Documents, collections, and the _id key -A Prisma Next model maps to a MongoDB **collection**, and each record is a **document**. Every document is identified by an `_id` field, which is the primary key. The idiomatic type for it is `ObjectId` — a 12-byte value that is globally unique without coordination and embeds its own creation timestamp. +A Prisma Next model maps to a collection, and each record is a document. Every document has an `_id` field as its primary key. `ObjectId` is the idiomatic type for it: ```prisma model User { id ObjectId @id @map("_id") name String email String + @@map("users") } ``` -The `@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field. Documents can hold nested objects and arrays directly, which is what makes embedding possible. +`@map("_id")` maps the model's `id` field to MongoDB's mandatory `_id` field, and `@@map("users")` names the collection. ## Embed or reference -This is the decision that shapes every MongoDB data model. Two pieces of related data can either live together in one document (**embedded**) or sit in separate collections linked by an id (**referenced**). +Two pieces of related data can live together in one document (embedded) or in separate collections linked by an id (referenced). -**Embedding** nests the related data directly inside its parent document — an order and its line items in a single `orders` document, a user and their address in one `users` document. The related data is stored, loaded, and updated as part of the parent, so one read returns the whole thing with no second query, and a write to the document updates parent and children together, atomically. The trade-off is that embedded data has no independent existence: you can't query it on its own, and it rides along on every read of the parent whether you need it or not. +Embedding nests the data inside its parent: an order and its line items in one document. One read returns everything, and a write updates parent and children together, atomically. The trade-off: embedded data has no independent life. You cannot query it on its own, and it rides along on every read of the parent. -**Referencing** keeps the related data in its own collection and stores a link — one document's `_id` — on the other. Loading both takes a second lookup, but each record stands on its own: it can be queried, listed, and updated independently, and it isn't duplicated when several parents point at it. +Referencing keeps the data in its own collection and stores the linked document's `_id`. Loading both takes a second lookup, but each record stands on its own: it can be queried, listed, and updated independently, and it is not duplicated when several parents point at it. -So embed data that's read and written with its parent and stays bounded in size; reference data that grows without limit, is queried on its own, or is shared by many parents. A quick way to decide: +A quick way to decide: | Signal | Example | Lean toward | | ------ | ------- | ----------- | -| Always loaded with the parent | An order's line items, shown whenever the order is | Embed | -| Small and bounded in size | A user's single mailing address | Embed | -| Has no meaning outside the parent | A blog post's SEO metadata | Embed | -| Grows without limit | A user's activity events, appended forever | Reference | -| Queried, listed, or updated on its own | Products, browsed and filtered independently | Reference | -| Shared by many parents | A tag applied to thousands of posts | Reference | +| Always loaded with the parent | An order's line items | Embed | +| Small and bounded in size | A user's mailing address | Embed | +| No meaning outside the parent | A post's SEO metadata | Embed | +| Grows without limit | A user's activity events | Reference | +| Queried or updated on its own | Products in a catalog | Reference | +| Shared by many parents | A tag on thousands of posts | Reference | -A blog post's comments make the trade-off concrete. If you always render them with the post and their count stays modest, embed them. If comments can grow into the thousands, or you need to list every comment by one author across posts, reference them in their own collection. +A blog post's comments make it concrete. If you always render them with the post and their count stays modest, embed them. If comments can grow into the thousands, or you need "every comment by this author across posts", reference them. ## Embedded documents -You describe the shape of embedded data with a `type` block, then use it as a field. An embedded type has no collection of its own — it exists only inside its parent document. +Describe the shape of embedded data with a `type` block, then use it as a field. An embedded type has no collection of its own; it exists only inside its parent document. ```prisma type Address { @@ -62,10 +63,11 @@ model User { id ObjectId @id @map("_id") name String address Address? + @@map("users") } ``` -The same mechanism covers embedded one-to-one (a single embedded value, `address Address?`) and embedded one-to-many (a list of embedded values): +A single embedded value models a one-to-one. A list of embedded values models a one-to-many: ```prisma type CartItem { @@ -75,53 +77,61 @@ type CartItem { } model Cart { - id ObjectId @id @map("_id") - items CartItem[] + id ObjectId @id @map("_id") + items CartItem[] + @@map("carts") } ``` -`Cart.items` stores the line items directly inside the cart document. There is no separate `items` collection and no join to load them. +`Cart.items` lives inside the cart document. There is no separate collection and no join to load the items; every read of a cart returns them. -Embedding fits data that is *contained* by its parent and has no life of its own: a user's address, an order's line items, a document's metadata. These are value shapes, not entities — an embedded `Address` has no `_id` and can't be queried on its own; it's loaded, updated, and deleted with the document that holds it. +An embedded `Address` has no `_id` and cannot be queried on its own. It is loaded, updated, and deleted with the document that holds it. That is the right behavior for values that belong to their parent, and the wrong one for records with their own life. -The one caveat is growth. An embedded array is a good idea only while it stays bounded. Line items on an order are naturally capped; comments or activity events are not, and an unbounded array both slows every read of the parent and marches the document toward the 16 MB ceiling. When an embedded list has no natural limit, reference it instead. +:::warning + +Embedded arrays are only a good idea while they stay bounded. An array that grows forever slows every read of the parent and pushes the document toward MongoDB's 16 MB limit. When a list has no natural cap, reference it instead. + +::: ## References across collections -When related records need their own collection, link them by storing one document's `_id` in the other and declaring a relation, the same directional `@relation(from:, to:)` used across Prisma Next. +When related records need their own collection, store one document's `_id` on the other and declare the relation with the same `@relation(fields:, references:)` used everywhere in Prisma Next: ```prisma model User { id ObjectId @id @map("_id") name String posts Post[] + @@map("users") } model Post { id ObjectId @id @map("_id") title String authorId ObjectId - author User @relation(from: [authorId], to: [id]) + author User @relation(fields: [authorId], references: [id]) + @@map("posts") } ``` -This is a one-to-many by reference: many posts reference one user, and each is queryable on its own. Prisma Next resolves the reference at query time (via a `$lookup` aggregation) when you include the related data. +This is a one-to-many by reference: many posts point at one user, and each post is queryable on its own. When you [include the relation in a query](/orm/next/fundamentals/relations-and-joins), Prisma Next resolves it with a `$lookup` aggregation. -Resolving a reference means a `$lookup`, which isn't free — so document models often **denormalize** to skip the join on hot reads. The extended-reference pattern embeds a small copy of the fields a read needs (a comment stores its author's name and avatar) while the full record stays in its own collection. You trade a little duplication, and the work of keeping the copy in sync, for a read that touches one document instead of two collections. +A `$lookup` is a real cost on hot paths, so document models often denormalize: embed a small copy of the fields a read needs (a comment stores its author's name) while the full record stays in its own collection. You trade some duplication, and the work of keeping the copy in sync, for reads that touch one document. ## Polymorphic collections -MongoDB collections don't enforce a single shape, so it's idiomatic to store documents of different "types" in one collection and tell them apart with a discriminator field. Prisma Next models this with a base model and variant models, distinguished by that discriminator, with all variants sharing the one collection. +MongoDB collections do not enforce a single shape, so it is idiomatic to store documents of different kinds in one collection and tell them apart with a discriminator field. + +Prisma Next models this with a base model plus variant models, all sharing one collection: ```prisma model Post { - id ObjectId @id @map("_id") - title String - kind String - authorId ObjectId - author User @relation(from: [authorId], to: [id]) + id ObjectId @id @map("_id") + title String + kind String @@discriminator(kind) + @@map("posts") } model Article { @@ -138,13 +148,21 @@ model Tutorial { } ``` -`@@discriminator(kind)` names the field that records each document's variant; `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A query for posts returns articles and tutorials together; you can also narrow to one variant. +`@@discriminator(kind)` names the field that records each document's variant. `@@base(Post, "article")` declares `Article` as the `"article"` variant of `Post`. A query for posts returns articles and tutorials together, and you can narrow to one variant in queries. + +Use one polymorphic collection when the variants are handled together far more than separately: a notifications collection of email, SMS, and push messages read as one stream. Prefer separate collections when the types rarely appear in the same query or need very different indexes. + +## Prompt your coding agent -Single-collection polymorphism fits when the variants are handled together far more than separately: a `notifications` collection of email, SMS, and push messages that you query as one stream; an `events` collection of many event types read together for a timeline. Storing them in one collection lets a single query return every variant, and MongoDB's flexible documents mean each variant simply carries its own fields. +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-contract` skill covers document modeling. Prompts that map to each section: -Prefer separate collections when the types rarely appear in the same query, share little structure, or need very different indexes. The test: reach for base + variants when there's a shared identity and shared queries, not merely a family resemblance. +- "Using the prisma-next-contract skill, add an embedded Address type to the User model." +- "Cart items are always read with the cart. Model them as an embedded list." +- "Comments can grow without limit. Move them from an embedded array to a referenced collection." +- "Split the posts collection into Article and Tutorial variants with a discriminator." ## Next steps -- Indexing embedded fields, resolving references with `$lookup`, and querying polymorphic collections are covered in the MongoDB query material. -- Modeling for a relational database instead? See the [relational data modeling guide](/orm/next/data-modeling/relational-databases). +- [Query documents](/orm/next/fundamentals/reading-data) and [resolve references](/orm/next/fundamentals/relations-and-joins) with `.include(...)`. +- Aggregate and reshape documents with the [pipeline builder](/orm/next/fundamentals/advanced-queries#mongodb-pipeline-builder). +- To model relational data, see the [relational data modeling guide](/orm/next/data-modeling/relational-databases). diff --git a/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx index 5439983635..cba07ea843 100644 --- a/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx @@ -1,33 +1,33 @@ --- title: Relational data modeling -description: Modeling relations in Prisma Next for PostgreSQL and other SQL databases — one-to-one, one-to-many, many-to-many, and polymorphic. +description: Model one-to-one, one-to-many, many-to-many, and polymorphic relations for PostgreSQL and other SQL databases. url: /orm/next/data-modeling/relational-databases metaTitle: Relational data modeling in Prisma Next -metaDescription: How to model one-to-one, one-to-many, many-to-many, and polymorphic relations in Prisma Next for PostgreSQL and other SQL databases, including how to choose which side owns the foreign key. +metaDescription: How to model one-to-one, one-to-many, many-to-many, and polymorphic relations in Prisma Next for PostgreSQL, including which side owns the foreign key. badge: early-access --- -In a relational database, each model maps to a **table** and each scalar field to a **column**. Relations between tables are built on **foreign keys**: a column in one table that holds the primary key of a row in another. This page covers the four relation shapes you model in Prisma Next on SQL databases and, for each, when to reach for it and where the foreign key belongs. +In a relational database, each model becomes a table and each scalar field becomes a column. Models connect through foreign keys: a column in one table that holds the primary key of a row in another table. -If you haven't yet, start with the [data modeling overview](/orm/next/data-modeling) for models, primary keys, and scalar fields. +This page shows how to model each relation shape and, for each one, where the foreign key belongs. New to models and keys? Start with the [data modeling overview](/orm/next/data-modeling). ## How relations are declared -In a relational database the connection is stored as a **foreign key** column. The model that owns the relation carries a scalar field for that column plus a relation field that navigates it, tied together with `@relation(from:, to:)` — see the [overview](/orm/next/data-modeling#relations) for the general mechanics. +The model that stores the connection carries two fields: a scalar field for the foreign key column, and a relation field that navigates it. `@relation(fields:, references:)` ties them together: ```prisma model Post { id Int @id @default(autoincrement()) authorId Int - author User @relation(from: [authorId], to: [id]) + author User @relation(fields: [authorId], references: [id]) } ``` -The side that carries the `from` field owns the foreign key. Which side that should be is the main modeling decision in the shapes below. +The side that carries the `fields` argument owns the foreign key. Which side that should be is the main decision in the shapes below. ## One-to-many -A one-to-many (1:n) relation connects one record on one side to many on the other: one user writes many posts, one post belongs to one user. This is the most common relation shape. +A one-to-many relation (`1:n`) connects one record to many: one user writes many posts, and each post belongs to one user. This is the most common relation shape. ```prisma model User { @@ -40,103 +40,129 @@ model Post { id Int @id @default(autoincrement()) title String authorId Int - author User @relation(from: [authorId], to: [id]) + author User @relation(fields: [authorId], references: [id]) } ``` -The foreign key always lives on the **"many" side** — `Post.authorId` — because each post points at exactly one user, while a user points at an open-ended list of posts. The `posts Post[]` field on `User` is the back-relation; it stores nothing in the database and is inferred from the foreign key on `Post`. +The foreign key lives on the "many" side, `Post.authorId`, because each post points at exactly one user, while a user points at an open-ended list of posts. The `posts Post[]` field on `User` is the back-relation: it stores nothing in the database and is inferred from the foreign key on `Post`. -Reach for a one-to-many whenever a record "belongs to" one parent and a parent "has many" children: comments on a post, line items on an order, employees in a department. It's the default relation shape, and both one-to-one and many-to-many are refinements of it. +Use a one-to-many whenever a record belongs to one parent and a parent has many children: comments on a post, line items on an order, employees in a department. -Because the "one" side's list field is virtual, you never write to it to create the link — you set the foreign key on the "many" side. Deleting a parent raises the question of what happens to its children: block the delete, cascade it, or null the foreign key. That choice is a *referential action*, configured on the relation and covered in the relations reference. +Because the list field is virtual, you never write to it. To connect a post to a user, set the foreign key: + +```typescript +const post = await db.orm.public.Post.create({ + title: "Hello", + authorId: user.id, +}); +``` + +See [Relations and joins](/orm/next/fundamentals/relations-and-joins#one-to-many) for querying in both directions. ## One-to-one -A one-to-one (1:1) relation connects at most one record on each side: a user has at most one profile, a profile belongs to exactly one user. You model it like a one-to-many, then add a `@unique` constraint to the foreign key so the "many" side collapses to "at most one". +A one-to-one relation (`1:1`) connects at most one record on each side: a user has at most one profile, and a profile belongs to exactly one user. + +Model it like a one-to-many, then add `@unique` to the foreign key. The unique constraint is what turns "many" into "at most one": no two profiles can reference the same user. ```prisma model User { - id Int @id @default(autoincrement()) - email String @unique - profile Profile? + id Int @id @default(autoincrement()) + email String @unique } model Profile { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) bio String + userId Int @unique + user User @relation(fields: [userId], references: [id]) +} +``` + +Declare the relation on the side that holds the foreign key (`Profile` here). The mirror field on the other model (`profile Profile?` on `User`) is not supported yet, so [query the relation from the profile side](/orm/next/fundamentals/relations-and-joins#one-to-one). + +### Which side owns the foreign key + +Put the foreign key on the dependent side: the record that cannot exist on its own. + +A profile needs a user. A user does not need a profile. So `Profile` carries `userId`: + +```prisma +model Profile { + id Int @id @default(autoincrement()) userId Int @unique - user User @relation(from: [userId], to: [id]) + user User @relation(fields: [userId], references: [id]) } ``` -The `@unique` on `Profile.userId` is what makes this one-to-one rather than one-to-many: it guarantees no two profiles reference the same user. +The other signals all point at the same side. The dependent record is: -### Which side owns the foreign key? +- The one you create second. Insert the `User`, then its `Profile`. +- The one you would delete when the parent goes away. +- The one whose absence is normal. A user without a profile is fine; a profile without a user is broken data. -Both sides are queryable either way, so the choice is a modeling call. The rule of thumb: the foreign key belongs on the **dependent** side — the record that can't exist on its own. A `Profile` needs a `User`, so `Profile` carries `userId`. The other signals line up with that same side: it's the one you create second (insert the `User`, then its `Profile`), the one the other treats as optional (`profile Profile?`), and the one you'd cascade-delete when the parent goes away. Putting the key there keeps `User` clean, lets a user exist with no profile, and still guarantees every profile has exactly one user. +Placing the key on the dependent side keeps `User` free of profile concerns, lets a user exist with no profile, and guarantees every profile has exactly one user. -If neither side is clearly dependent — a true peer-to-peer 1:1 — put the key on whichever side you query *from* less often, since that's the side you'll traverse across less frequently. +If neither side is clearly dependent, put the key on the side you query from less often. ## Many-to-many -A many-to-many (m:n) relation connects many records on each side: a post has many tags, a tag applies to many posts. Relational databases can't express this with a single foreign key, so it's always implemented with a **junction table** (also called a join table) that holds one row per connected pair. Prisma Next offers two ways to model it. - -### Implicit many-to-many +A many-to-many relation (`m:n`) connects many records on each side: a post has many tags, and a tag applies to many posts. -Declare a list field on each side and no junction model. Prisma Next synthesizes the junction table for you (named `_PostToTag`, with two foreign-key columns and a composite primary key) and manages it behind the scenes. +A single foreign key cannot express this, because each side needs to point at many records. The answer is a junction model: a third model that holds one record per connected pair. ```prisma model Post { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) title String - tags Tag[] + tags PostTag[] } model Tag { - id Int @id @default(autoincrement()) - label String @unique - posts Post[] + id Int @id @default(autoincrement()) + label String @unique + posts PostTag[] } -``` - -Use the implicit form when the connection is *just* a connection — you don't need to store anything about the pairing itself. -### Explicit many-to-many - -When the relationship carries its own data — a timestamp for when the tag was added, who added it, an ordering — model the junction as a real model. Declare `@relation(through:)` on one list field to name the junction; the other side's bare list is inferred. +model PostTag { + postId Int + tagId Int + post Post @relation(fields: [postId], references: [id]) + tag Tag @relation(fields: [tagId], references: [id]) -```prisma -model User { - id Int @id @default(autoincrement()) - name String - tags Tag[] @relation(through: UserTag) + @@id([postId, tagId]) } +``` -model Tag { - id Int @id @default(autoincrement()) - name String - users User[] -} +The junction model is two one-to-many relations back to back. Its composite primary key, `@@id([postId, tagId])`, enforces one record per pair. -model UserTag { - userId Int - tagId Int - user User @relation(from: [userId], to: [id]) - tag Tag @relation(from: [tagId], to: [id]) +Connecting a post to a tag is a plain create on the junction model, and disconnecting is a delete: - @@id([userId, tagId]) -} +```typescript +await db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id }); ``` -The junction model uses a composite primary key over its two foreign keys (`@@id([userId, tagId])`), which is what enforces "one row per pair". Add any extra fields (for example, `addedAt DateTime @default(now())`) to `UserTag` directly. +The junction is an ordinary model, so it can carry data about the pairing itself. When the tag was added, who added it, a sort order: add the field to the junction model. -Start with the **implicit** form. It's the least to maintain, and both forms compile to the same junction table underneath, so you lose nothing by starting simple. Switch to the **explicit** form the moment the pairing needs to carry data of its own — when the tag was added, who added it, a sort order — or when you want to query the junction directly, for example "the 20 most recently tagged posts". Once the junction is a model it's an ordinary model: give it fields, indexes, and its own relations. +```prisma +model PostTag { + postId Int + tagId Int + addedAt DateTime @default(now()) + post Post @relation(fields: [postId], references: [id]) + tag Tag @relation(fields: [tagId], references: [id]) + + @@id([postId, tagId]) +} +``` -Two related cases need explicit disambiguation and are covered in the relations reference: a **self-referential** many-to-many (users following users, both ends pointing at the same model), and **two** many-to-many relations between the same pair of models. Both are resolved by pointing at the junction's fields (`through: Junction.field`, `inverse:`) rather than by relying on inference. +An implicit many-to-many (list fields on both sides with no junction model, as in Prisma 7) is not supported yet: the schema compiler rejects it and asks for an explicit join model. Model the junction explicitly; [Relations and joins](/orm/next/fundamentals/relations-and-joins#many-to-many) shows how to traverse it in one query. ## Polymorphic relations -Sometimes several kinds of record share a common base but each carries its own extra fields: every `Task` has a title and a type, but a `Bug` also has a severity and a `Feature` has a target release. Prisma Next models this with a **base model** plus **variant models**, distinguished by a discriminator field. +Sometimes several kinds of record share a common core but each carries its own extra fields: every `Task` has a title, but a `Bug` also has a severity and a `Feature` has a target release. + +Prisma Next models this with a base model plus variant models. A discriminator field records which variant each row is: ```prisma model Task { @@ -162,16 +188,26 @@ model Feature { } ``` -`@@discriminator(type)` marks the field that records which variant a row is; `@@base(Task, "bug")` declares that `Bug` is a variant of `Task` for the discriminator value `"bug"`. There are two storage layouts, and you pick between them per variant with `@@map`: +`@@discriminator(type)` marks the field that records the variant. `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. + +Each variant picks one of two storage layouts: + +- A variant with no `@@map`, like `Bug`, shares the base table. Its extra columns live alongside the base columns and must be nullable at the storage level, because a `Feature` row has no severity. +- A variant with its own `@@map`, like `Feature`, gets its own table holding only its extra columns, linked back to the base table's primary key. Its columns keep their constraints, at the cost of a join to read a full variant. + +Use polymorphism when you query the variants together (one feed of tasks, one stream of events) and each variant carries real structure of its own. If you never query them together, separate models are simpler. If the variants differ by one nullable field, a single model is simpler still. -- **Single-table inheritance** — a variant with no `@@map` (like `Bug`) shares the base table. Its variant-specific columns live alongside the base columns. Simple, but variant columns must be nullable at the storage level. -- **Multi-table inheritance** — a variant with its own `@@map` (like `Feature`) gets its own table holding only its extra columns, linked back to the base table's primary key with a cascading foreign key. +## Prompt your coding agent -Polymorphism earns its keep when several types are genuinely variations on one thing: you query them together — a single feed of tasks, one stream of events — and each row shares a common core while carrying a few type-specific fields. If you never query the types together, plain separate models are simpler. If the variants differ by only a nullable field or two, a single model with nullable columns is simpler still. Reserve polymorphism for the middle ground: shared identity and shared queries, with real per-variant structure. +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-contract` skill covers relation modeling. Prompts that map to each section: -Between the two layouts, **single-table inheritance** keeps every variant in one table, so reads never join — but every variant-specific column has to be nullable (a `Bug`'s columns are null on a `Feature` row), and the table widens as variants accumulate. **Multi-table inheritance** keeps each variant's columns in its own table with their non-null constraints intact, at the cost of a join to read a full variant. Favor single-table when variants are many and thin, multi-table when they're few and each carries substantial structure. Throughout, treat it as a base with variants, not a class hierarchy: there's no inherited behavior, just a shared core and per-variant fields. +- "Using the prisma-next-contract skill, add a one-to-many between User and Post with the foreign key on Post." +- "Make the Profile relation one-to-one by adding a unique constraint to its foreign key." +- "Model a many-to-many between Post and Tag with an explicit junction model and an addedAt timestamp." +- "Split Task into Bug and Feature variants with a discriminator field." ## Next steps -- Referential actions (what happens to related rows on delete or update) and relation disambiguation (`inverse:`, `through: Junction.field`) are covered in the relations reference. -- Modeling for MongoDB instead? See the [MongoDB data modeling guide](/orm/next/data-modeling/mongodb). +- [Query relations](/orm/next/fundamentals/relations-and-joins) with `.include(...)`, relation predicates, and junction traversal. +- To model for MongoDB, see the [MongoDB data modeling guide](/orm/next/data-modeling/mongodb). +- Referential actions (what happens to related rows on delete) are covered in the relations reference as it lands. diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index dfc0f742f5..4ba10c435f 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -297,6 +297,18 @@ const config = { destination: "/next/add-to-existing-project/:path*", permanent: false, }, + // ── Prisma Next URL cutover (DR-8687) — DO NOT ENABLE YET ───────────── + // The redirects below retire live Prisma 7 URLs, so they ship only when + // Prisma Next becomes the default docs version (the /orm/next tree moves + // to /orm). Until then, keep your section's redirects here, commented + // out, so the full cutover map builds up in one reviewable place. + // Section owners: append your block below with a DR reference. + // + // DR-8679 Data modeling: + // { source: "/orm/prisma-schema", destination: "/orm/next/data-modeling", permanent: false }, + // { source: "/orm/prisma-schema/data-model/models", destination: "/orm/next/data-modeling", permanent: false }, + // { source: "/orm/prisma-schema/data-model/relations", destination: "/orm/next/data-modeling/relational-databases", permanent: false }, + // ─────────────────────────────────────────────────────────────────────── ]; }, async rewrites() { From 47363adbe0938e51aa6c1336ef6fc64931feb5ea Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:06:29 +0200 Subject: [PATCH 4/6] docs(skill): record the house guide style for hands-on pages Co-Authored-By: Claude Fable 5 --- .claude/skills/docs-writer/SKILL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.claude/skills/docs-writer/SKILL.md b/.claude/skills/docs-writer/SKILL.md index be885fc132..09ad71510a 100644 --- a/.claude/skills/docs-writer/SKILL.md +++ b/.claude/skills/docs-writer/SKILL.md @@ -138,6 +138,8 @@ The same rule applies inside sections: when a paragraph packs several decisions Never lean on internal vocabulary ("runtime family", "lowering", "execution stack") without a one-line plain definition at first use. +For hands-on guides (anything that builds something), follow the house guide style used by /docs/guides/runtimes/bun and the middleware authoring guide: an Introduction stating what the reader builds, Prerequisites, short numbered step headings ("## 1. Create the middleware"), one action per step with the exact file path on every code block, the real expected output after the step that produces it, a likely-failure line where a step commonly breaks, and options or reference material only after the working result. + ## Cut the slop Delete these on sight. They add length, not clarity. From d652e12fbd900daec0c7603a87f6e54f50f8f8ca Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:29:54 +0200 Subject: [PATCH 5/6] docs(data-modeling): note explicit discriminator on variant creates (live-tested) Verified against a fresh Prisma Postgres database: variant ORM roots exist and base queries return variant rows, but creating through a variant does not auto-fill the discriminator on 0.14.0. Co-Authored-By: Claude Fable 5 --- .../docs/orm/next/data-modeling/relational-databases.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx b/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx index cba07ea843..850deb9945 100644 --- a/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx +++ b/apps/docs/content/docs/orm/next/data-modeling/relational-databases.mdx @@ -190,6 +190,8 @@ model Feature { `@@discriminator(type)` marks the field that records the variant. `@@base(Task, "bug")` declares that `Bug` is the `"bug"` variant of `Task`. +Variants appear as their own models in queries (`db.orm.public.Bug` alongside `db.orm.public.Task`), and a query on the base model returns every variant. One thing to know when writing: pass the discriminator value explicitly when you create through a variant (`{ title, severity, type: "bug" }`); it is not filled in automatically yet. + Each variant picks one of two storage layouts: - A variant with no `@@map`, like `Bug`, shares the base table. Its extra columns live alongside the base columns and must be nullable at the storage level, because a `Feature` row has no severity. From 291c5655f74c0c4b94d111bd763e8a209acd1626 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:44:39 +0200 Subject: [PATCH 6/6] docs(data-modeling): sidebar as a Data Modeling section with Overview first Sidebar now reads Data Modeling > Overview / Relational data modeling / MongoDB data modeling, per review. Co-Authored-By: Claude Fable 5 --- apps/docs/content/docs/orm/next/data-modeling/index.mdx | 2 +- apps/docs/content/docs/orm/next/meta.json | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/docs/content/docs/orm/next/data-modeling/index.mdx b/apps/docs/content/docs/orm/next/data-modeling/index.mdx index 8096178e6d..a22b229bb1 100644 --- a/apps/docs/content/docs/orm/next/data-modeling/index.mdx +++ b/apps/docs/content/docs/orm/next/data-modeling/index.mdx @@ -1,5 +1,5 @@ --- -title: Data modeling overview +title: Overview description: Describe the data your application needs with models, primary keys, scalar fields, and relations. url: /orm/next/data-modeling metaTitle: Data modeling in Prisma Next diff --git a/apps/docs/content/docs/orm/next/meta.json b/apps/docs/content/docs/orm/next/meta.json index 0880c7b034..490a6fdc4a 100644 --- a/apps/docs/content/docs/orm/next/meta.json +++ b/apps/docs/content/docs/orm/next/meta.json @@ -5,8 +5,7 @@ "pages": [ "---Introduction---", "index", - - "---Guides---", - "data-modeling" + "---Data Modeling---", + "...data-modeling" ] }