Skip to main content
Technology & EngineeringOrm300 lines

Mikro ORM

MikroORM for TypeScript with Unit of Work, Identity Map, decorator-based entities, and support for SQL and MongoDB

Quick Summary32 lines
You are an expert in MikroORM for database access and data modeling. You help developers define entities, manage the Unit of Work, write queries, handle migrations, and apply MikroORM patterns effectively.

## Key Points

- **Entities** — decorator-based class definitions
- **EntityManager (em)** — manages entity lifecycle, implements Unit of Work
- **Identity Map** — ensures one instance per entity per request
- **QueryBuilder** — type-safe fluent query API
- **Schema Generator & Migrations** — DDL management
- **Always fork the EntityManager** per request using `em.fork()` or `RequestContext`. Never share an em across requests — the Identity Map will cause stale data and memory leaks.
- **Let the Unit of Work batch changes** — call `persist()` multiple times but `flush()` only once at the end. This minimizes database round-trips.
- **Use `populate`** to eagerly load relations instead of accessing uninitialized collections, which throws.
- **Prefer `em.findOneOrFail()`** over `em.findOne()` when a missing entity is an error condition.
- **Use `em.create()`** instead of `new Entity()` for proper entity initialization and type safety.
- **Configure `RequestContext`** in web frameworks so each HTTP request gets its own Identity Map.
- **Accessing unloaded collections**: Trying to iterate a `Collection` that hasn't been populated throws `ValidationError`. Always `populate` or `init()` collections before access.

## Quick Example

```bash
npm install @mikro-orm/core @mikro-orm/postgresql  # or mysql, sqlite, mongodb
npm install -D @mikro-orm/cli @mikro-orm/migrations
```

```bash
npx mikro-orm migration:create    # generate migration from schema diff
npx mikro-orm migration:up        # run pending migrations
npx mikro-orm migration:down      # revert last migration
npx mikro-orm schema:update --run # direct schema sync (dev only)
```
skilldb get orm-skills/Mikro ORMFull skill: 300 lines
Paste into your CLAUDE.md or agent config

MikroORM — Database Toolkit

You are an expert in MikroORM for database access and data modeling. You help developers define entities, manage the Unit of Work, write queries, handle migrations, and apply MikroORM patterns effectively.

Overview

MikroORM is a TypeScript ORM based on the Data Mapper, Unit of Work, and Identity Map patterns. It supports PostgreSQL, MySQL, MariaDB, SQLite, and MongoDB. It draws inspiration from Doctrine (PHP) and Hibernate (Java), offering a sophisticated entity management system with automatic change tracking.

Key components:

  • Entities — decorator-based class definitions
  • EntityManager (em) — manages entity lifecycle, implements Unit of Work
  • Identity Map — ensures one instance per entity per request
  • QueryBuilder — type-safe fluent query API
  • Schema Generator & Migrations — DDL management

Setup & Configuration

Installation

npm install @mikro-orm/core @mikro-orm/postgresql  # or mysql, sqlite, mongodb
npm install -D @mikro-orm/cli @mikro-orm/migrations

Configuration (mikro-orm.config.ts)

import { defineConfig } from '@mikro-orm/postgresql';
import { Migrator } from '@mikro-orm/migrations';

export default defineConfig({
  entities: ['./dist/entities'],
  entitiesTs: ['./src/entities'],
  dbName: process.env.DB_NAME ?? 'myapp',
  host: process.env.DB_HOST ?? 'localhost',
  port: Number(process.env.DB_PORT ?? 5432),
  user: process.env.DB_USER ?? 'postgres',
  password: process.env.DB_PASSWORD ?? '',
  debug: process.env.NODE_ENV === 'development',
  extensions: [Migrator],
  migrations: {
    path: './src/migrations',
    pathTs: './src/migrations',
  },
});

Entity Definitions

// src/entities/User.ts
import { Entity, PrimaryKey, Property, OneToMany, Collection, Unique } from '@mikro-orm/core';
import { Post } from './Post';

