mikro-orm для TypeScript: полное руководство с примерами
1. Что это и зачем нужна?
Mikro-ORM — это современная объектно-реляционная библиотека (ORM) для TypeScript и JavaScript (Node.js), которая реализует паттерны Identity Map и Unit of Work. В отличие от классических ORM вроде TypeORM или Sequelize, Mikro-ORM вдохновлена Doctrine (PHP) и Hibernate (Java) и предлагает более предсказуемое управлением состоянием сущностей и их синхронизацией с базой данных.
Зачем она нужна? В типичных веб-приложениях разработчики часто сталкиваются с проблемой «грязных» данных, когда одна и та же запись загружается дважды в разных местах кода, что приводит к конфликтам при сохранении. Mikro-ORM решает эту проблему с помощью Identity Map: для каждого первичного ключа существует ровно один экземпляр сущности в рамках одного контекста (EntityManager). Это гарантирует, что вы всегда работаете с актуальной версией объекта, а все изменения отслеживаются автоматически.
Библиотека поддерживает множество СУБД (PostgreSQL, MySQL, SQLite, MongoDB, MariaDB) и предоставляет мощные инструменты: миграции, встроенную поддержку транзакций, каскадные операции, загрузку отношений (eager/lazy), встроенный валидатор схем и генератор сущностей из существующей базы данных. Mikro-ORM идеально подходит для сложных доменных моделей и микросервисной архитектуры, где важна согласованность данных.
2. Установка
Для установки Mikro-ORM и необходимого драйвера базы данных выполните команду через npm (или yarn/pnpm):
# Установка ядра библиотеки
npm install @mikro-orm/core
# Установка драйвера для вашей БД (выберите один):
npm install @mikro-orm/sqlite # SQLite
npm install @mikro-orm/postgresql # PostgreSQL
npm install @mikro-orm/mysql # MySQL / MariaDB
npm install @mikro-orm/mongodb # MongoDB
# Дополнительно: CLI для миграций и генерации схем
npm install @mikro-orm/cli
npm install @mikro-orm/migrations # для миграций
Также рекомендуется установить reflect-metadata для работы декораторов:
npm install reflect-metadata
Добавьте в tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2021",
"module": "commonjs"
}
}
3. Быстрый старт — минимальный рабочий пример
Создадим простое приложение на SQLite с одной сущностью User.
Шаг 1. Определяем сущность (entities/User.ts):
import { Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Property()
email!: string;
@Property()
createdAt: Date = new Date();
}
Шаг 2. Настраиваем Mikro-ORM (index.ts):
import 'reflect-metadata';
import { MikroORM } from '@mikro-orm/core';
import { SqliteDriver } from '@mikro-orm/sqlite';
import { User } from './entities/User';
async function main() {
const orm = await MikroORM.init({
entities: [User],
dbName: 'test.db',
driver: SqliteDriver,
debug: true, // логи SQL-запросов
});
// Создаем схему (в dev-режиме)
await orm.getSchemaGenerator().createSchema();
// Получаем EntityManager
const em = orm.em.fork();
// Создаем пользователя
const user = em.create(User, {
name: 'Иван',
email: 'ivan@example.com',
});
await em.flush(); // сохраняем в БД
// Читаем пользователя
const found = await em.findOne(User, { email: 'ivan@example.com' });
console.log('Найден:', found);
// Обновляем
found!.name = 'Петр';
await em.flush();
// Удаляем
await em.removeAndFlush(found!);
await orm.close();
}
main().catch(console.error);
Запустите скрипт:
npx ts-node index.ts
Вы увидите SQL-запросы в консоли и созданный файл test.db.
4. Основные методы/функции/классы
4.1. MikroORM.init()
Сигнатура: static init(options: Options): Promise<MikroORM>
Что делает: Инициализирует ORM, создает пул соединений и загружает метаданные сущностей. Это главная точка входа.
Пример:
const orm = await MikroORM.init({
entities: ['./dist/entities'], // путь к скомпилированным сущностям
dbName: 'my_db',
driver: PostgreSqlDriver,
user: 'admin',
password: 'secret',
});
4.2. EntityManager.flush()
Сигнатура: flush(): Promise<void>
Что делает: Синхронизирует все изменения (вставки, обновления, удаления) с базой данных в рамках одной транзакции. Является реализацией паттерна Unit of Work.
Пример:
const em = orm.em.fork();
const user = em.create(User, { name: 'Анна' });
user.name = 'Анна Смирнова';
await em.flush(); // INSERT + UPDATE выполнится одним запросом
4.3. EntityManager.find()
Сигнатура: find(entityName: EntityName<T>, where: FilterQuery<T>, options?: FindOptions<T>): Promise<T[]>
Что делает: Выполняет поиск сущностей по заданным критериям. Возвращает массив сущностей.
Пример:
const admins = await em.find(User, { role: 'admin' }, {
populate: ['profile'], // загрузить связанные сущности
limit: 10,
orderBy: { createdAt: 'DESC' },
});
4.4. EntityManager.findOne()
Сигнатура: findOne(entityName: EntityName<T>, where: FilterQuery<T>, options?: FindOneOptions<T>): Promise<T | null>
Что делает: Ищет одну сущность по условию. Возвращает null, если ничего не найдено.
Пример:
const user = await em.findOne(User, { id: 1 });
if (user) {
console.log(user.name);
}
4.5. EntityManager.create()
Сигнатура: create(entityName: EntityName<T>, data: Partial<T>): T
Что делает: Создает новый экземпляр сущности на основе переданных данных и помечает его для вставки при следующем flush(). Не выполняет запрос к БД.
Пример:
const user = em.create(User, {
name: 'Ольга',
email: 'olga@test.com',
});
// user теперь управляется EntityManager, но не сохранен
4.6. EntityManager.persist()
Сигнатура: persist(entity: AnyEntity | AnyEntity[]): void
Что делает: Помечает сущность (или массив сущностей) для сохранения. Отличие от create в том, что persist работает с уже существующими объектами.
Пример:
const user = new User();
user.name = 'Дмитрий';
em.persist(user); // теперь он будет сохранен при flush
await em.flush();
4.7. EntityManager.remove()
Сигнатура: remove(entity: AnyEntity): void
Что делает: Помечает сущность для удаления при следующем flush().
Пример:
const user = await em.findOne(User, 1);
em.remove(user!);
await em.flush(); // DELETE FROM user WHERE id = 1
4.8. EntityManager.fork()
Сигнатура: fork(options?: ForkOptions): EntityManager
Что делает: Создает новый дочерний EntityManager с собственным Identity Map. Полезно для обработки запросов в веб-приложениях, чтобы избежать проблем с кэшированием.
Пример:
// В каждом HTTP-запросе создаем новый контекст
app.get('/users', async (req, res) => {
const em = orm.em.fork();
const users = await em.find(User, {});
res.json(users);
});
4.9. QueryBuilder — em.createQueryBuilder()
Сигнатура: createQueryBuilder(entityName: EntityName<T>, alias?: string): QueryBuilder<T>
Что делает: Создает построитель запросов для сложных SQL-запросов (JOIN, подзапросы, агрегации).
Пример:
const qb = em.createQueryBuilder(User, 'u');
const result = await qb
.select('u.*')
.leftJoinAndSelect('u.posts', 'p')
.where({ 'u.active': true })
.orderBy({ 'u.createdAt': 'DESC' })
.limit(5)
.execute();
4.10. EntityRepository — em.getRepository()
Сигнатура: getRepository(entityName: EntityName<T>): EntityRepository<T>
Что делает: Возвращает репозиторий для сущности, который предоставляет удобные методы для работы (find, findOne, persist и т.д.). Репозиторий можно использовать как сервис.
Пример:
const userRepo = em.getRepository(User);
const user = await userRepo.findOneOrFail({ email: 'test@test.com' });
await userRepo.nativeDelete({ active: false }); // DELETE напрямую
5. Реальный практический пример (100-200 строк кода)
Создадим мини-блог с сущностями User, Post и Comment, используя связи ManyToOne и OneToMany.
entities/User.ts:
import { Entity, PrimaryKey, Property, OneToMany, Collection } from '@mikro-orm/core';
import { Post } from './Post';
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Property({ unique: true })
email!: string;
@OneToMany(() => Post, post => post.author)
posts = new Collection(this);
}
entities/Post.ts:
import { Entity, PrimaryKey, Property, ManyToOne, OneToMany, Collection } from '@mikro-orm/core';
import { User } from './User';
import { Comment } from './Comment';
@Entity()
export class Post {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property({ columnType: 'text' })
content!: string;
@ManyToOne(() => User)
author!: User;
@OneToMany(() => Comment, comment => comment.post)
comments = new Collection(this);
@Property()
createdAt: Date = new Date();
}
entities/Comment.ts:
import { Entity, PrimaryKey, Property, ManyToOne } from '@mikro-orm/core';
import { Post } from './Post';
@Entity()
export class Comment {
@PrimaryKey()
id!: number;
@Property()
text!: string;
@ManyToOne(() => Post)
post!: Post;
@Property()
createdAt: Date = new Date();
}
app.ts — основной файл с логикой:
import 'reflect-metadata';
import { MikroORM, EntityManager } from '@mikro-orm/core';
import { SqliteDriver } from '@mikro-orm/sqlite';
import { User } from './entities/User';
import { Post } from './entities/Post';
import { Comment } from './entities/Comment';
async function blogExample() {
const orm = await MikroORM.init({
entities: [User, Post, Comment],
dbName: 'blog.db',
driver: SqliteDriver,
debug: false,
});
// Создаем схему
await orm.getSchemaGenerator().createSchema();
const em = orm.em.fork();
// 1. Создаем автора
const author = em.create(User, {
name: 'Мария',
email: 'maria@blog.com',