Files
medusa-store/www/apps/book/app/advanced-development/modules/db-operations/page.mdx
Shahed Nasser fb67d90b64 docs: improvements + additions to module docs (#9152)
- Split Module and Module Links to their own chapters
- Add new docs on db operations and transactions in modules, multiple services, links with custom columns, etc...
- Added a list of registered dependencies in a module container
2024-10-01 11:20:54 +00:00

465 lines
14 KiB
Plaintext

import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `${pageNumber} Perform Database Operations in a Service`,
}
# {metadata.title}
In this chapter, you'll learn how to perform database operations in a module's service.
<Note>
This chapter is intended for more advanced database use-cases where you need more control over queries and operations. For basic database operations, such as creating or retrieving data of a model, use the [Service Factory](../service-factory/page.mdx) instead.
</Note>
## Run Queries
[MikroORM's entity manager](https://mikro-orm.io/docs/entity-manager) is a class that has methods to run queries on the database and perform operations.
Medusa provides an `InjectManager` decorator imported from `@medusajs/utils` that injects a service's method with a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager).
So, to run database queries in a service:
1. Add the `InjectManager` decorator to the method.
2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator imported from `@medusajs/utils`. This context holds database-related context, including the manager injected by `InjectManager`
For example, in your service, add the following methods:
export const methodsHighlight = [
["4", "getCount", "Retrieves the number of records in `my_custom` using the `count` method."],
["8", "getCountSql", "Retrieves the number of records in `my_custom` using the `execute` method."]
]
```ts highlights={methodsHighlight}
// other imports...
import {
InjectManager,
MedusaContext
} from "@medusajs/framework/utils"
class HelloModuleService {
// ...
@InjectManager()
async getCount(
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<number> {
return await sharedContext.manager.count("my_custom")
}
@InjectManager()
async getCountSql(
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<number> {
const data = await sharedContext.manager.execute(
"SELECT COUNT(*) as num FROM my_custom"
)
return parseInt(data[0].num)
}
}
```
You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator.
The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/5.9/knex/class/EntityManager).
You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count.
<Note>
Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods.
</Note>
---
## Execute Operations in Transactions
To wrap database operations in a transaction, you create two methods:
1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator imported from `@medusajs/utils`.
2. A public method that calls the transactional method. You use on it the `InjectManager` decorator as explained in the previous section.
Both methods must accept as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator imported from `@medusajs/utils`. It holds database-related contexts passed through the Medusa application.
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."],
]
```ts highlights={opHighlights}
import {
InjectManager,
InjectTransactionManager,
MedusaContext
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
const transactionManager = sharedContext.transactionManager
await transactionManager.nativeUpdate(
"my_custom",
{
id: input.id
},
{
name: input.name
}
)
// retrieve again
const updatedRecord = await transactionManager.execute(
`SELECT * FROM my_custom WHERE id = '${input.id}'`
)
return updatedRecord
}
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
return await this.update_(input, sharedContext)
}
}
```
The `HelloModuleService` has two methods:
- 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/5.9/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 uncomitted 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 connect 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 { EntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
const newData = await this.update_(input, sharedContext)
await 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 method.
For example:
```ts
// other imports...
import { EntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectTransactionManager()
protected async anotherMethod(
@MedusaContext() sharedContext?: Context<EntityManager>
) {
// ...
}
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
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.
---
## Configure Transactions
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.
The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more.
The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction.
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"]]}
import { MedusaService } from "@medusajs/framework/utils"
import MyCustom from "./models/my-custom"
import { DAL } from "@medusajs/framework/types"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
}
class HelloModuleService extends MedusaService({
MyCustom,
}){
protected baseRepository_: DAL.RepositoryService
constructor({ baseRepository }: InjectedDependencies) {
super(...arguments)
this.baseRepository_ = baseRepository
}
}
export default HelloModuleService
```
</CodeTab>
<CodeTab label="Without Service Factory" value="no-service-factory">
```ts highlights={[["10"]]}
import { DAL } from "@medusajs/framework/types"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
}
class HelloModuleService {
protected baseRepository_: DAL.RepositoryService
constructor({ manager }: InjectedDependencies) {
this.baseRepository_ = baseRepository
}
}
export default HelloModuleService
```
</CodeTab>
</CodeTabs>
Then, add the following method that uses it:
export const repoHighlights = [
["20", "transaction", "Wrap the function parameter in a transaction."]
]
```ts highlights={repoHighlights}
// ...
import {
InjectManager,
InjectTransactionManager,
MedusaContext
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
return await this.baseRepository_.transaction(
async (transactionManager) => {
await transactionManager.nativeUpdate(
"my_custom",
{
id: input.id
},
{
name: input.name
}
)
// retrieve again
const updatedRecord = await transactionManager.execute(
`SELECT * FROM my_custom WHERE id = '${input.id}'`
)
return updatedRecord
},
{
transaction: sharedContext.transactionManager
}
)
}
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
return await this.update_(input, sharedContext)
}
}
```
The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction.
The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations.
The `baseRepository_.transaction` method also receives as a second parameter an object of options. You must pass in it the `transaction` property and set its value to the `sharedContext.transactionManager` property so that the function wrapped in the transaction uses the injected transaction manager.
<Note>
Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods.
</Note>
### Transaction Options
The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties:
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"]]}
// other imports...
import { EntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectTransactionManager()
async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
return await this.baseRepository_.transaction<EntityManager>(
async (transactionManager) => {
// ...
},
{
transaction: sharedContext.transactionManager
}
)
}
}
```
2. `isolationLevel`: Sets the transaction's [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html). Its values can be:
- `read committed`
- `read uncommitted`
- `snapshot`
- `repeatable read`
- `serializable`
```ts highlights={[["19"]]}
// other imports...
import { IsolationLevel } from "@mikro-orm/core"
class HelloModuleService {
// ...
@InjectTransactionManager()
async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
return await this.baseRepository_.transaction<EntityManager>(
async (transactionManager) => {
// ...
},
{
isolationLevel: IsolationLevel.READ_COMMITTED
}
)
}
}
```
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"]]}
class HelloModuleService {
// ...
@InjectTransactionManager()
async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
return await this.baseRepository_.transaction<EntityManager>(
async (transactionManager) => {
// ...
},
{
enableNestedTransactions: false
}
)
}
}
```