@Entity()
export class User {
  @PrimaryKey()
  id!: number;

  @Property()
  @Unique()
  email!: string;

  @Property({ nullable: true, length: 100 })
  name?: string;

  @OneToMany(() => Post, (post) => post.author)
  posts = new Collection<Post>(this);

  @Property()
  createdAt: Date = new Date();

  @Property({ onUpdate: () => new Date() })
  updatedAt: Date = new Date();
}

// src/entities/Post.ts
import { Entity, PrimaryKey, Property, ManyToOne, Index } from '@mikro-orm/core';
import { User } from './User';

@Entity()
export class Post {
  @PrimaryKey()
  id!: number;

  @Property({ length: 255 })
  title!: string;

  @Property({ type: 'text', nullable: true })
  content?: string;

  @Property({ default: false })
  published: boolean = false;

  @ManyToOne(() => User)
  @Index()
  author!: User;

  @Property()
  createdAt: Date = new Date();
}

Initialization

// src/db.ts
import { MikroORM, EntityManager } from '@mikro-orm/postgresql';
import config from '../mikro-orm.config';

let orm: MikroORM;

export async function initORM(): Promise<MikroORM> {
  orm = await MikroORM.init(config);
  return orm;
}

export function getEM(): EntityManager {
  return orm.em.fork(); // always fork for request isolation
}

Migrations

npx mikro-orm migration:create    # generate migration from schema diff
npx mikro-orm migration:up        # run pending migrations
npx mikro-orm migration:down      # revert last migration
npx mikro-orm schema:update --run # direct schema sync (dev only)

Core Patterns

Unit of Work — Persist & Flush

import { getEM } from './db';
import { User } from './entities/User';
import { Post } from './entities/Post';

const em = getEM();

// Create — changes tracked, not yet persisted
const user = em.create(User, { email: 'alice@example.com', name: 'Alice' });
em.persist(user);

const post = em.create(Post, { title: 'First post', author: user, published: true });
em.persist(post);

// Flush — commits all tracked changes in a single transaction
await em.flush();

Querying

const em = getEM();

// Find by primary key
const user = await em.findOneOrFail(User, { id: 1 });

// Find with conditions
const users = await em.find(User, {
  email: { $like: '%@example.com' },
}, {
  orderBy: { createdAt: 'DESC' },
  limit: 10,
  offset: 0,
  fields: ['id', 'email', 'name'],
});

// Find with populated relations
const postsWithAuthors = await em.find(Post, { published: true }, {
  populate: ['author'],
  orderBy: { createdAt: 'DESC' },
});

// Find or create
const [user, created] = await em.findOrCreate(User, {
  email: 'alice@example.com',
}, {
  name: 'Alice',
});

QueryBuilder

const em = getEM();

const results = await em.createQueryBuilder(Post, 'p')
  .select(['p.title', 'u.name'])
  .join('p.author', 'u')
  .where({ 'p.published': true })
  .orderBy({ 'p.createdAt': 'DESC' })
  .limit(20)
  .getResultList();

// Aggregation
const stats = await em.createQueryBuilder(Post, 'p')
  .select(['p.author', 'count(p.id) as postCount'])
  .groupBy('p.author')
  .having({ 'count(p.id)': { $gt: 5 } })
  .execute();

Updating Entities

const em = getEM();

// Load, modify, flush (recommended)
const user = await em.findOneOrFail(User, { id: 1 });
user.name = 'Alice Updated';
await em.flush();  // Unit of Work detects the change automatically

// Bulk update via QueryBuilder
await em.createQueryBuilder(Post)
  .update({ published: false })
  .where({ createdAt: { $lt: cutoffDate } })
  .execute();

Transactions

const em = getEM();

await em.transactional(async (txEm) => {
  const user = txEm.create(User, { email: 'bob@example.com', name: 'Bob' });
  txEm.persist(user);

  const post = txEm.create(Post, { title: 'Hello', author: user, published: true });
  txEm.persist(post);

  // flush happens automatically at end of transactional block
});

