MikroORM 4: Filling the Gaps

Martin Adámek
Sep 8 · 7 min read

After four months of active development, I am thrilled to announce the release of MikroORM 4.

When I started to work on v4, the goal was to make it a relatively small release, mainly to drop support for TypeScript 3.6 and Node.js 8, and to split the project into multiple packages, so we can have more fine grained control over the dependencies (mainly because of ts-morph having TS as a runtime dependency).

But what a major release would that be, without having a bunch of new features as well, right?

Image for post
Image for post
Photo by Ryoji Iwata on Unsplash

In case you don’t know…

If you never heard of MikroORM, it’s a TypeScript data-mapper ORM with Unit of Work and Identity Map. It supports MongoDB, MySQL, PostgreSQL and SQLite drivers currently. Key features of the ORM are:

Image for post
Image for post

You can read the full introductory article here or browse through the docs.

Quick summary of 3.x releases

Before I dive into all the things v4, let’s recap the major features that landed in 3.x releases:

Monorepo

The first major change I want to talk about is the split into multiple packages. As mentioned above, the biggest motivation for this change was to get rid of TS as a runtime dependency, when it is not needed. Another nice example is knex, which is used as a base layer for SQL driver, but has no meaning for mongodb users. Lastly, it turned out Highlight.js, that was used for query highlighting, is also quite fat and slow, so I ended up writing custom highlighters that are built for CLI and are (almost) dependency free.

In v4, there are 12 packages and 2 highlighters, you install only what you use, and you have control over what is needed in production and what is just a dev dependency. This is especially useful for serverless users, where cold start speeds matter.

It felt natural to offer some shortcuts on the EntityManager and EntityRepository level, so we now have flavours of those classes in place, that offer things like em.execute(sql) or em.aggregate(). To access those driver specific methods, be sure to use the classes from driver packages:

Database connectors like pg or sqlite3 are now dependencies of the driver packages (e. g. @mikro-orm/sqlite).

Filters

Probably the most interesting feature of v4 are filters, also known as association scopes. They allow you to define data visibility rules, both global and bound to entity. One common application of filters are soft deletes, or automatic tenant conditions.

Filters are applied to those methods of EntityManager: find(), findOne(), findAndCount(), findOneOrFail(), count(), nativeUpdate() and nativeDelete(). Filters can be parametric, the parameter can be also in form of callback (possibly async). You can also make the filter enabled by default.

Filter can be defined at the entity level, dynamically via EM (global filters) or in the ORM configuration.

Global Filters

We can also register filters dynamically via EntityManager API. We call such filters global. They are enabled by default (unless disabled via last parameter in addFilter() method), and applied to all entities. You can limit the global filter to only specified entities.

Filters as well as filter params set on the EM will be copied to all its forks.

EventSubscribers and flush events

As opposed to regular lifecycle hooks, we can now use EventSubscriber to hook to multiple entities or if you do not want to pollute the entity prototype. All methods are optional, if you omit the getSubscribedEntities() method, it means you are subscribing to all entities.

Flush events

There is a special kind of events executed during the commit phase (flush operation). They are executed before, during and after the flush, and they are not bound to any entity in particular.

  • beforeFlush is executed before change sets are computed, this is the only event where it is safe to persist new entities.
  • onFlush is executed after the change sets are computed.
  • afterFlush is executed as the last step just before the flush call resolves. it will be executed even if there are no changes to be flushed.

Flush event args will not contain any entity instance, as they are entity agnostic. They do contain additional reference to the UnitOfWork instance.

Following example demonstrates the hidden power of flush events — they allow to hook into the change set tracking, adjusting what will be persisted and how. Here we try to find a CREATE change set for entity FooBar, and if there is any, we automatically create a new FooBaz entity, connecting it to the FooBar one. This kind of operations was previously impossible, as in regular lifecycle hooks we can only adjust the entity that triggers the event.

Joined loading strategy

Loading of complex relations now support so called JOINED strategy. Its name is quite self-explanatory — instead of the default (SELECT_IN) strategy, it uses single SQL query and maps the result to multiple entities.

Single Table Inheritance

