Mikro ORM
MikroORM for TypeScript with Unit of Work, Identity Map, decorator-based entities, and support for SQL and MongoDB
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 linesMikroORM — 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()withoutflush()—persist()only marks an entity as managed by the Unit of Work. Without a subsequentflush(), 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 aCollectionthat has not been populated throws aValidationError. Always usepopulatein find options or callcollection.init()before accessing collection contents. -
Using
schema:update --runin 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 becauseCollectionis a managed object. Useuser.posts.set([newPost]),user.posts.add(newPost), oruser.posts.remove(oldPost)to modify collections correctly.
Best Practices
- Always fork the EntityManager per request using
em.fork()orRequestContext. 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 butflush()only once at the end. This minimizes database round-trips. - Use
populateto eagerly load relations instead of accessing uninitialized collections, which throws. - Prefer
em.findOneOrFail()overem.findOne()when a missing entity is an error condition. - Use
em.create()instead ofnew Entity()for proper entity initialization and type safety. - Configure
RequestContextin web frameworks so each HTTP request gets its own Identity Map.
Common Pitfalls
- Accessing unloaded collections: Trying to iterate a
Collectionthat hasn't been populated throwsValidationError. Alwayspopulateorinit()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. Withoutflush(), 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(). synchronizein production: Usingschema:update --runin production can drop columns. Always use migrations.- Collection assignment: You cannot reassign a
Collectionproperty (e.g.,user.posts = [...]). Usecollection.set(),collection.add(), orcollection.remove()instead.
Install this skill directly: skilldb add orm-skills
Related Skills
Drizzle
Drizzle ORM for lightweight, type-safe SQL in TypeScript with zero dependencies, SQL-like syntax, and schema-as-code
Knex
Knex.js SQL query builder for Node.js with schema building, migrations, seed files, and support for PostgreSQL, MySQL, and SQLite
Kysely
Kysely type-safe SQL query builder for TypeScript with zero overhead, full autocompletion, and no code generation
Mongoose
Mongoose ODM for MongoDB with schema validation, middleware hooks, population, virtuals, and TypeScript support in Node.js
Prisma
Prisma ORM for type-safe database access with auto-generated client, migrations, and schema-first modeling in TypeScript/Node.js
Sequelize
Sequelize ORM for promise-based SQL database access with model definitions, associations, migrations, and transaction support in Node.js