< Back to articles

The Best of ORMs: #2 Mikro-ORM

Jakub ValaJakub Vala
November 16, 2022

Previously, I briefly explained what ORMs are, why to use them, and I wrote about Sequelize. In this blog post I'm gonna share something about Mikro-ORM, and I'm going to compare it with Sequelize.

Mikro-ORM is a TypeScript ORM for Node.js applications. It comes with many features: 

  • multiple ways to define entities,
  • entity generator from existing database,
  • implicit transactions,

which makes it a strong tool. You can choose to work with SQL or No-SQL databases, but in this blog I'm gonna be using PostgreSQL and the database schema that I described in my previous blog post.

Installing Mikro-ORM for PostgreSQL

npm i @mikro-orm/core  
  
npm i @mikro-orm/postgresql

Set up for mikro-orm/cli

Define path to a config file in package.json

"mikro-orm": {  
    "configPaths": [  
      "./db.config.js"  
    ]  
  },

Connection

I used types where possible, because Mikro-ORM type support is great. For database connection I specified the type of db driver I'm gonna be using and the properties needed for initialization.

You can specify some other additional properties like replica databases, logger, debug logs, and more, which will help you with development or debugging in case you have some issues.

import type { PostgreSqlDriver } from '@mikro-orm/postgresql';  
  
const orm = MikroORM.init({  
  entitiesTs: ['src/app/database/**.*ts'],  
  dbName: 'my-db-name',  
  type: 'postgresql',  
  host: 'localhost',  
  port: 5432,  
  user: 'postgres',  
  password: 'postgres',  
 baseDir: process.cwd(),  
  
replicas: [  
     { user: 'read-user-1', host: 'read-1.db.example.com', port: 5433 },  
     { user: 'read-user-2', host: 'read-2.db.example.com', port: 5434 },  
     { user: 'read-user-3', host: 'read-3.db.example.com', port: 5435 },  
  
  ],  
 logger: (message: string) => myLogger.info(message),  
  debug: true, _// or provide array like ['query', 'query-params']_  
})

Entity

There are two approaches to modeling entities:

  • using classes and decorators
  • using classes with entity schema

I went with decorated classes, because for me it’s more straightforward.

Going with the first approach, you need to use decorators and for that you need to set up the tsconfig.json file.

...  
"experimentalDecorators": true,  
"emitDecoratorMetadata": true,  
"esModuleInterop": true  
...

Entity (class with decorators)

With decorated class entities, defining relations worked correctly for me. This is how schemas are defined in typescript with Mikro-ORM:

import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'  
import { TripDriver } from './TripDriver.entity'  
  
@Entity({ tableName: 'drivers' })  
export class Driver {  
  @PrimaryKey()  
  id!: number  
  
  @Property({ nullable: false, unique: true })  
  username!: string  
  
  @Property({ nullable: true })  
  firstname?: string  
  
  
  @Property({ nullable: true })  
  surname?: string  
  
  @Property({ nullable: true, onCreate: () => new Date() })  
  created_at?: Date  
  
  @Property({ nullable: true, onCreate: () => new Date(), onUpdate: () => new Date() })  
  updated_at?: Date  
  
  @Property({ nullable: true })  
  deleted_at?: Date  
  
  @OneToMany('tripDriver','driver')  
  tripDriver = new Collection(this)  
}

On every column you need to use the decorator @Property and define constraints, hooks (onCreate, onUpdate, ...), and the name of the column.

For the entity, you can specify a relation. In my case, I defined the relation @ManyToOne, a reference to the drivers entity on tripDrivers entity:

@ManyToOne(() => Driver, { wrappedReference: true })  
  driver: IdentifiedReference'id'>  
  
  
constructor(driver: Driver) {  
  this.driver = Reference.create(driver)  
}

wrappedReference wraps an entity in a Reference wrapper which provides better type-safety. It maps the reference to the referenced entity object. However, you have to create a reference to the entity in the entity constructor.

When you populate the referenced entity, you can call Entity.getEntity() on a referenced entity, which is a synchronous getter that will first check whether the wrapped entity is initialized and if not, it will throw an error.

const driverToTrips = tripDriverRepo.findAll({ populate: ['driver'] })  
  
driverToTrips.map(trips => {  
    const driver = trips.driver.getEntity()  
})

IdentifiedReference directly adds a primary key to the Reference instance.

A simpler way to reference an entity is to define the entity itself when you define the  @ManyToOne relation. Without wrappedReference, typescript would think that the entity is always loaded. Without wrappedReference:

@ManyToOne()  
driver!: Driver
const driverToTrips = tripDriverRepo.findAll({ populate: ['driver'] })  
  
driverToTrips.map(trips => {  
    const driver = trips.driver  
})

@OneToMany, a reference to the tripDrivers entity on Driver entity:

@OneToMany('TripDriver','driver')  
  tripDriver = new Collection(this)

CRUD operations

To perform a CRUD operation, you need to create a repository from an initialized connection and a defined entity, in my case Driver. You call a connection.em.getRepository(Driver) on the connection with an entity and from that you get an entity repository object with CRUD operation methods.

