docs: improve database operations docs + added data model repository reference (#12273)

* some changes

* docs: improve database operations docs + add data model repository reference

* regenerate
This commit is contained in:
Shahed Nasser authored and GitHub committed 2025-04-23 19:22:50 +03:00
1 parent 876a14eb98
commit 54bae210b8
21 files changed
+20651 -17089

No files matched your search

@@ -1,4 +1,4 @@
import { CodeTabs, CodeTab } from "docs-ui"
import { CodeTabs, CodeTab, TypeList } from "docs-ui"
export const metadata = {
title: `${pageNumber} Perform Database Operations in a Service`,
@@ -78,7 +78,18 @@ Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManage
---
## Execute Operations in Transactions
## Perform Database Operations
There are two ways to perform database operations in transactional methods:
1. Using the [data model repositories](#perform-database-operations-with-data-model-repositories) in your module.
2. Using the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager) injected into the method's shared context.
For both approaches, you must wrap the method performing the database operations in a transaction.
### Wrap Database Operations in Transactions
When performing database operations without using the [Service Factory](../service-factory/page.mdx), you must wrap the method performing the database operations in a transaction.
To wrap database operations in a transaction, you create two methods:
@@ -92,9 +103,7 @@ For example:
export const opHighlights = [
["11", "InjectTransactionManager", "A decorator that injects the a transactional entity manager into the `sharedContext` parameter."],
["17", "MedusaContext", "A decorator to use Medusa's shared context."],
["20", "nativeUpdate", "Update a record."],
["31", "execute", "Retrieve the updated record."],
["38", "InjectManager", "A decorator that injects a forked entity manager into the context."],
["24", "InjectManager", "A decorator that injects a forked entity manager into the context."],
]
```ts highlights={opHighlights}
@@ -106,6 +115,273 @@ import {
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
const transactionManager = sharedContext?.transactionManager
// TODO: update the record
}
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
return await this.update_(input, sharedContext)
}
}
```
The `BlogModuleService` has two methods:
- A protected `update_` that performs the database operations inside a transaction.
- A public `update` that executes the transactional protected method.
You can then perform in the transactional method the database operations either using the [data model repository](#perform-database-operations-with-data-model-repositories) or the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager).
#### Why Wrap a Transactional Method
The variables in the transactional method (such as `update_`) hold values that are uncommitted to the database. They're only committed once the method finishes execution.
So, if in your method you perform database operations, then use their result to perform other actions, such as connecting to a third-party service, you'll be working with uncommitted data.
By placing only the database operations in a method that has the `InjectTransactionManager` and using it in a wrapper method, the wrapper method receives the committed result of the transactional method.
<Note title="Optimization Tip">
This is also useful if you perform heavy data normalization outside of the database operations. In that case, you don't hold the transaction for a longer time than needed.
</Note>
For example, the `update` method may call other methods than `update_` to perform other actions:
```ts
// other imports...
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@InjectTransactionManager()
protected async update_(
// ...
): Promise<any> {
// ...
}
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
const newData = await this.update_(input, sharedContext)
// example method that sends data to another system
await this.sendNewDataToSystem(newData)
return newData
}
}
```
In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method.
#### Using Methods in Transactional Methods
If your protected transactional method uses other methods that accept a Medusa context, pass the shared context to those methods.
For example:
export const anotherMethodHighlights = [
["26", "sharedContext", "Pass the context to the other transactional method."]
]
```ts highlights={anotherMethodHighlights}
// other imports...
import {
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@InjectTransactionManager()
protected async anotherMethod(
@MedusaContext() sharedContext?: Context<EntityManager>
) {
// ...
}
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
this.anotherMethod(sharedContext)
}
}
```
You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context.
The `anotherMethod` now runs in the same transaction as the `update_` method.
### Perform Database Operations with Data Model Repositories
For every data model in your module, Medusa generates a data model repository that has methods to perform database operations.
For example, if your module has a `Post` model, it has a `postRepository` in the container.
The data model repository is a wrapper around the [entity manager](https://mikro-orm.io/api/knex/class/EntityManager) that provides a higher-level API for performing database operations.
<Note>
To use the low-level entity manager, use the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager) instead.
</Note>
#### Resolve Data Model Repository
When the Medusa application injects a data model repository into a module's container, it formats the registration name by:
- Taking the data model's name that's passed as the first parameter of `model.define`
- Lower-casing the first character
- Suffixing the result with `Repository`.
For example:
- `Post` model: `postRepository`
- `My_Custom` model: `my_CustomRepository`
So, to resolve a data model repository from a module's container, pass the expected registration name of the repository in the first parameter of the module's constructor (the container).
For example:
<CodeTabs group="service-type">
<CodeTab label="Extending Service Factory" value="service-factory">
export const serviceFactoryRepoHighlights = [
["20", "postRepository", "Resolve the data model repository of the `Post` model."],
]
```ts highlights={serviceFactoryRepoHighlights}
import { MedusaService } from "@medusajs/framework/utils"
import { InferTypeOf, DAL } from "@medusajs/framework/types"
import Post from "./models/post"
type Post = InferTypeOf<typeof Post>
type InjectedDependencies = {
postRepository: DAL.RepositoryService<Post>
}
class BlogModuleService extends MedusaService({
Post,
}){
protected postRepository_: DAL.RepositoryService<Post>
constructor({
postRepository
}: InjectedDependencies) {
super(...arguments)
this.postRepository_ = postRepository
}
}
export default BlogModuleService
```
</CodeTab>
<CodeTab label="Without Service Factory" value="no-service-factory">
export const noServiceFactoryRepoHighlights = [
["17", "postRepository", "Resolve the data model repository of the `Post` model."],
]
```ts highlights={noServiceFactoryRepoHighlights}
import { InferTypeOf, DAL } from "@medusajs/framework/types"
import Post from "./models/post"
type Post = InferTypeOf<typeof Post>
type InjectedDependencies = {
postRepository: DAL.RepositoryService<Post>
}
class BlogModuleService {
protected postRepository_: DAL.RepositoryService<Post>
constructor({
postRepository
}: InjectedDependencies) {
super(...arguments)
this.postRepository_ = postRepository
}
}
export default BlogModuleService
```
</CodeTab>
</CodeTabs>
You can then use the data model repository in your service to perform database operations.
#### Data Model Repository Methods
A data model repository has methods that allows you to create, update, and delete records, among other operations.
To learn about the methods available in a data model repository, refer to the [Data Model Repository](!resources!/data-model-repository-reference) reference.
### Perform Database Operations with the Transactional Entity Manager
Your transactional method can use the transactional entity manager injected into the method's shared context to perform database operations. It's an instance of the [MikroORM EntityManager](https://mikro-orm.io/api/knex/class/EntityManager) class.
<Note>
To use an easier higher-level API focused on each data model, use the [data model repository](#perform-database-operations-with-data-model-repositories) instead.
</Note>
For example:
export const transactionalEntityManagerHighlights = [
["19", "transactionManager", "Use the transactional entity manager injected\ninto the `sharedContext` parameter."],
["20", "nativeUpdate", "Perform a native update operation."],
["31", "execute", "Perform a native query operation."],
]
```ts highlights={transactionalEntityManagerHighlights}
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@InjectTransactionManager()
@@ -148,119 +424,15 @@ class BlogModuleService {
}
```
The `BlogModuleService` has two methods:
The `update_` method uses the transactional entity manager injected into the `sharedContext.transactionManager` property to perform the database operations.
- A protected `update_` that performs the database operations inside a transaction.
- A public `update` that executes the transactional protected method.
The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations.
<Note>
Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods.
</Note>
### Why Wrap a Transactional Method
The variables in the transactional method (for example, `update_`) hold values that are uncommitted to the database. They're only committed once the method finishes execution.
So, if in your method you perform database operations, then use their result to perform other actions, such as connecting to a third-party service, you'll be working with uncommitted data.
By placing only the database operations in a method that has the `InjectTransactionManager` and using it in a wrapper method, the wrapper method receives the committed result of the transactional method.
<Note title="Optimization Tip">
This is also useful if you perform heavy data normalization outside of the database operations. In that case, you don't hold the transaction for a longer time than needed.
</Note>
For example, the `update` method could be changed to the following:
```ts
// other imports...
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@InjectTransactionManager()
protected async update_(
// ...
): Promise<any> {
// ...
}
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
const newData = await this.update_(input, sharedContext)
// example method that sends data to another system
await this.sendNewDataToSystem(newData)
return newData
}
}
```
In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method.
### Using Methods in Transactional Methods
If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods.
For example:
```ts
// other imports...
import {
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@InjectTransactionManager()
protected async anotherMethod(
@MedusaContext() sharedContext?: Context<EntityManager>
) {
// ...
}
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
this.anotherMethod(sharedContext)
}
}
```
You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context.
The `anotherMethod` now runs in the same transaction as the `update_` method.
Find all available methods in the [MikroORM EntityManager](https://mikro-orm.io/api/knex/class/EntityManager) reference.
---
## Configure Transactions
## Configure Transactions with the Base Repository
To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` dependency registered in your module's container.
To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` class registered in your module's container.
The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more.
@@ -271,7 +443,11 @@ For example, resolve the `baseRepository` in your service's constructor:
<CodeTabs group="service-type">
<CodeTab label="Extending Service Factory" value="service-factory">
```ts highlights={[["14"]]}
export const baseRepoHighlights = [
["16", "baseRepository", "Resolve the base repository."],
]
```ts highlights={baseRepoHighlights}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
import { DAL } from "@medusajs/framework/types"
@@ -293,10 +469,15 @@ class BlogModuleService extends MedusaService({
export default BlogModuleService
```
</CodeTab>
<CodeTab label="Without Service Factory" value="no-service-factory">
```ts highlights={[["10"]]}
export const noServiceFactoryBaseRepoHighlights = [
["11", "baseRepository", "Resolve the base repository."],
]
```ts highlights={noServiceFactoryBaseRepoHighlights}
import { DAL } from "@medusajs/framework/types"
type InjectedDependencies = {
@@ -317,7 +498,7 @@ export default BlogModuleService
</CodeTab>
</CodeTabs>
Then, add the following method that uses it:
Then, use it in the service's transactional methods:
export const repoHighlights = [
["20", "transaction", "Wrap the function parameter in a transaction."]
@@ -399,7 +580,7 @@ The second parameter of the `baseRepository_.transaction` method is an object of
1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section.
```ts highlights={[["16"]]}
```ts highlights={[["24"]]}
// other imports...
import { EntityManager } from "@mikro-orm/knex"
import {
@@ -407,7 +588,6 @@ import {
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class BlogModuleService {
// ...
@@ -438,7 +618,7 @@ class BlogModuleService {
- `repeatable read`
- `serializable`
```ts highlights={[["19"]]}
```ts highlights={[["25"]]}
// other imports...
import {
InjectTransactionManager,
@@ -473,7 +653,7 @@ class BlogModuleService {
3. `enableNestedTransactions`: (default: `false`) whether to allow using nested transactions.
- If `transaction` is provided and this is disabled, the manager in `transaction` is re-used.
```ts highlights={[["16"]]}
```ts highlights={[["24"]]}
// other imports...
import {
InjectTransactionManager,
+1 -1
View File
@@ -68,7 +68,7 @@ export const generatedEditDates = {
"app/learn/fundamentals/module-links/directions/page.mdx": "2025-03-17T12:52:06.161Z",
"app/learn/fundamentals/module-links/page.mdx": "2025-04-17T08:50:17.036Z",
"app/learn/fundamentals/module-links/query/page.mdx": "2025-04-18T11:13:02.240Z",
"app/learn/fundamentals/modules/db-operations/page.mdx": "2025-03-21T09:21:46.901Z",
"app/learn/fundamentals/modules/db-operations/page.mdx": "2025-04-23T14:24:37.910Z",
"app/learn/fundamentals/modules/multiple-services/page.mdx": "2025-03-18T15:11:44.632Z",
"app/learn/fundamentals/modules/page.mdx": "2025-03-18T07:51:09.049Z",
"app/learn/debugging-and-testing/instrumentation/page.mdx": "2025-02-24T08:12:53.132Z",
File diff suppressed because it is too large. Load diff