Skip to main content
Version: Next

Upgrading from v5 to v6

Following sections describe (hopefully) all breaking changes, most of them might be not valid for you, like if you do not use custom NamingStrategy implementation, you do not care about the interface being changed.

Node 18.12+ required

Support for older node versions was dropped.

⚠️ TypeScript 5.0+ required

Support for older TypeScript versions was dropped.

Strict partial loading

The Loaded type is now improved to support the partial loading hints (fields option). When used, the returned type will only allow accessing selected properties. Primary keys are automatically selected.

// book is typed to `Selected<Book, 'author', 'title' | 'author.email'>`
const book = await em.findOneOrFail(Book, 1, {
fields: ['title', 'author.email'],
populate: ['author'],
});

const id = book.id; // ok, PK is selected automatically
const title = book.title; // ok, title is selected
const publisher = book.publisher; // fail, not selected
const author = book.author.id; // ok, PK is selected automatically
const email = book.author.email; // ok, selected
const name = book.author.name; // fail, not selected

Joined strategy changes

The joined strategy now supports populateWhere: 'all', which is the default behavior, and means "populate the full relations regardless of the where condition". This was previously not working with the joined strategy, as it was reusing the same join clauses as the where clause. In v6, the joined strategy will use a separate join branch for the populated relations. This aligns the behavior between the strategies.

The order by clause is shared for both the join branches and a new populateOrderBy option is added to allow control of the order of populated relations separately.

The default loading strategy for SQL drivers has been changed to the joined strategy.

To keep the old behaviour, you can override the default loading strategy in your ORM config.

Removed methods from EntityRepository

Following methods are no longer available on the EntityRepository instance.

  • persist
  • persistAndFlush
  • remove
  • removeAndFlush
  • flush

They were confusing as they gave a false sense of working with a scoped context (e.g. only with a User type), while in fact, they were only shortcuts for the same methods of underlying EntityManager. You should work with the EntityManager directly instead of using a repository when it comes to entity persistence, repositories should be treated as an extension point for custom logic (e.g. wrapping query builder usage).

-userRepository.persist(user);
-await userRepository.flush();
+em.persist(user);
+await em.flush();

Alternatively, you can use the repository.getEntityManager() method to access those methods directly on the EntityManager.

If you want to keep those methods on repository level, you can define custom base repository and use it globally:

import { EntityManager, EntityRepository, AnyEntity } from '@mikro-orm/mysql';

export class ExtendedEntityRepository<T extends object> extends EntityRepository<T> {

persist(entity: AnyEntity | AnyEntity[]): EntityManager {
return this.em.persist(entity);
}

async persistAndFlush(entity: AnyEntity | AnyEntity[]): Promise<void> {
await this.em.persistAndFlush(entity);
}

remove(entity: AnyEntity): EntityManager {
return this.em.remove(entity);
}

async removeAndFlush(entity: AnyEntity): Promise<void> {
await this.em.removeAndFlush(entity);
}

async flush(): Promise<void> {
return this.em.flush();
}

}

And specify it in the ORM config:

MikroORM.init({
entityRepository: ExtendedEntityRepository,
})

You might as well want to use the EntityRepositoryType symbol, possibly in a custom base entity.

Removed @Subscriber() decorator

Decorators generally work only if you import the file in your code, and as subscribers are often not imported anywhere in your app, the decorator often didn't work. In that case, people often left it there, but also added the subscriber to the ORM config, causing a duplication, as suddenly the decorator is executed and starts to work.

Use subscribers array in the ORM config, it now also accepts class reference, not just instances.

MikroORM.init({
subscribers: [MySubscriber], // or `new MySubscriber()`
})

Removal of static require calls

There were some places where we did a static require() call, e.g. when loading the driver implementation based on the type option. Those places were problematic for bundlers like webpack, as well as new school build systems like vite.

The type option is removed in favour of driver exports

Instead of specifying the type we now have several options:

  1. use defineConfig() helper imported from the driver package to create your ORM config:
    mikro-orm.config.ts
    import { defineConfig } from '@mikro-orm/mysql';

    export default defineConfig({ ... });
  2. use MikroORM.init() on class imported from the driver package:
    app.ts
    import { MikroORM } from '@mikro-orm/mysql';

    const orm = await MikroORM.init({ ... });
  3. specify the driver option:
    mikro-orm.config.ts
    import { MySqlDriver } from '@mikro-orm/mysql';

    export default {
    driver: MySqlDriver,
    ...
    };

The MIKRO_ORM_TYPE is still supported, but no longer does a static require of the driver class. Its usage is rather discouraged, and it might be removed in future versions too.

ORM extensions