STI is an inheritance mapping strategy where all classes of a hierarchy are mapped to a single database table. In order to distinguish which row represents which type in the hierarchy a so-called discriminator column is used.

If no discriminator map is provided, it will be generated automatically.

Following example defines 3 entities — they will all be stored in a single database table called person, with a special column named type, that will be used behind the scenes to know what class should be used to represent given row/entity.

Embeddables

Embeddables are classes which are not entities themselves, but are embedded in entities and can also be queried. You’ll mostly want to use them to reduce duplication or separating concerns. Value objects such as date range or address are the primary use case for this feature.

Embeddables can only contain properties with basic @Property() mapping.

Following example will result in a single database table, where the address fields will be inlined (with prefix) to the user table.

Lazy scalar properties

In MikroORM 4, we can mark any property as lazy: true to omit it from the select clause. This can be handy for properties that are too large and you want to have them available only some times, like a full text of an article.

When we need such value, we can use populate parameter to load it as if it was a reference.

If the entity is already loaded and you need to populate a lazy scalar property, you might need to pass refresh: true in the FindOptions.

Computed Properties

Another small enhancement in entity definition is the @Formula() decorator. It can be used to map some SQL snippet to your entity. The SQL fragment can be as complex as you want and even include subselects.

Formulas will be added to the select clause automatically. In case you are facing problems with NonUniqueFieldNameException, you can define the formula as a callback that will receive the entity alias in the parameter.

Type-safe references

Next feature I would like to mention is rather hidden, and is a bit experimental. In MikroORM 4, all EntityManager and EntityRepository methods for querying entities (e.g. find()) will now return special Loaded type, where we automatically infer what relations are populated. It dynamically adds special get() method to both Reference and Collection instances, that you can use to ensure the relation is loaded on the type level.

QueryBuilder improvements

There have been quite a lot of small adjustments in QueryBuilder, to name a few things:

  • support for subqueries and qb.ref()
  • using sql snippets with qb.raw()
  • pagination support via subselects (QueryFlag.PAGINATE)
  • update & delete queries with auto-joining

Here are few examples of those features in action:

And many many more…

  • em.begin/commit/rollback() methods are back
  • using file globs for discovery (**/*.entity.ts)
  • custom driver exceptions (UniqueConstraintViolationException, …)
  • adding items to not-initialized collections
  • bulk deletes and other performance improvements
  • inference of custom repository type (EntityRepositoryType)
  • property serializers

See the changelog for full list of new features and fixes.

More example integrations

Upgrading

For smooth upgrading, read the full upgrading guide. Here are few notable breaking changes:

  • Default metadata provider is ReflectMetadataProvider, to use ts-morph, you need to install it from @mikro-orm/reflection and explicitly provide it in the ORM configuration. If you want to use ReflectMetadataProvider, be sure to see the list of its limitations.
  • TsMorphMetadataProvider now uses *.d.ts files in production mode, so be sure to enable them in your tsconfig.json.
  • @mikro-orm/core package is not dependent on knex, and therefore cannot provide methods like createQueryBuilder() — instead, those methods exist on SqlEntityManager. You can import it from the driver package, e.g. import { EntityManager } from '@mikro-orm/mysql;.
  • To use CLI, you need to install @mikro-orm/cli package.
  • When using folder based discovery, the options entitiesDirs and entitiesDirsTs are now removed in favour of entities and entitiesTs. You can now mix entity references with folders and file globs, negative globs are also supported.
  • For Nest.js users, there is a new @mikro-orm/nestjs package, which is a fork of the nestjs-mikro-orm module with changes needed for
    MikroORM 4.

What’s next?

Here are some features I’d like to work on in the near future:

  • Improved schema diffing
  • ts-morph reflection via custom TS compiler plugin
  • Query caching
  • MS SQL Server support

WDYT?

So thit is MikroORM 4, what do you think about it? What features or changes would you like to see next? Or what part of the documentation should be improved and how?


Like MikroORM? ⭐️ Star it on GitHub and share this article with your friends. If you want to support the project financially, you can do so via GitHub Sponsors.

DailyJS

JavaScript news and opinion.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store