Skip to main content
Version: 7.0 (next)

Defining Entities via EntitySchema

With EntitySchema helper you define the schema programmatically.

./entities/Book.ts
export interface Book extends CustomBaseEntity {
title: string;
author: Author;
publisher: Publisher;
tags: Collection<BookTag>;
}

// The second type argument is optional, and should be used only with custom
// base entities, not with the `BaseEntity` class exported from `@mikro-orm/core`.
export const BookSchema = new EntitySchema<Book, CustomBaseEntity>({
// name should be used only with interfaces
name: 'Book',
// if we use actual class, we need this instead:
// class: Book,
extends: CustomBaseEntitySchema, // only if we extend custom base entity
properties: {
title: { type: 'string' },
author: { kind: 'm:1', entity: () => Author, inversedBy: 'books' },
publisher: { kind: 'm:1', entity: () => Publisher, inversedBy: 'books' },
tags: { kind: 'm:n', entity: () => BookTag, inversedBy: 'books', fixedOrder: true },
},
});

When creating new entity instances, you will need to use em.create() method that will create instance of internally created class.

// instance of internal Book class
const book = em.create(BookSchema, {});
await em.flush();

Alternatively, you can use EntitySchema.new method, which will call the internal class constructor directly.

// instance of internal Book class
const book = BookSchema.new();
await em.persist(book).flush(); // `em.persist()` required as opposed to em.create()

Using this approach, metadata caching is automatically disabled as it is unnecessary.

Using custom entity classes

You can optionally use custom class for entity instances.

./entities/Author.ts
export class Author extends CustomBaseEntity {
name: string;
email: string;
age?: number;
termsAccepted?: boolean;
identities?: string[];
born?: string;
books = new Collection<Book>(this);
favouriteBook?: Book;
version?: number;

constructor(name: string, email: string) {
super();
this.name = name;
this.email = email;
}
}

export const AuthorSchema = new EntitySchema({
class: Author,
extends: CustomBaseEntitySchema,
properties: {
name: { type: 'string' },
email: { type: 'string', unique: true },
age: { type: 'number', nullable: true },
termsAccepted: { type: 'boolean', default: 0, onCreate: () => false },
identities: { type: 'string[]', nullable: true },
born: { type: DateType, nullable: true, length: 3 },
books: { kind: '1:m', entity: () => 'Book', mappedBy: book => book.author },
favouriteBook: { kind: 'm:1', type: 'Book' },
version: { type: 'number', persist: false },
},
});

Then you can use the entity class as usual:

const author = new Author('name', 'email');
await em.persist(author).flush();

Using custom base entity

Do not forget that base entities needs to be discovered just like normal entities.

./entities/BaseEntity.ts
export interface CustomBaseEntity {
id: number;
createdAt: Date;
updatedAt: Date;
}

export const schema = new EntitySchema<CustomBaseEntity>({
name: 'CustomBaseEntity',
abstract: true,
properties: {
id: { type: 'number', primary: true },
createdAt: { type: 'Date', onCreate: () => new Date(), nullable: true },
updatedAt: { type: 'Date', onCreate: () => new Date(), onUpdate: () => new Date(), nullable: true },
},
});

Configuration Reference

The parameter of EntitySchema requires to provide either name or class parameters. When using class, extends will be automatically inferred. You can optionally pass these additional parameters:

name: string;
class: Constructor<T>;
extends: string;
tableName: string; // alias for `collection: string`
properties: { [K in keyof T & string]: EntityProperty<T[K]> };
indexes: { properties: string | string[]; name?: string; type?: string }[];
uniques: { properties: string | string[]; name?: string }[];
repository: () => Constructor<EntityRepository<T>>;
hooks: Partial<Record<keyof typeof EventType, ((string & keyof T) | NonNullable<EventSubscriber[keyof EventSubscriber]>)[]>>;
abstract: boolean;

Every property then needs to contain a type specification - one of type or entity options. Here are some examples of various property types:

export enum MyEnum {
LOCAL = 'local',
GLOBAL = 'global',
}

