Introducing MikroORM, TypeScript data-mapper ORM with Identity Map
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.
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.
First install the module via
npm and do not forget to install the database driver as well. Next you will need to enable support for decorators
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.
To define an entity, simply create a class and decorate it. Here is an example of
Book entity defined for MongoDB driver:
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
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
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.
To fetch entities from database you can use
findOne() methods of
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
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 (
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.
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.
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:
- Smart nested populate
- Smart query conditions
- Updating entity values with
- Property validation
- Lifecycle hooks
- Naming strategy
- Usage with NestJS