Similarly, we had to get rid of the require() calls for extensions like Migrator, EntityGenerator and Seeder. Those need to be registered as extensions in your ORM config. SchemaGenerator extension is registered automatically.

This is required only for the shortcuts available on MikroORM object, e.g. orm.migrator.up(), alternatively you can instantiate the Migrator yourself explicitly.

mikro-orm.config.ts
import { defineConfig } from '@mikro-orm/mysql';
import { Migrator } from '@mikro-orm/migrations';
import { EntityGenerator } from '@mikro-orm/entity-generator';
import { SeedManager } from '@mikro-orm/seeder';

export default defineConfig({
dbName: 'test',
extensions: [Migrator, EntityGenerator, SeedManager], // those would have a static `register` method
});

MikroORM.init() no longer accepts a Configuration instance

The options always needs to be plain JS object now. This was always only an internal way, partially useful in tests, never meant to be a user API (while many people since the infamous Ben Awad video mistakenly complicated their typings with it).

MikroORM.init() no longer accepts second connect parameter

Use the connect option instead.

All drivers now re-export the @mikro-orm/core package

This means we no longer have to think about what package to use for imports, the driver package should be always preferred.

-import { Entity, PrimaryKey } from '@mikro-orm/core';
-import { MikroORM, EntityManager } from '@mikro-orm/mysql';
+import { Entity, PrimaryKey, MikroORM, EntityManager } from '@mikro-orm/mysql';

Removed MongoDriver methods

  • createCollections in favour of orm.schema.createSchema()
  • dropCollections in favour of orm.schema.dropSchema()
  • refreshCollections in favour of orm.schema.refreshDatabase()
  • ensureIndexes in favour of orm.schema.ensureIndexes()

Removed JavaScriptMetadataProvider

Use EntitySchema instead, for easy migration there is EntitySchema.fromMetadata() factory, but the interface is very similar on its own.

Removed PropertyOptions.customType in favour of just type

-@Property({ customType: new ArrayType() })
+@Property({ type: new ArrayType() })
foo: string[];

Removal of deprecated methods

  • em.nativeInsert() -> em.insert()
  • em.persistLater() -> em.persist()
  • em.removeLater() -> em.remove()
  • IdentifiedReference -> Ref
  • uow.getOriginalEntityData() without parameters
  • orm.schema.generate()

BaseEntity no longer has generic type arguments

Instead, the this type is used.

-class User extends BaseEntity<User> { ... }
+class User extends BaseEntity { ... }

wrap helper no longer accepts undefined on type level

The runtime implementation still checks for this case and returns the argument, but on type level this will fail to compile. It was never correct to pass in nullable values inside as it were not allowed in the return type.

Note that if you used it for converting entity instance to reference wrapper, the new ref() helper does a better job at that (and accepts nullable values).

Primary key inference

Some methods allowed you to pass in the primary key property via second generic type argument, this is now removed in favor of the automatic inference. To set the PK type explicitly, use the PrimaryKeyProp symbol.

PrimaryKeyType symbol has been removed, use PrimaryKeyProp instead if needed. Moreover, the value for composite PKs now has to be a tuple instead of a union to ensure we preserve the order of keys:

@Entity()
export class Foo {

@ManyToOne(() => Bar, { primary: true })
bar!: Bar;

@ManyToOne(() => Baz, { primary: true })
baz!: Baz;

- [PrimaryKeyType]?: [number, number];
- [PrimaryKeyProp]?: 'bar' | 'baz';
+ [PrimaryKeyProp]?: ['bar', 'baz'];

}

Removed BaseEntity.toJSON method

The signature became more complex on type level which made it harder to override, and as this was the only method meant for overriding, it should provide better experience when there won't be any.

The method was only forwarding the call to BaseEntity.toObject, so use that in the code instead.

The method is still present on the prototype as with any other entity, regardless of whether they extend from the BaseEntity.

Renames

  • PropertyOptions.onUpdateIntegrity -> PropertyOptions.updateRule
  • PropertyOptions.onDelete -> PropertyOptions.deleteRule
  • EntityProperty.reference -> EntityProperty.kind
  • ReferenceType -> ReferenceKind
  • PropertyOptions.wrappedReference -> PropertyOptions.ref
  • AssignOptions.mergeObjects -> AssignOptions.mergeObjectProperties
  • EntityOptions.customRepository -> EntityOptions.repository
  • Options.cache -> Options.metadataCache
  • UnitOfWork.registerManaged -> UnitOfWork.register
  • baseDir -> path option in EntityGenerator.generate()
  • MIKRO_ORM_CLI env var -> MIKRO_ORM_CLI_CONFIG
  • InitOptions -> InitCollectionOptions

Removed dependency on faker in seeder package

