loading...

Introducing MikroORM, TypeScript data-mapper ORM with Identity Map

b4nan profile image Martin Adámek ・6 min read

Motivation

During my early days at university, I remember how quickly I fell in love with object oriented programming and the concepts of Object-relational mapping and Domain Driven Design. Back then, I was mainly a PHP programmer (while we did a lot of Java/Hibernate at school), so a natural choice for me was to start using Doctrine.

A few years ago, when I switched from PHP to Node.js (and later to TypeScript), I was really confused. How come there is nothing similar to Hibernate or Doctrine in the JavaScript world? About a year ago, I finally came across TypeORM, and when I read this line in the readme I thought I found what I was looking for:

TypeORM is highly influenced by other ORMs, such as Hibernate, Doctrine and Entity Framework.

I started playing with it immediately, but I got disappointed very quickly. No Identity Map that would keep track of all loaded entities. No Unit of Work that would handle transaction isolation. No unified API for references with very strange support for accessing just the identifier without populating the entity, MongoDB driver (which I was aiming to use) was experimental and I had a lot problems setting it up. After a few days of struggle, I went away from it.

By that time, I started to think about writing something myself. And that is how MikroORM started!

MikroORM is TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns.

Currently it supports MongoDB, MySQL, PostgreSQL and SQLite databases, but more can be supported via custom drivers right now. It has first class TypeScript support, while staying back compatible with Vanilla JavaScript.

Installation

First install the module via yarn or npm and do not forget to install the database driver as well. Next you will need to enable support for decorators

in tsconfig.json via experimentalDecorators flag. Then call MikroORM.init as part of bootstrapping your application.

Last step is to provide forked EntityManager for each request, so it will have its own unique Identity Map. To do so, you can use EntityManager.fork() method. Another way, that is more DI friendly, is to create new request context for each request, which will use some dark magic in the background to always pick the right EntityManager for you.

Defining entities

To define an entity, simply create a class and decorate it. Here is an example of Book entity defined for MongoDB driver:

As you can see, it’s pretty simple and straightforward. Entities are simple JavaScript objects (so called POJO), decorated with @Entity decorator (for TypeScript), or accompanied with schema definition object (for vanilla JavaScript). No real restrictions are made, you do not have to extend any base class, you are more than welcome to use entity constructors for specifying required parameters to always keep the entity in valid state. The only requirement is to define the primary key property.

You might be curious about the last line with Book as an interface. This is called interface merging and it is there to let TypeScript know the entity will have some extra API methods (like init() or isInitialized()) available as it will be monkey-patched during discovery process. More about this can be found in the docs.

Persisting entities with EntityManager

To save entity state to database, you need to persist it. Persist takes care or deciding whether to use insert or update and computes appropriate change-set. As a result, only changed fields will be updated in database.

MikroORM comes with support for cascading persist and remove operations. Cascade persist is enabled by default, which means that by persisting an entity, all referenced entities will be automatically persisted too.

Fetching entities

To fetch entities from database you can use find() and findOne() methods of EntityManager:

More convenient way of fetching entities from database is by using EntityRepository, that carries the entity name so you do not have to pass it to every find and findOne calls:

Working with references

Entity associations are mapped to entity references. Reference is an entity that has at least the identifier (primary key). This reference is stored in the Identity Map so you will get the same object reference when fetching the same document from database.

Thanks to this concept, MikroORM offers unified API for accessing entity references, regardless of whether the entity is initialized or not. Even if you do not populate an association, there will be its reference with primary key set. You can call await entity.init() to initialize the entity. This will trigger database call and populate itself, keeping the same reference to entity object in identity map.

Identity Map and Unit of Work

MikroORM uses the Identity Map in background to track objects. This means that whenever you fetch entity via EntityManager, MikroORM will keep a reference to it inside its UnitOfWork, and will always return the same instance of it, even if you query one entity via different properties. This also means you can compare entities via strict equality operators (=== and !==):

Another benefit of Identity Map is that this allows us to skip some database calls. When you try to load an already managed entity by its identifier, the one from Identity Map will be returned, without querying the database.