This is a slightly different approach from Sequelize ORM where you can perform CRUD operations on the entity itself. Here you separate the entity and connection and then you connect the entity with the connection. I prefer working with data mapper APIs, because they generally do better separation of concerns. The other approach would be the active record. The differences between active record and data mapper are well described in the article available here: https://orkhan.gitbook.io/typeorm/docs/active-record-data-mapper.

One great feature is that all CRUD operations support batching, which means you can delete/update/create/read more records in one call by default.

Read

Call find, or findOn.

const selectQueries = (connection: MikroORM) => {  
    const driverRepo = connection.em.getRepository(Driver)  
  
    const selectById = (id: number) => driverRepo.findOne({ id })  
    const selectAllLimited = () => driverRepo.find({  
        $and: [{  
            firstname: 'Charles',  
            surname: 'Fourth',  
        }]  
    },{  
        orderBy: {  
            id: QueryOrder.DESC,  
        },  
        limit: 10,  
    })  
    ...

If you want to use operators such as and, or, in, etc, you can specify them in the find call in the first parameter with a dollar sign prefix, and you put operator values in an array object. You have basically the same options as you would have writing operators in a raw query. Operators which you can use with specific databases are defined in documentation.

Create

User create, or nativeInsert. Native means that the input is transformed into native sql query via QueryBuilder. The same for update and delete methods. That means it doesn't hydrate results to entities and it doesn't trigger the onCreate lifecycle hooks.

const driverRepo = connection.em.getRepository(Driver)  
  
const insertDriver = (surname: string) =>  
    driverRepo.nativeInsert({ username: randomUUID(), surname })

Update 

   const driverRepo = connection.em.getRepository(Driver)  
   const updateById = (id: number) =>  
       driverRepo.nativeUpdate({ id }, { surname: 'Vomáčka' })

Delete

   const driverRepo = connection.em.getRepository(Driver)  
   const deleteById = (id: number) =>  
       driverRepo.nativeDelete({ id })

Relations

I described how to define relations on entities before. When you want to map a referenced entity you call findAll and provide {populate: [...]} parameter to relate one entity to another.

driverRepo.findAll({ populate: ['tripDriver'] })
tripDriverRepo.findAll({ populate: ['driver'] })

Migrations

Migrations are a strong side of Mikro-ORM. However, you need to do a little bit of a set up and I’m gonna show you how to do it.

Install dependencies for running migrations

npm i @mikro-orm/cli @mikro-orm/core @mikro-orm/migrations @mikro-orm/postgresql pg

Create a config.js file

const options = {  
 ...  
 migrations: {  
       tableName: 'migrations-mikro-orm',  
       path: 'migrations',  
       transactional: true,  
       allOrNothing: true,  
       emit: 'js',  
  
                    safe: true,  
                    disableForeignKeys: false,  
                    dropTables: false,  
                    fileName: timestamp => `${timestamp}-my-migration`,  
                    snapshot: true,  
 },  
 }  
  
module.exports = options

Initial migration

Initial migration is a specific migration operation and it's used only when you've already created entities and relations among them in your code. By running initial migration, a migration file is created with generated code that will create tables based on defined entities. After running the created migration file from initial migration, it creates tables in your database.

npx mikro-orm migration:create --initial

Migrate up to the latest version

npx mikro-orm migration:up

Migrate only specific migrations

npx mikro-orm migration:up --from 2019101911 --to 2019102117

Migrate one migration

npx mikro-orm migration:up --only 2019101923 

Rollback migration

npx mikro-orm migration:down

List all migrations

npx mikro-orm migration:list

List pending migrations

npx mikro-orm migration:pending

Drop the database and migrate up to the latest version

mikro-orm migration:fresh

Seeds

Seeds are a strong side of Mikro-ORM as well. The greatest thing is you can run seeds from an sql file directly. You only need to specify a path to a seed file.

npx mikro-orm database:import ../seeds.sql

Summary

Mikro-ORM is a very powerful tool, it provides a lot of regular and advanced features. The documentation is well written, although for a basic use, it's maybe too detailed, and it could be tricky to find what you want at first glance.

With entity modeling, I like that you can choose between decorated classes and entity schema. If I compare defining entities with Sequelize, I would say it uses the same approach, but offers a little bit more options.

When it comes to migrations and seeds, Mikro-ORM is the right tool for that and honestly for me is a winner so far. Apart from basic migration functionality, it provides you with some options that other ORMs don't have, such as generating database tables schema from entities in code or the other way of generating entities from database tables. I only missed describing configuration for migrations, because I had to do a little bit of investigation on how to set up the configuration file for migrations properly.

That was a short description of how to use Mikro-ORM, the key features of it and its pros and cons. The next post will be about Zapatos.

Jakub Vala
Jakub Vala
Backend DeveloperKuba's focus is always on writing clean code. He like cats, skateboarding, beaches and warm weather.

Are you interested in working together? Let’s discuss it in person!

Get in touch >