# MikroORM
# Features
Currently, @tsed/mikro-orm
allows you:
- Configure one or more MikroORM instances via the
@Configuration
decorator. All databases will be initialized when the server starts during the server'sOnInit
phase. - Use the Entity MikroORM as Model for Controllers, AJV Validation and Swagger.
# Installation
To begin, install the MikroORM module for TS.ED:
Once the installation process is completed, we can import the MikroOrmModule
into the Server
configuration:
import {Configuration} from "@tsed/di";
import {MikroOrmModule} from "@tsed/mikro-orm";
@Configuration({
imports: [MikroOrmModule],
mikroOrm: [
{
contextName: 'default',
type: 'postgresql',
...,
entities: [
`./entity/*{.ts,.js}`
]
},
{
contextName: 'mongo',
type: 'mongo',
...
}
]
})
export class Server {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
The mikroOrm
options accepts the same configuration object as init()
from the MikroORM package. Check this page (opens new window) for the complete configuration documentation.
# Obtain ORM instance
@Orm
decorator lets you retrieve an instance of MikroORM.
import {Injectable, AfterRoutesInit} from "@tsed/common";
import {Orm} from "@tsed/mikro-orm";
import {MikroORM} from "@mikro-orm/core";
@Injectable()
export class UsersService {
@Orm()
private readonly orm!: MikroORM;
async create(user: User): Promise<User> {
// do something
// ...
// Then save
await this.orm.em.persistAndFlush(user);
console.log("Saved a new user with id: " + user.id);
return user;
}
async find(): Promise<User[]> {
const users = await this.orm.em.find(User, {});
console.log("Loaded users: ", users);
return users;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
It's also possible to inject an ORM by its context name:
import {Injectable} from "@tsed/di";
@Injectable()
export class MyService {
@Orm("mongo")
private readonly orm!: MikroORM;
}
2
3
4
5
6
7
# Obtain EntityManager
@EntityManager
and @Em
decorators lets you retrieve an instance of EntityManager.
import {Injectable, AfterRoutesInit} from "@tsed/common";
import {Em} from "@tsed/mikro-orm";
import {EntityManager} from "@mikro-orm/mysql"; // Import EntityManager from your driver package or `@mikro-orm/knex`
@Injectable()
export class UsersService {
@Em()
private readonly em!: EntityManager;
async create(user: User): Promise<User> {
await this.em.persistAndFlush(user);
console.log("Saved a new user with id: " + user.id);
return user;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
It's also possible to inject Entity manager by his context name:
import {Injectable, AfterRoutesInit} from "@tsed/common";
import {Em} from "@tsed/mikro-orm";
import {EntityManager} from "@mikro-orm/mysql"; // Import EntityManager from your driver package or `@mikro-orm/knex`
@Injectable()
export class UsersService {
@Em("contextName")
private readonly em!: EntityManager;
async create(user: User): Promise<User> {
await this.em.persistAndFlush(user);
console.log("Saved a new user with id: " + user.id);
return user;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Use Entity with Controller
To begin, we need to define an Entity MikroORM like this and use Ts.ED Decorator to define the JSON Schema.
import {Property, MaxLength, Required} from "@tsed/common";
import {Entity, Property, PrimaryKey, Property as Column} from "@mikro-orm/core";
@Entity()
export class User {
@PrimaryKey()
@Property()
id!: number;
@Column()
@MaxLength(100)
@Required()
firstName!: string;
@Column()
@MaxLength(100)
@Required()
lastName!: string;
@Column()
@Mininum(0)
@Maximum(100)
age!: number;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Now, the model is correctly defined and can be used with a Controller (opens new window) , AJV validation, Swagger and MikroORM (opens new window).
We can use this model with a Controller like that:
import {Controller, Post, BodyParams, Inject, Post, Get} from "@tsed/common";
@Controller("/users")
export class UsersCtrl {
@Inject()
private readonly usersService!: UsersService;
@Post("/")
create(@BodyParams() user: User): Promise<User> {
return this.usersService.create(user);
}
@Get("/")
getList(): Promise<User[]> {
return this.usersService.find();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Transactions and Request context
As mentioned in the docs (opens new window), we need to isolate a state for each request. That is
handled automatically thanks to the AsyncLocalStorage
registered via interceptor.
We can use the @Transactional()
decorator, which will register a new request context for your method and execute it
inside the context.
import {Controller, Post, BodyParams, Inject, Get} from "@tsed/common";
import {Transactional} from "@tsed/mikro-orm";
@Controller("/users")
export class UsersCtrl {
@Inject()
private readonly usersService!: UsersService;
@Post("/")
@Transactional()
create(@BodyParams() user: User): Promise<User> {
return this.usersService.create(user);
}
@Get("/")
getList(): Promise<User[]> {
return this.usersService.find();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Retry policy
By default, IsolationLevel.READ_COMMITTED
is used. You can override it, specifying the isolation level for the transaction by supplying it as the isolationLevel
parameter in the @Transactional
decorator:
@Post("/")
@Transactional({isolationLevel: IsolationLevel.SERIALIZABLE})
create(@BodyParams() user: User): Promise<User> {
return this.usersService.create(user);
}
2
3
4
5
The MikroORM supports the standard isolation levels such as SERIALIZABLE
or REPEATABLE READ
, the full list of available options see here (opens new window).
You can also set the flushing strategy (opens new window) for the transaction by setting the flushMode
:
@Post("/")
@Transactional({flushMode: FlushMode.AUTO})
create(@BodyParams() user: User): Promise<User> {
return this.usersService.create(user);
}
2
3
4
5
In some cases, you might need to avoid an explicit transaction, but preserve an async context to prevent the usage of the global identity map. For example, starting with v3.4, the MongoDB driver supports transactions. Yet, you have to use a replica set, otherwise, the driver will raise an exception.
To prevent @Transactional()
use of an explicit transaction, you just need to set the disabled
field to true
:
@Post("/")
@Transactional({disabled: true})
create(@BodyParams() user: User): Promise<User> {
return this.usersService.create(user);
}
2
3
4
5
By default, the automatic retry policy is disabled. You can implement your own to match the business requirements and the nature of the failure. For some noncritical operations, it is better to fail as soon as possible rather than retry a coupe of times. For example, in an interactive web application, it is better to fail right after a smaller number of retries with only a short delay between retry attempts, and display a message to the user (for example, "please try again later").
The @Transactional()
decorator allows you to enable a retry policy for the particular resources. You just need to implement the RetryStrategy
interface and use registerProvider()
or @OverrideProvider()
to register it in the IoC container. Below you can find an example to handle occurred optimistic locks based on an exponential backoff retry strategy (opens new window).
import {OptimisticLockError} from "@mikro-orm/core";
import {RetryStrategy} from "@tsed/mikro-orm";
export interface ExponentialBackoffOptions {
maxDepth: number;
}
export class ExponentialBackoff implements RetryStrategy {
private depth = 0;
constructor(private readonly options: ExponentialBackoffOptions) {}
public async acquire<T extends (...args: unknown[]) => unknown>(task: T): Promise<ReturnType<T>> {
try {
return (await task()) as ReturnType<T>;
} catch (e) {
if (this.shouldRetry(e as Error) && this.depth < this.options.maxDepth) {
return this.retry(task);
}
throw e;
}
}
private shouldRetry(error: Error): boolean {
return error instanceof OptimisticLockError;
}
private async retry<T extends (...args: unknown[]) => unknown>(task: T): Promise<ReturnType<T>> {
await this.sleep(2 ** this.depth * 50);
this.depth += 1;
return this.acquire(task);
}
private sleep(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
}
registerProvider({
provide: RetryStrategy,
useFactory(): ExponentialBackoff {
return new ExponentialBackoff({maxDepth: 3});
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
ExponentialBackoff
invokes passed function recursively is contained in a try/catch block. The method returns control to the interceptor if the call to the task
function succeeds without throwing an exception. If the task
method fails, the catch block examines the reason for the failure. If it's optimistic locking the code waits for a short delay before retrying the operation.
Once a retry strategy is implemented, you can enable an automatic retry mechanism using the @Transactional
decorator like that:
import {Controller, Post, BodyParams, Inject, Get} from "@tsed/common";
import {Transactional} from "@tsed/mikro-orm";
@Controller("/users")
export class UsersCtrl {
@Inject()
private readonly usersService!: UsersService;
@Post("/")
@Transactional({retry: true})
create(@BodyParams() user: User): Promise<User> {
return this.usersService.create(user);
}
@Get("/")
getList(): Promise<User[]> {
return this.usersService.find();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Author
# Maintainers
Other topics
- Session & cookies
- Passport.js
- Keycloak
- Prisma
- TypeORM
- MikroORM
- Mongoose
- GraphQL
- Socket.io
- Swagger
- AJV
- Multer
- Serve static files
- Templating
- Serverless HTTP
- Seq
- OIDC
- Stripe
- Agenda
- Terminus
- Serverless
- IORedis
- Controllers
- Providers
- Model
- JsonMapper
- Middlewares
- Pipes
- Interceptors
- Authentication
- Hooks
- Exceptions
- Throw HTTP Exceptions
- Cache
- Command
- Response Filter
- Injection scopes
- Custom providers
- Lazy-loading provider
- Custom endpoint decorator
- Testing
- Customize 404