docs: add routing page (#9550)
- Add a new homepage to `book` project for the routing page - Move all main doc pages to be under `/v2/learn` (and added redirects + fixed links across docs) - Other: add admin components to resources dropdown + fixes to search on mobile. Closes DX-955 Preview: https://docs-v2-git-docs-router-page-medusajs.vercel.app/v2
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
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 = [
|
||||
["11", "getCount", "Retrieves the number of records in `my_custom` using the `count` method."],
|
||||
["18", "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,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user