Faker is rather fat library that can result in perf degradation just by importing it, and since we were not working with the library anyhow, there is no reason to keep it in the dependencies. Users who want to use faker can just install it and use it directly, having the faker import under their own control.

-import { Factory, Faker } from '@mikro-orm/seeder';
+import { Factory } from '@mikro-orm/seeder';
+import { faker } from '@faker-js/faker/locale/en';

export class ProjectFactory extends Factory<Project> {

model = Project;

- definition(faker: Faker): Partial<Project> {
+ definition(): Partial<Project> {
return {
name: faker.company.name(),
};
}

}

Removed RequestContext.createAsync

Use RequestContext.create instead, it can be awaited now.

-const ret = await RequestContext.createAsync(em, async () => { ... });
+const ret = await RequestContext.create(em, async () => { ... });

Renamed @UseRequestContext()

The decorator was renamed to @CreateRequestContext() to make it clear it always creates new context, and a new @EnsureRequestContext() decorator was added that will reuse existing contexts if available.

Raw SQL fragments now require raw helper

The raw SQL fragments used to be detected automatically, which wasn't very precise. In v6, a new raw static helper is introduced to deal with this:

const users = await em.find(User, {
- [expr('lower(email)')]: 'foo@bar.baz',
+ [raw('lower(email)')]: 'foo@bar.baz',
});

The previous em.raw() and qb.raw() helpers are now removed in favor of this new static raw helper. Similarly, the expr helper is also removed in favor of it.

Note that this new helper can be also used to do atomic updates via flush:

const ref = em.getReference(User, 1);
ref.age = raw(`age * 2`);
await em.flush();
console.log(ref.age); // real value is available after flush

Alternatively, you can use the new sql tagged template function:

ref.age = sql`age * 2`;

This works on query keys as well as parameters, and is required for any SQL fragments.

Read more about this in Using raw SQL query fragments section.

Removed qb.ref()

Removed in favour of sql.ref().

Changed default PostgreSQL Date mapping precision

Previously, all drivers defaulted the Date type mapping to a timestamp with 0 precision (so seconds). This is discouraged in PostgreSQL, and is no longer valid - the default mapping without the length property being explicitly set is now timestamptz, which stores microsecond precision, so equivalent to timestamptz(6).

To revert back to the v5 behavior, you can either set the columnType: 'timestamptz(0)', or use length: 0:

@Property({ length: 0 })

Metadata CacheAdapter requires sync API

To allow working with cache inside MikroORM.initSync, the metadata cache now enforces sync API. You should usually depend on the file-based cache for the metadata, which now uses sync methods to work with the file system.

Implicit serialization changes

Implicit serialization, so calling toObject() or toJSON() on the entity, as opposed to explicitly using the serialize() helper, now works entirely based on populate hints. This means that, unless you explicitly marked some entity as populated via wrap(entity).populated(), it will be part of the serialized form only if it was part of the populate hint:

// let's say both Author and Book entity has a m:1 relation to Publisher entity
// we only populate the publisher relation of the Book entity
const user = await em.findOneOrFail(Author, 1, {
populate: ['books.publisher'],
});

const dto = wrap(user).toObject();
console.log(dto.publisher); // only the FK, e.g. `123`
console.log(dto.books[0].publisher); // populated, e.g. `{ id: 123, name: '...' }`

Moreover, the implicit serialization now respects the partial loading hints too. Previously, all loaded properties were serialized, partial loading worked only on the database query level. Since v6, we also prune the data on runtime. This means that unless the property is part of the partial loading hint (fields option), it won't be part of the DTO - only exception is the primary key, you can optionally hide it via hidden: true in the property options. Main difference here will be the foreign keys, those are often automatically selected as they are needed to build the entity graph, but will no longer be part of the DTO.

const user = await em.findOneOrFail(Author, 1, {
fields: ['books.publisher.name'],
});

const dto = wrap(user).toObject();
// only the publisher's name will be available, previously there would be also `book.author`
// `{ id: 1, books: [{ id: 2, publisher: { id: 3, name: '...' } }] }`

This also works for embeddables, including nesting and object mode.

Changes in Date property mapping

Previously, mapping of datetime columns to JS Date objects was dependent on the driver, while SQLite didn't have this out of box support and required manual conversion on various places. All drivers now have disabled Date conversion and this is now handled explicitly, in the same way for all of them.

Moreover, the date type was previously seen as a datetime, while now only Date (with uppercase D) will be considered as datetime, while date is just a date.

Lastly, DateType (used for mapping date column type, not a datetime) no longer maps to a Date objects (maps to a string instead).

Native BigInt support

The default mapping of bigint columns is now using the native JavaScript BigInt, and is configurable, so it can also map to numbers or strings:

@PrimaryKey()
id0: bigint; // type is inferred

@PrimaryKey({ type: new BigIntType('bigint') })
id1: bigint; // same as `id0`

@PrimaryKey({ type: new BigIntType('string') })
id2: string;

@PrimaryKey({ type: new BigIntType('number') })
id3: number;

Join condition alias

Additional join conditions used to be implicitly aliased to the root entity, now they are aliased to the joined entity instead. If you are using explicit aliases in the join conditions, nothing changes.

// the `name` used to resolve to `b.name`, now it will resolve to `a.name` instead
qb.join('b.author', 'a', { name: 'foo' });

Embedded properties respect NamingStrategy

This is breaking mainly for SQL drivers, where the default naming strategy is underscoring, and will now apply to the embedded properties too. You can restore to the old behaviour by implementing custom naming strategy, overriding the propertyToColumnName method. It now has a second boolean parameter to indicate if the property is defined inside a JSON object context.

import { UnderscoreNamingStrategy } from '@mikro-orm/core';

class CustomNamingStrategy extends UnderscoreNamingStrategy {

propertyToColumnName(propertyName: string, object?: boolean): string {
if (object) {
return propertyName;
}

return super.propertyToColumnName(propertyName, object);
}

}

Iterating not initialized Collection throws

When you try to iterate a collection instance, it will now check if its initialized, and throw otherwise.

const author = await em.findOne(User, 1);

// this will throw as the books collection is not initialized
for (const book of author.books) {
// ...
}

Type-safe populate: ['*'] and removed populate: true support

When populating all relations, the Loaded type will now respect * in the populate hint. The old boolean variant is now removed in favor of new populate: ['*'].

This also applies to the serialize() helper and its populate parameter.

const users = await em.find(User, {}, {
- populate: true,
+ populate: ['*'],
});

populate: false is still allowed and serves as a way to disable eager loaded properties.

em.populate() returns just the entity when called on a single entity

em.populate() now returns what you feed in—when you call it with a single entity, you get single entity back, when you call it with an array of entities, you get an array back.

This has been the case initially, but it was problematic to type the method strictly, so it was changed to always an array in v5. This is now resolved in v6, so we can have the previous behavior back, but type-safe.

-const [author] = await em.populate(author, ['books']);
+const author = await em.populate(author, ['books']);

Duplicate field names are now validated

When you use the same fieldName for two properties in one entity, error will be thrown:

@Entity()
class User {

@PrimaryKey()
id!: number;

@Property({ name: 'custom_name' })
name!: number;

@Property({ name: 'custom_name' })
age!: number;

}

This does not apply to virtual properties:

@Entity()
class User {

@PrimaryKey()
id!: number;

@ManyToOne(() => User, { name: 'parent_id' })
parent!: User;

@Property({ name: 'parent_id', persist: false })
parentId!: number;

}

This validation can be disabled via discovery.checkDuplicateFieldNames ORM config option.

Reference.load(prop: keyof T) signature removed

The Reference.load() method allowed two signatures, one to ensure the entity is loaded, and another to get a property value in one step. The latter is now removed in favor of a new method called Reference.loadProperty(prop):

-const email = book.author.load('email');
+const email = book.author.loadProperty('email');

Reference.set() is private

Reference wrapper holds an identity—this means its instance is bound to the underlying entity. This imposes a problem when you try to change the wrapped entity, as the same instance of the Reference wrapper can be present on other places in the entity graph. For this reason, the set method is no longer public, as replacing the reference instance should be always preferred.

-book.author.set(other);
+book.author = ref(other);

Reference.load() can return null

Reference.load() (and other methods that are using WrappedEntity.init() under the hood) can now return null when the target entity is not found instead of resolving to unloaded entity. This can happen either because it was removed in the meantime, or it is not compatible with the currently enabled filters. A new method called loadOrFail() is added to the Reference class which always returns a value or throws otherwise, just like em.findOneOrFail().

-const publisher = await book.publisher.load();
+const publisher = await book.publisher.loadOrFail();

em.insert() respects required properties

em.insert() will now require you to pass all non-optional properties just like em.create() already did. Some properties might be defined as required for TS, but we have a default value for them (either runtime, or database one) - for such we can use OptionalProps symbol (or the new Opt type) to specify which properties should be considered as optional.

.env files are no longer automatically loaded

Previously, if there was a .env file in your root directory, it was automatically loaded. Now instead of loading, it is only checked for the ORM env vars (those prefixed with MIKRO_ORM_) and all the others are ignored. If you want to access all your env vars defined in the .env file, call import 'dotenv/config' yourself in your app (or possibly in your ORM config file).