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:
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,
|
||||
|
||||
Reference in new issue
Block a user