The power of Unit of Work is in running all queries inside a batch and wrapped inside a transaction (if supported by given driver). This approach is usually more performant as opposed to firing queries from various places.

Collections

OneToMany and ManyToMany collections are stored in a Collection wrapper. It implements iterator so you can use for of loop to iterate through it.

Another way to access collection items is to use bracket syntax like when you access array items. Keep in mind that this approach will not check if the collection is initialized, while using get method will throw error in this case.

More informations about collections can be found in the docs.

What’s next?

So you read through the whole article, got here and still not satisfied? There are more articles to come (beginning with integration manual for popular frameworks like Express or NestJS), but you can take a look at some advanced features covered in docs right now:

To start playing with MikroORM, go through quick start and read the docs. You can also take a look at example integrations with some popular frameworks.

Like MikroORM? ⭐️ Star it on GitHub and share this article with your friends.

This article was originally published on Medium: https://medium.com/dailyjs/introducing-mikro-orm-typescript-data-mapper-orm-with-identity-map-9ba58d049e02

Posted on by:

b4nan profile

Martin Adámek

@b4nan

Long time PHP developer (Nette, Doctrine), now shifter towards JavaScript world (Node.js, Angular, TypeScript, NestJS).

Discussion

pic
Editor guide
 

Looks very interesting! I too came from the PHP world and made a 2nd stop at TypeORM (sequelize was my 1st had some really bad experiences with it).

Got one question though, since I see many similarities between TypeORM and MikroORM: Why not try to contribute to the TypeORM community, to help them grow? I think it's OK to reinvent the wheel when learning, but on Open Source wouldn't it make more sense to prefer collaboration with existing solutions instead of rolling out your own? I'd understand if you needed extra power and made a work/proposal that got rejected.

 

I don't think they are that similar under the hood. TypeORM does not implement neither unit of work nor identity map, so the underlying logic is really different I believe.

My main reason to start new project was not to be bound by such a huge community - I don't see how we could maintain back compatibility while introducing UoW and IM to TypeORM. It would have to be another (optional) way to use it, which would make it even more complex (which it already is).

 
 

Hello. This is very nice. May I ask whats the difference between Entity manager and repository? Can I use them interchangeably? Or they work together in the same way? Can I use it like an active record? If there not the same is there a benefit over the other? Thank you for your response sir. Will definitely share this to some friends.

Example:

class BaseClass extends EntityManager {}

@Entity()
class User extends BaseClass {
 aMethod: void {};
}

User.create()

Ps: i googled the difference, it shows the stack over flow about java and its all abstraction over the other.

 

All what depository does it to forward the calls to EM, so the only real difference is that it bares the entity name for you. Check the source code, its really just about that:

github.com/mikro-orm/mikro-orm/blo...

// this is equal
const a1 = await em.findOne(Author, 1);
const a2 = await em.getRepository(Author).findOne(1);

So the benefit is that the API is simpler (no need to pass the entity name), and that repositories are an extension point - you can implement your own and add more methods there.

And no, there is no active record support and it never will be - I don't think it could even work.

 

But still I can use the EntityManager like:

class User extends EntityManage {
}

User.findOne(User, 1)

I'm asking this because instead of importing em each time you want to access an Entity would be cumbersome. Instead, I can just extend its static methods.

Thank you for the great response sir.

No, as I said, this is not supported. You need to have an instance of entity manager (one per each request) as it holds the state (identity map).

 

I know you metioned you had mongodb in mind when writing
I've read that sql type of relationships are actually not that useful in mongo and can put some performance penalties on large datasets, are the relationship decorators useful in the case of mongodb?

 

Sure they are! MikroORM uses simple queries to fetch relationships so that each database table is queried once instead of complex joining. I believe that the problem you are talking about is when using complex aggregations to imitate SQL joins, which is not the case here.

Take a look here for example of how populating relationships work: b4nan.github.io/mikro-orm/nested-p...

One note about performance - there are 4 test suites testing entity manager, one for each driver, and the mongo driver one runs about 2-3x faster than mysql/postgres (sqlite one is of course fastest).

 

Ahh I overlooked that in the docs! I shall take this one on a spin then :) awesome Project by the way!