- 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
465 lines
14 KiB
Plaintext
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
|
|
}
|
|
)
|
|
}
|
|
}
|
|
```
|