Skip to main content
Version: 7.2

Usage with SQL drivers

MikroORM supports several SQL databases out of the box. Install the driver package for your database:

# for postgresql (works with cockroachdb too)
npm install @mikro-orm/postgresql

# for pglite (embedded PostgreSQL in WASM)
npm install @mikro-orm/pglite

# for mysql (works with mariadb too)
npm install @mikro-orm/mysql

# for mariadb (works with mysql too)
npm install @mikro-orm/mariadb

# for sqlite
npm install @mikro-orm/sqlite

# for libsql/turso
npm install @mikro-orm/libsql

# for sql.js (in-memory SQLite in WASM, works in the browser)
npm install @mikro-orm/sql-js

# for mssql
npm install @mikro-orm/mssql

# for oracle
npm install @mikro-orm/oracledb

The driver package has @mikro-orm/core as a peer dependency, which most package managers install automatically. If you plan to use additional packages like @mikro-orm/cli, @mikro-orm/migrations, or @mikro-orm/seeder, install @mikro-orm/core explicitly to ensure all packages share the same instance:

npm install @mikro-orm/core @mikro-orm/postgresql @mikro-orm/migrations @mikro-orm/cli

Getting started​

Create a configuration file and call MikroORM.init() to bootstrap the ORM. Import from your driver package to get access to driver-specific features like QueryBuilder:

import { MikroORM } from '@mikro-orm/postgresql'; // or any other SQL driver package

const orm = await MikroORM.init({
entities: [Author, Book],
dbName: 'my-db-name',
});

You can also use the defineConfig helper for type-safe configuration:

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

export default defineConfig({
entities: [Author, Book],
dbName: 'my-db-name',
});

To access driver-specific methods like em.createQueryBuilder(), import MikroORM, EntityManager, or EntityRepository from the driver package rather than @mikro-orm/core.

Schema management​

MikroORM provides several tools for managing your database schema:

  • SchemaGenerator — create, update, or drop your schema directly from entity metadata. Useful for prototyping and development.
  • Migrations — version-controlled schema changes for production workflows.
  • EntityGenerator — introspect an existing database and generate entity files from it.

QueryBuilder​

The QueryBuilder provides a fluent, type-safe API for constructing SQL queries. It is metadata-aware and can automatically handle joins, aliasing, and column mapping:

const qb = em.createQueryBuilder(Author);
qb.select('*')
.where({ name: { $like: '%test%' } })
.orderBy({ name: 'asc' })
.limit(10);

const authors = await qb.getResultList();

You can also use it for update and delete operations:

const qb = em.createQueryBuilder(Author);
await qb.update({ name: 'updated' }).where({ id: 123 }).execute();

await em.createQueryBuilder(Author).delete().where({ id: 456 }).execute();

For more details, see the QueryBuilder documentation.

Transactions​

All changes computed during em.flush() are executed inside a database transaction by default — you don't need to manage transactions manually for typical operations.

When you need explicit control, use em.transactional():

await em.transactional(async em => {
const author = new Author('God', 'hello@heaven.god');
em.persist(author);
// if an error occurs, all changes are rolled back
});

ManyToMany relations with pivot tables​

SQL drivers use pivot tables for ManyToMany relations. MikroORM manages these automatically — you only need to define the relation on your entities:

@ManyToMany(() => BookTag)
tags = new Collection<BookTag>(this);

To customize the pivot table name, use the pivotTable option:

@ManyToMany({ entity: () => BookTag, pivotTable: 'book2tag' })
tags = new Collection<BookTag>(this);

Native queries​

When you need to bypass the ORM and execute raw SQL:

// via QueryBuilder
const qb = em.createQueryBuilder(Author);
qb.select('*').where({ id: { $in: [1, 2, 3] } });
const res = await qb.execute();

// or raw SQL directly
const result = await em.execute('SELECT 1 + 1 as result');

For bulk operations that don't need change tracking, use the native methods:

// insert without creating an entity instance
await em.insert(Author, { name: 'test', email: 'test@example.com' });

// bulk update
await em.nativeUpdate(Author, { active: false }, { active: true });

// bulk delete
await em.nativeDelete(Author, { active: false });

These methods execute SQL directly and do not trigger lifecycle hooks.

Using SQLite extensions​

SQLite extensions like sqlean can add features that are missing by default (e.g., regexp). Load them via pool.afterCreate:

const orm = await MikroORM.init({
// ...
pool: {
afterCreate: (conn: any, done: any) => {
conn.loadExtension('/path/to/sqlean');
done(null, conn);
},
},
});

Using Turso database​

To connect to a remote Turso database, use the @mikro-orm/libsql driver with the password option for the auth token:

import { defineConfig } from '@mikro-orm/libsql';

export default defineConfig({
dbName: process.env.LIBSQL_URL,
password: process.env.LIBSQL_AUTH_TOKEN,
});

For embedded replicas with sync, use the driverOptions:

import { defineConfig } from '@mikro-orm/libsql';

export default defineConfig({
dbName: 'local.db',
password: process.env.LIBSQL_AUTH_TOKEN,
driverOptions: {
syncUrl: process.env.LIBSQL_URL,
syncPeriod: 0.5, // 500ms
},
});

Using Cloudflare D1 database​

Experimental: D1 support is experimental and has significant limitations. Use with caution.

Cloudflare D1 is a serverless SQLite database. Use MikroORM with D1 by passing a Kysely D1 dialect via driverOptions:

import { MikroORM } from '@mikro-orm/sqlite';
import { D1Dialect } from 'kysely-d1';

export default {
async fetch(request: Request, env: Env) {
const orm = await MikroORM.init({
entities: [...],
// the `dbName` is not used when a dialect is provided, but it's still required
dbName: 'd1',
driverOptions: new D1Dialect({ database: env.DB }),
// required: D1 does not support explicit transactions
implicitTransactions: false,
});

// ...
},
};

You can also pass a factory function if you need to create the dialect lazily:

MikroORM.init({
entities: [...],
dbName: 'd1',
driverOptions: () => new D1Dialect({ database: env.DB }),
implicitTransactions: false,
});

D1 Limitations​

D1 has significant limitations compared to regular SQLite:

  • No transaction support: D1 does not support explicit transaction statements (BEGIN TRANSACTION). You must set implicitTransactions: false for em.flush() to work. This means changes are not applied atomically — if an error occurs mid-flush, some changes may be persisted while others are not.
  • em.transactional() will not work: Since there's no transaction support, wrapping code in em.transactional() provides no atomicity guarantees.
  • No query streaming: Large result sets cannot be streamed and must be fetched entirely into memory.
  • Limited ALTER TABLE: No support for ALTER COLUMN or ADD CONSTRAINT, which affects schema migrations.

See the D1 SQL documentation for more details on supported SQL statements.

MS SQL Server limitations​

  • UUID values are returned in upper case
  • Cycles in cascade paths are not supported
  • Schema diffing capabilities are limited
  • No native support for fulltext search
  • Upsert support is limited

Oracle limitations​

  • Cycles in cascade paths are not supported
  • No ON UPDATE clause for foreign keys (Oracle does not support it natively)
  • No support for multiple statements in a single query
  • No support for tuple comparison in WHERE clauses
  • Foreign keys are not automatically indexed

Custom driver​

If you need to support a database that is not included, you can implement your own driver. See the custom driver documentation for details.

import { MyCustomDriver } from './MyCustomDriver.ts';

const orm = await MikroORM.init({
entities: [Author, Book],
dbName: 'my-db-name',
driver: MyCustomDriver,
});