Request Context (Web Frameworks)

// Express middleware
import { RequestContext } from '@mikro-orm/core';

app.use((req, res, next) => {
  RequestContext.create(orm.em, next);
});

// In route handlers, em is automatically forked per request
app.get('/users', async (req, res) => {
  const em = orm.em;  // returns the forked em from RequestContext
  const users = await em.find(User, {});
  res.json(users);
});

Core Philosophy

MikroORM brings the Unit of Work and Identity Map patterns — proven in enterprise ORMs like Hibernate and Doctrine — to the TypeScript ecosystem. The Unit of Work tracks every entity loaded or created during a request, detects which properties changed, and flushes all modifications in a single, optimally batched transaction when you call flush(). This means you do not issue individual UPDATE statements for each change; the ORM collects changes and writes them all at once, minimizing database round-trips and ensuring transactional consistency.

The Identity Map guarantees that loading the same entity twice within a single EntityManager fork returns the same object reference, not a duplicate. This eliminates an entire class of bugs where two copies of the same database row get out of sync within a request. It also means that modifications to an entity are visible everywhere that entity is referenced, without explicit cache invalidation. The Identity Map is request-scoped by design: every HTTP request (or every unit of work) gets a fresh fork of the EntityManager with its own Identity Map, preventing cross-request data leakage.

The per-request EntityManager fork is the architectural foundation of MikroORM in web applications, and getting it wrong breaks everything. A shared EntityManager across requests causes the Identity Map to grow unbounded, serve stale data from previous requests, and leak one user's data to another. Every request must call em.fork() or use RequestContext to get an isolated EntityManager instance. This is not optional — it is the single most important pattern in MikroORM usage.

Anti-Patterns

  • Sharing an EntityManager across requests — using a single em instance for all requests causes the Identity Map to accumulate entities from every request, serving stale data and leaking information between users. Always fork per request.

  • Calling persist() without flush()persist() only marks an entity as managed by the Unit of Work. Without a subsequent flush(), nothing is written to the database. This is the most common "my data is not saving" bug.

  • Accessing unloaded collections without populate — iterating or reading a Collection that has not been populated throws a ValidationError. Always use populate in find options or call collection.init() before accessing collection contents.

  • Using schema:update --run in production — the schema synchronizer compares entities to the database and applies changes directly, which can drop columns and lose data. Always use the migration system for production schema changes.

  • Reassigning Collection properties — writing user.posts = [newPost] does not work because Collection is a managed object. Use user.posts.set([newPost]), user.posts.add(newPost), or user.posts.remove(oldPost) to modify collections correctly.

Best Practices

  • Always fork the EntityManager per request using em.fork() or RequestContext. Never share an em across requests — the Identity Map will cause stale data and memory leaks.
  • Let the Unit of Work batch changes — call persist() multiple times but flush() only once at the end. This minimizes database round-trips.
  • Use populate to eagerly load relations instead of accessing uninitialized collections, which throws.
  • Prefer em.findOneOrFail() over em.findOne() when a missing entity is an error condition.
  • Use em.create() instead of new Entity() for proper entity initialization and type safety.
  • Configure RequestContext in web frameworks so each HTTP request gets its own Identity Map.

Common Pitfalls

  • Accessing unloaded collections: Trying to iterate a Collection that hasn't been populated throws ValidationError. Always populate or init() collections before access.
  • Shared EntityManager: Using a single em across requests causes the Identity Map to grow unbounded and serve stale data. Always fork per request.
  • Forgetting flush(): persist() only marks entities for tracking. Without flush(), nothing is written to the database.
  • Detached entities across ems: Passing an entity from one forked em to another causes errors. Re-fetch or use em.merge().
  • synchronize in production: Using schema:update --run in production can drop columns. Always use migrations.
  • Collection assignment: You cannot reassign a Collection property (e.g., user.posts = [...]). Use collection.set(), collection.add(), or collection.remove() instead.

Install this skill directly: skilldb add orm-skills

Get CLI access →