docs: use mappedBy property for all relationship types (#12973)

This commit is contained in:
Shahed Nasser
2025-07-16 14:26:51 +03:00
committed by GitHub
parent 5527d95b5c
commit ab82e4c9fa
8 changed files with 175 additions and 121 deletions

View File

@@ -39,8 +39,9 @@ For example:
export const oneToOneHighlights = [
["5", "hasOne", "A user has one email."],
["10", "belongsTo", "An email belongs to a user."],
["11", `"email"`, "The relationship's name in the `User` data model."]
["6", `"user"`, "The relationship's name in the `Email` data model."],
["12", "belongsTo", "An email belongs to a user."],
["13", `"email"`, "The relationship's name in the `User` data model."]
]
```ts highlights={oneToOneHighlights}
@@ -48,7 +49,9 @@ import { model } from "@medusajs/framework/utils"
const User = model.define("user", {
id: model.id().primaryKey(),
email: model.hasOne(() => Email),
email: model.hasOne(() => Email, {
mappedBy: "user",
}),
})
const Email = model.define("email", {
@@ -63,15 +66,68 @@ In the example above, a user has one email, and an email belongs to one user.
The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model.
The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model.
Both methods also accept a second parameter object with the property `mappedBy`. Its value is the name of the relationship property in the other data model.
### Optional Relationship
To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](../properties/page.mdx#make-property-optional).
### One-to-One Relationship in the Database
When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property:
1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column.
2. A foreign key on the `{relation_name}_id` column to the table of the related data model.
![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg)
### One-sided One-to-One Relationship
If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method.
In some use cases, you may want to define a one-to-one relationship only on one side. This means that the other data model does not have a relationship property pointing to the first one.
You can do this either from the `hasOne` or the `belongsTo` side.
#### hasOne Side
By default, the foreign key column is added to the table of the data model that has the `belongsTo` property. For example, if the `Email` data model belongs to the `User` data model, then the foreign key column is added to the `email` table.
If you want to define a one-to-one relationship only on the `User` data model's side (`hasOne` side), you can do so by passing the following properties to the second parameter of the `hasOne` method:
- `foreignKey`: A boolean indicating whether the foreign key column should be added to the table of the data model.
- `mappedBy`: Set to `undefined`, since the relationship is only defined on one side.
For example:
export const oneToOneForeignKeyHighlights = [
["5", "hasOne", "A user has one email."],
["6", "foreignKey", "Add the foreign key column to the `user` table."],
["7", "mappedBy", "Set to `undefined` since the relationship is only defined on the `User` data model."],
]
```ts highlights={oneToOneForeignKeyHighlights}
import { model } from "@medusajs/framework/utils"
const User = model.define("user", {
id: model.id().primaryKey(),
email: model.hasOne(() => Email, {
foreignKey: true,
mappedBy: undefined,
}),
})
const Email = model.define("email", {
id: model.id().primaryKey(),
})
```
In the example above, you add a one-to-one relationship from the `User` data model to the `Email` data model.
The foreign key column is added to the `user` table, and the `Email` data model does not have a relationship property pointing to the `User` data model.
#### belongsTo Side
To define the one-to-one relationship on the `belongsTo` side, pass `undefined` to the `mappedBy` property in the `belongsTo` method's second parameter.
For example:
@@ -94,14 +150,9 @@ const Email = model.define("email", {
})
```
### One-to-One Relationship in the Database
In the example above, you add a one-to-one relationship from the `Email` data model to the `User` data model.
When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property:
1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column.
2. A foreign key on the `{relation_name}_id` column to the table of the related data model.
![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg)
The `User` data model does not have a relationship property pointing to the `Email` data model.
---
@@ -118,8 +169,9 @@ For example:
export const oneToManyHighlights = [
["5", "hasMany", "A store has many products."],
["10", "belongsTo", "A product has one store."],
["11", `"products"`, "The relationship's name in the `Store` data model."]
["6", `"store"`, "The relationship's name in the `Product` data model."],
["12", "belongsTo", "A product has one store."],
["13", `"products"`, "The relationship's name in the `Store` data model."]
]
```ts highlights={oneToManyHighlights}
@@ -127,7 +179,9 @@ import { model } from "@medusajs/framework/utils"
const Store = model.define("store", {
id: model.id().primaryKey(),
products: model.hasMany(() => Product),
products: model.hasMany(() => Product, {
mappedBy: "store",
}),
})
const Product = model.define("product", {
@@ -165,7 +219,7 @@ For example:
export const manyToManyHighlights = [
["5", "manyToMany", "An order is associated with many products."],
["12", "manyToMany", "A product is associated with many orders."]
["15", "manyToMany", "A product is associated with many orders."]
]
```ts highlights={manyToManyHighlights}
@@ -278,43 +332,6 @@ The `OrderProduct` model defines, aside from the ID, the following properties:
---
## Set Relationship Name in the Other Model
The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model.
This is useful if the relationship propertys name is different from that of the associated data model.
As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method.
For example:
export const relationNameHighlights = [
["6", `"owner"`, "The relationship's name in the `Email` data model."],
["13", `"email"`, "The relationship's name in the `User` data model."]
]
```ts highlights={relationNameHighlights}
import { model } from "@medusajs/framework/utils"
const User = model.define("user", {
id: model.id().primaryKey(),
email: model.hasOne(() => Email, {
mappedBy: "owner",
}),
})
const Email = model.define("email", {
id: model.id().primaryKey(),
owner: model.belongsTo(() => User, {
mappedBy: "email",
}),
})
```
In this example, you specify in the `User` data models relationship property that the name of the relationship in the `Email` data model is `owner`.
---
## Cascades
When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it.

View File

@@ -36,7 +36,7 @@ export const generatedEditDates = {
"app/learn/fundamentals/data-models/manage-relationships/page.mdx": "2025-04-25T14:16:41.124Z",
"app/learn/fundamentals/modules/remote-query/page.mdx": "2024-07-21T21:20:24+02:00",
"app/learn/fundamentals/modules/options/page.mdx": "2025-03-18T15:12:34.510Z",
"app/learn/fundamentals/data-models/relationships/page.mdx": "2025-03-18T07:52:07.421Z",
"app/learn/fundamentals/data-models/relationships/page.mdx": "2025-07-16T09:51:22.141Z",
"app/learn/fundamentals/workflows/compensation-function/page.mdx": "2025-04-24T13:16:00.941Z",
"app/learn/fundamentals/modules/service-factory/page.mdx": "2025-03-18T15:14:13.486Z",
"app/learn/fundamentals/modules/module-links/page.mdx": "2024-09-30T08:43:53.126Z",

View File

@@ -9131,7 +9131,9 @@ import { model } from "@medusajs/framework/utils"
const User = model.define("user", {
id: model.id().primaryKey(),
email: model.hasOne(() => Email),
email: model.hasOne(() => Email, {
mappedBy: "user",
}),
})
const Email = model.define("email", {
@@ -9146,15 +9148,61 @@ In the example above, a user has one email, and an email belongs to one user.
The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model.
The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model.
Both methods also accept a second parameter object with the property `mappedBy`. Its value is the name of the relationship property in the other data model.
### Optional Relationship
To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md).
### One-to-One Relationship in the Database
When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property:
1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column.
2. A foreign key on the `{relation_name}_id` column to the table of the related data model.
![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg)
### One-sided One-to-One Relationship
If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method.
In some use cases, you may want to define a one-to-one relationship only on one side. This means that the other data model does not have a relationship property pointing to the first one.
You can do this either from the `hasOne` or the `belongsTo` side.
#### hasOne Side
By default, the foreign key column is added to the table of the data model that has the `belongsTo` property. For example, if the `Email` data model belongs to the `User` data model, then the foreign key column is added to the `email` table.
If you want to define a one-to-one relationship only on the `User` data model's side (`hasOne` side), you can do so by passing the following properties to the second parameter of the `hasOne` method:
- `foreignKey`: A boolean indicating whether the foreign key column should be added to the table of the data model.
- `mappedBy`: Set to `undefined`, since the relationship is only defined on one side.
For example:
```ts highlights={oneToOneForeignKeyHighlights}
import { model } from "@medusajs/framework/utils"
const User = model.define("user", {
id: model.id().primaryKey(),
email: model.hasOne(() => Email, {
foreignKey: true,
mappedBy: undefined,
}),
})
const Email = model.define("email", {
id: model.id().primaryKey(),
})
```
In the example above, you add a one-to-one relationship from the `User` data model to the `Email` data model.
The foreign key column is added to the `user` table, and the `Email` data model does not have a relationship property pointing to the `User` data model.
#### belongsTo Side
To define the one-to-one relationship on the `belongsTo` side, pass `undefined` to the `mappedBy` property in the `belongsTo` method's second parameter.
For example:
@@ -9173,14 +9221,9 @@ const Email = model.define("email", {
})
```
### One-to-One Relationship in the Database
In the example above, you add a one-to-one relationship from the `Email` data model to the `User` data model.
When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property:
1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column.
2. A foreign key on the `{relation_name}_id` column to the table of the related data model.
![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg)
The `User` data model does not have a relationship property pointing to the `Email` data model.
***
@@ -9200,7 +9243,9 @@ import { model } from "@medusajs/framework/utils"
const Store = model.define("store", {
id: model.id().primaryKey(),
products: model.hasMany(() => Product),
products: model.hasMany(() => Product, {
mappedBy: "store",
}),
})
const Product = model.define("product", {
@@ -9328,38 +9373,6 @@ The `OrderProduct` model defines, aside from the ID, the following properties:
***
## Set Relationship Name in the Other Model
The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model.
This is useful if the relationship propertys name is different from that of the associated data model.
As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method.
For example:
```ts highlights={relationNameHighlights}
import { model } from "@medusajs/framework/utils"
const User = model.define("user", {
id: model.id().primaryKey(),
email: model.hasOne(() => Email, {
mappedBy: "owner",
}),
})
const Email = model.define("email", {
id: model.id().primaryKey(),
owner: model.belongsTo(() => User, {
mappedBy: "email",
}),
})
```
In this example, you specify in the `User` data models relationship property that the name of the relationship in the `Email` data model is `owner`.
***
## Cascades
When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it.
@@ -44063,12 +44076,14 @@ Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamen
The following creates a one-to-one relationship between the `User` and `Email` data models:
```ts highlights={[["5", "hasOne"], ["10", "belongsTo"]]}
```ts highlights={[["5", "hasOne"], ["12", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"
const User = model.define("user", {
id: model.id().primaryKey(),
email: model.hasOne(() => Email),
email: model.hasOne(() => Email, {
mappedBy: "user",
}),
})
const Email = model.define("email", {
@@ -44085,12 +44100,14 @@ Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamen
The following creates a one-to-many relationship between the `Store` and `Product` data models:
```ts highlights={[["5", "hasMany"], ["10", "belongsTo"]]}
```ts highlights={[["5", "hasMany"], ["12", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"
const Store = model.define("store", {
id: model.id().primaryKey(),
products: model.hasMany(() => Product),
products: model.hasMany(() => Product, {
mappedBy: "store",
}),
})
const Product = model.define("product", {
@@ -44131,13 +44148,15 @@ Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamen
To configure cascade on a data model:
```ts highlights={[["7", "cascades"]]}
```ts highlights={[["10", "cascades"]]}
import { model } from "@medusajs/framework/utils"
// Product import
import Product from "./product"
const Store = model.define("store", {
id: model.id().primaryKey(),
products: model.hasMany(() => Product),
products: model.hasMany(() => Product, {
mappedBy: "store",
}),
})
.cascades({
delete: ["products"],
@@ -68162,7 +68181,9 @@ export const Wishlist = model.define("wishlist", {
id: model.id().primaryKey(),
customer_id: model.text(),
sales_channel_id: model.text(),
items: model.hasMany(() => WishlistItem),
items: model.hasMany(() => WishlistItem, {
mappedBy: "wishlist",
}),
})
.indexes([
{
@@ -89532,7 +89553,9 @@ export const Restaurant = model.define("restaurant", {
email: model.text(),
address: model.text(),
image_url: model.text().nullable(),
admins: model.hasMany(() => RestaurantAdmin),
admins: model.hasMany(() => RestaurantAdmin, {
mappedBy: "restaurant",
}),
})
```
@@ -92799,7 +92822,9 @@ const Vendor = model.define("vendor", {
handle: model.text().unique(),
name: model.text(),
logo: model.text().nullable(),
admins: model.hasMany(() => VendorAdmin),
admins: model.hasMany(() => VendorAdmin, {
mappedBy: "vendor",
}),
})
export default Vendor

View File

@@ -1245,12 +1245,14 @@ Learn more in [this documentation](!docs!/learn/fundamentals/data-models/propert
The following creates a one-to-one relationship between the `User` and `Email` data models:
```ts highlights={[["5", "hasOne"], ["10", "belongsTo"]]}
```ts highlights={[["5", "hasOne"], ["12", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"
const User = model.define("user", {
id: model.id().primaryKey(),
email: model.hasOne(() => Email),
email: model.hasOne(() => Email, {
mappedBy: "user",
}),
})
const Email = model.define("email", {
@@ -1267,12 +1269,14 @@ Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relatio
The following creates a one-to-many relationship between the `Store` and `Product` data models:
```ts highlights={[["5", "hasMany"], ["10", "belongsTo"]]}
```ts highlights={[["5", "hasMany"], ["12", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"
const Store = model.define("store", {
id: model.id().primaryKey(),
products: model.hasMany(() => Product),
products: model.hasMany(() => Product, {
mappedBy: "store",
}),
})
const Product = model.define("product", {
@@ -1313,13 +1317,15 @@ Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relatio
To configure cascade on a data model:
```ts highlights={[["7", "cascades"]]}
```ts highlights={[["10", "cascades"]]}
import { model } from "@medusajs/framework/utils"
// Product import
import Product from "./product"
const Store = model.define("store", {
id: model.id().primaryKey(),
products: model.hasMany(() => Product),
products: model.hasMany(() => Product, {
mappedBy: "store",
}),
})
.cascades({
delete: ["products"],

View File

@@ -239,7 +239,9 @@ export const Wishlist = model.define("wishlist", {
id: model.id().primaryKey(),
customer_id: model.text(),
sales_channel_id: model.text(),
items: model.hasMany(() => WishlistItem),
items: model.hasMany(() => WishlistItem, {
mappedBy: "wishlist",
}),
})
.indexes([
{

View File

@@ -122,7 +122,9 @@ export const Restaurant = model.define("restaurant", {
email: model.text(),
address: model.text(),
image_url: model.text().nullable(),
admins: model.hasMany(() => RestaurantAdmin),
admins: model.hasMany(() => RestaurantAdmin, {
mappedBy: "restaurant",
}),
})
```

View File

@@ -135,7 +135,9 @@ const Vendor = model.define("vendor", {
handle: model.text().unique(),
name: model.text(),
logo: model.text().nullable(),
admins: model.hasMany(() => VendorAdmin),
admins: model.hasMany(() => VendorAdmin, {
mappedBy: "vendor",
}),
})
export default Vendor

View File

@@ -114,7 +114,7 @@ export const generatedEditDates = {
"app/recipes/digital-products/examples/standard/page.mdx": "2025-04-24T15:41:05.364Z",
"app/recipes/digital-products/page.mdx": "2025-05-20T07:51:40.719Z",
"app/recipes/ecommerce/page.mdx": "2025-06-24T08:50:10.116Z",
"app/recipes/marketplace/examples/vendors/page.mdx": "2025-07-14T13:59:32.168Z",
"app/recipes/marketplace/examples/vendors/page.mdx": "2025-07-16T09:58:59.793Z",
"app/recipes/marketplace/page.mdx": "2025-05-20T07:51:40.721Z",
"app/recipes/multi-region-store/page.mdx": "2025-05-20T07:51:40.721Z",
"app/recipes/omnichannel/page.mdx": "2025-05-20T07:51:40.722Z",
@@ -569,7 +569,7 @@ export const generatedEditDates = {
"app/medusa-cli/commands/start/page.mdx": "2025-04-08T11:56:15.522Z",
"app/medusa-cli/commands/telemtry/page.mdx": "2025-01-16T09:51:24.323Z",
"app/medusa-cli/commands/user/page.mdx": "2024-08-28T10:44:52.489Z",
"app/recipes/marketplace/examples/restaurant-delivery/page.mdx": "2025-07-14T10:32:58.301Z",
"app/recipes/marketplace/examples/restaurant-delivery/page.mdx": "2025-07-16T09:58:31.748Z",
"references/types/HttpTypes/interfaces/types.HttpTypes.AdminCreateCustomerGroup/page.mdx": "2024-12-09T13:21:33.569Z",
"references/types/HttpTypes/interfaces/types.HttpTypes.AdminCreateReservation/page.mdx": "2025-04-11T09:04:47.498Z",
"references/types/HttpTypes/interfaces/types.HttpTypes.AdminCustomerGroup/page.mdx": "2025-05-20T07:51:41.059Z",
@@ -2166,7 +2166,7 @@ export const generatedEditDates = {
"app/commerce-modules/sales-channel/links-to-other-modules/page.mdx": "2025-04-17T16:00:09.483Z",
"app/commerce-modules/stock-location/links-to-other-modules/page.mdx": "2025-04-17T16:02:51.467Z",
"app/commerce-modules/store/links-to-other-modules/page.mdx": "2025-04-17T16:03:16.419Z",
"app/examples/page.mdx": "2025-04-17T08:50:17.036Z",
"app/examples/page.mdx": "2025-07-16T09:53:26.163Z",
"app/medusa-cli/commands/build/page.mdx": "2024-11-11T11:00:49.665Z",
"app/js-sdk/page.mdx": "2025-05-26T15:08:16.590Z",
"references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.apiKey/page.mdx": "2025-05-20T07:51:40.924Z",
@@ -5832,7 +5832,7 @@ export const generatedEditDates = {
"references/core_flows/types/core_flows.ThrowUnlessPaymentCollectionNotePaidInput/page.mdx": "2025-06-25T10:11:33.516Z",
"references/core_flows/types/core_flows.ValidatePaymentsRefundStepInput/page.mdx": "2025-06-25T10:11:34.185Z",
"references/core_flows/types/core_flows.ValidateRefundStepInput/page.mdx": "2025-06-25T10:11:34.177Z",
"app/plugins/guides/wishlist/page.mdx": "2025-05-20T07:51:40.717Z",
"app/plugins/guides/wishlist/page.mdx": "2025-07-16T09:54:20.345Z",
"app/plugins/page.mdx": "2025-02-26T11:39:25.709Z",
"app/admin-components/components/data-table/page.mdx": "2025-03-03T14:55:58.556Z",
"references/order_models/variables/order_models.Order/page.mdx": "2025-07-14T09:31:48.410Z",