export const schema = new EntitySchema<FooBar>({
name: 'FooBar',
tableName: 'tbl_foo_bar',
indexes: [{ name: 'idx1', properties: 'name' }],
uniques: [{ name: 'unq1', properties: ['name', 'email'] }],
repository: () => FooBarRepository,
properties: {
id: { type: 'number', primary: true },
name: { type: 'string' },
baz: { kind: '1:1', entity: () => FooBaz, orphanRemoval: true, nullable: true },
fooBar: { kind: '1:1', entity: () => FooBar, nullable: true },
publisher: { kind: 'm:1', entity: () => Publisher, inversedBy: 'books' },
books: { kind: '1:m', entity: () => 'Book', mappedBy: book => book.author },
tags: { kind: 'm:n', entity: () => BookTag, inversedBy: 'books', fixedOrder: true },
version: { type: 'Date', version: true, length: 0 },
type: { enum: true, items: () => MyEnum, default: MyEnum.LOCAL },
},
});

As a value for type you can also use one of String/Number/Boolean/Date.

defineEntity

defineEntity is built on top of EntitySchema, leveraging TypeScript's type inference capabilities to generate entity types. This reduces the amount of code while providing robust type safety and null safety.

import { type InferEntity, defineEntity, p } from '@mikro-orm/core';

// We use `p` as a shortcut for `defineEntity.properties`
const p = defineEntity.properties;

// It is recommended to use composition over inheritance when using `defineEntity`
export const baseProperties = {
id: p.integer().primary(),
createdAt: p.datetime().onCreate(() => new Date()),
updatedAt: p.datetime()
.onCreate(() => new Date())
.onUpdate(() => new Date()),
};

// Book is an instance of `EntitySchema`
export const Book = defineEntity({
name: 'Book',
properties: {
...baseProperties,
title: p.string(),
author: () => p.manyToOne(Author).inversedBy('books'),
publisher: () => p.oneToOne(Publisher).inversedBy('book'),
tags: () => p.manyToMany(BookTag).inversedBy('books').fixedOrder(),
},
});

// We can use `InferEntity` to infer the type of an entity
export type IBook = InferEntity<typeof Book>;

defineEntity.properties provides all MikroORM built-in types. To use custom types, you can also use p.type().

const p = defineEntity.properties;

const properties = {
string: p.string(),
float: p.float(),
boolean: p.boolean(),
json: p.json<{ foo: string; bar: number }>().nullable(),
stringArray: p.type(ArrayType<string>).nullable(),
numericArray: p.type(new ArrayType(i => +i)).nullable(),
point: p.type(PointType).nullable(),
};

Adding custom methods

When using defineEntity without a class, an internal entity class is auto-generated. You can extend this class to add custom methods without having to redeclare all properties:

import { defineEntity, p } from '@mikro-orm/core';

// Define the schema - this auto-generates an internal class
const UserSchema = defineEntity({
name: 'User',
tableName: 'users',
properties: {
id: p.integer().primary(),
firstName: p.string(),
lastName: p.string(),
},
});

// Extend the auto-generated class to add methods
class User extends UserSchema.class {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}

// Register the custom class with the schema
UserSchema.setClass(User);

Important: setClass() must be called before the ORM discovery process runs (i.e., before MikroORM.init()). Make sure to call it at module load time, right after defining the extended class.

After calling setClass(), the custom class is registered and will be used for all entity instances. This approach provides the best of both worlds:

  • No property duplication: Properties are defined once in the schema
  • Full type inference: The extended class inherits all property types
  • Custom methods: Add domain logic directly on entity instances

You can then use the entity normally:

const user = em.create(User, { firstName: 'John', lastName: 'Doe' });
console.log(user.fullName()); // "John Doe"

MongoDB example

export const BookTag = defineEntity({
name: 'BookTag',
properties: {
_id: p.type(ObjectId).primary(),
id: p.string().serializedPrimaryKey(),
name: p.string(),
books: () => p.manyToMany(Book).mappedBy('tags'),
},
});

export type IBookTag = InferEntity<typeof BookTag>;

Hooks example

Entity hooks can be defined either as a property name, or as a function. When defined as a function, the this argument will be the entity instance. Arrow functions can be used if desired, and the entity will be available at args.entity. See Events and Lifecycle Hooks section for more details on EventArgs.

// Defined outside, this available via args.
const beforeUpdate = (args: EventArgs) => args.entity.version++;

export const BookTag = defineEntity({
name: 'BookTag',
properties: {
_id: p.type(ObjectId).primary(),
id: p.string().serializedPrimaryKey(),
name: p.string(),
books: () => p.manyToMany(Book).mappedBy('tags'),
},
hooks: {
beforeUpdate: [beforeUpdate],
},
});

export type IBookTag = InferEntity<typeof BookTag>;