Modeling Entity Relationships
There are 4 types of entity relationships in MikroORM:
- ManyToOne
- OneToMany
- OneToOne
- ManyToMany
Relations can be unidirectional and bidirectional. Unidirectional are defined only on one side (the owning side). Bidirectional are defined on both sides, while one is owning side (where references are store), marked by inversedBy attribute pointing to the inverse side. On the inversed side we define it with mappedBy attribute pointing back to the owner:
When modeling bidirectional relationship, you can also omit the
inversedByattribute, definingmappedByon the inverse side is enough as it will be auto-wired.
ManyToOne
Many instances of the current Entity refer to One instance of the referred Entity.
There are multiple ways how to define the relationship, all of the following is equivalent:
- reflect-metadata
- ts-morph
- defineEntity
- EntitySchema
@Entity()
export class Book {
@ManyToOne(() => Author) // you need to specify type via callback
author1!: Author;
@ManyToOne('Author') // or as a string
author2!: Author;
@ManyToOne({ entity: () => Author }) // or use options object
author3!: Author;
}
@Entity()
export class Book {
@ManyToOne() // plain decorator is enough, type will be sniffed via ts-morph!
author!: Author;
}
import { defineEntity, InferEntity } from '@mikro-orm/core';
export const Book = defineEntity({
name: 'Book',
properties: p => ({
id: p.integer().primary(),
author: () => p.manyToOne(Author),
}),
});
export interface IBook extends InferEntity<typeof Book> {}
export interface IBook {
id: number;
author: Author;
}
export const Book = new EntitySchema<IBook>({
name: 'Book',
properties: {
id: { type: 'number', primary: true },
author: { kind: 'm:1', entity: 'Author' },
},
});
You can also specify how operations on given entity should cascade to the referred entity.
OneToMany
One instance of the current Entity has Many instances (references) to the referred Entity.
Again, all of the following is equivalent:
- reflect-metadata
- ts-morph
- defineEntity
- EntitySchema
@Entity()
export class Author {
@OneToMany(() => Book, book => book.author)
books1 = new Collection<Book>(this);
@OneToMany('Book', 'author')
books2 = new Collection<Book>(this);
@OneToMany({ mappedBy: book => book.author }) // referenced entity type can be sniffed too
books3 = new Collection<Book>(this);
@OneToMany({ entity: () => Book, mappedBy: 'author', orphanRemoval: true })
books4 = new Collection<Book>(this);
}
@Entity()
export class Author {
@OneToMany(() => Book, book => book.author)
books = new Collection<Book>(this);
}
import { defineEntity, InferEntity } from '@mikro-orm/core';
export const Author = defineEntity({
name: 'Author',
properties: p => ({
id: p.integer().primary(),
books: () => p.oneToMany(Book).mappedBy('author'),
}),
});
export interface IAuthor extends InferEntity<typeof Author> {}
export interface IAuthor {
id: number;
books: Collection<Book>;
}
export const Author = new EntitySchema<IAuthor>({
name: 'Author',
properties: {
id: { type: 'number', primary: true },
books: { kind: '1:m', entity: () => Book, mappedBy: 'author' },
},
});
As you can see, OneToMany is the inverse side of ManyToOne (which is the owning side). More about how collections work can be found on collections page.
You can also specify how operations on given entity should cascade to the referred entities. There is also more aggressive remove mode called Orphan Removal (books4 example).
OneToOne
One instance of the current Entity refers to One instance of the referred Entity.
This is a variant of ManyToOne, where there is always just one entity on both sides. This means that the foreign key column is also unique.
Owning Side
- reflect-metadata
- ts-morph
- defineEntity
- EntitySchema
@Entity()
export class User {
// when none of `owner/inversedBy/mappedBy` is provided, it will be considered owning side
@OneToOne()
bestFriend1!: User;
// side with `inversedBy` is the owning one, to define inverse side use `mappedBy`
@OneToOne({ inversedBy: 'bestFriend1' })
bestFriend2!: User;
// when defining it like this, you need to specifically mark the owning side with `owner: true`
@OneToOne(() => User, user => user.bestFriend2, { owner: true })
bestFriend3!: User;
}
@Entity()
export class User {
// when none of `owner/inversedBy/mappedBy` is provided, it will be considered owning side
@OneToOne()
bestFriend1!: User;
// side with `inversedBy` is the owning one, to define inverse side use `mappedBy`
@OneToOne({ inversedBy: 'bestFriend1' })
bestFriend2!: User;
// when defining it like this, you need to specifically mark the owning side with `owner: true`
@OneToOne(() => User, user => user.bestFriend2, { owner: true })
bestFriend3!: User;
}
import { defineEntity, InferEntity } from '@mikro-orm/core';
export const User = defineEntity({
name: 'User',
properties: p => ({
id: p.integer().primary(),
// when none of `owner/inversedBy/mappedBy` is provided, it will be considered owning side
bestFriend1: () => p.oneToOne(User),
// side with `inversedBy` is the owning one, to define inverse side use `mappedBy`
bestFriend2: () => p.oneToOne(User).inversedBy('bestFriend1'),
// you need to specifically mark the owning side with `owner: true`
bestFriend3: () => p.oneToOne(User).owner(),
}),
});
export interface IUser extends InferEntity<typeof User> {}
export interface IUser {
id: number;
bestFriend1: User;
bestFriend2: User;
bestFriend3: User;
}
export const User = new EntitySchema<IUser>({
name: 'User',
properties: {
id: { type: 'number', primary: true },
// when none of `owner/inversedBy/mappedBy` is provided, it will be considered owning side
bestFriend1: { kind: '1:1', entity: 'User' },
// side with `inversedBy` is the owning one, to define inverse side use `mappedBy`
bestFriend2: { kind: '1:1', entity: 'User', inversedBy: 'bestFriend1' },
// you need to specifically mark the owning side with `owner: true`
bestFriend3: { kind: '1:1', entity: 'User', owner: true },
},
});
Inverse Side
- reflect-metadata
- ts-morph
- defineEntity
- EntitySchema
@Entity()
export class User {
@OneToOne({ mappedBy: 'bestFriend1', orphanRemoval: true })
bestFriend1!: User;
@OneToOne(() => User, user => user.bestFriend2, { orphanRemoval: true })
bestFriend2!: User;
}
@Entity()
export class User {
@OneToOne({ mappedBy: 'bestFriend1', orphanRemoval: true })
bestFriend1!: User;
@OneToOne(() => User, user => user.bestFriend2, { orphanRemoval: true })
bestFriend2!: User;
}
import { defineEntity, InferEntity } from '@mikro-orm/core';
export const User = defineEntity({
name: 'User',
properties: p => ({
id: p.integer().primary(),
bestFriend1: () => p.oneToOne(User).mappedBy('bestFriend1').orphanRemoval(),
bestFriend2: () => p.oneToOne(User).mappedBy('bestFriend2').orphanRemoval(),
}),
});
export interface IUser extends InferEntity<typeof User> {}
export interface IUser {
id: number;
bestFriend1: User;
bestFriend2: User;
}
export const User = new EntitySchema<IUser>({
name: 'User',
properties: {
id: { type: 'number', primary: true },
bestFriend1: { kind: '1:1', entity: 'User', mappedBy: 'bestFriend1', orphanRemoval: true },
bestFriend2: { kind: '1:1', entity: 'User', mappedBy: 'bestFriend2', orphanRemoval: true },
},
});
As you can see, relationships can be also self-referencing (all of them. OneToOne also supports Orphan Removal).
ManyToMany
Many instances of the current Entity refers to Many instances of the referred Entity.
Here are examples of how you can define ManyToMany relationship:
Owning Side
- reflect-metadata
- ts-morph
- defineEntity
- EntitySchema
@Entity()
export class Book {
@ManyToMany(() => BookTag, 'books')
tags1 = new Collection<BookTag>(this);
@ManyToMany(() => BookTag, 'books', { owner: true })
tags2 = new Collection<BookTag>(this);
@ManyToMany(() => BookTag, 'books', { owner: true })
tags3 = new Collection<BookTag>(this);
// to define uni-directional many to many
@ManyToMany(() => Author)
friends: Collection<Author> = new Collection<Author>(this);
}
@Entity()
export class Book {
@ManyToMany()
tags = new Collection<BookTag>(this);
// to define uni-directional many to many
@ManyToMany()
friends = new Collection<Author>(this);
}
import { defineEntity, InferEntity } from '@mikro-orm/core';
export const Book = defineEntity({
name: 'Book',
properties: p => ({
id: p.integer().primary(),
// when none of `owner/inversedBy/mappedBy` is provided, it will be considered owning side
tags: () => p.manyToMany(BookTag),
// to define uni-directional many to many, simply omit `mappedBy`
friends: () => p.manyToMany(Author),
}),
});
export interface IBook extends InferEntity<typeof Book> {}
export interface IBook {
id: number;
tags: Collection<BookTag>;
friends: Collection<Author>;
}
export const Book = new EntitySchema<IBook>({
name: 'Book',
properties: {
id: { type: 'number', primary: true },
tags: { kind: 'm:n', entity: 'BookTag' },
// to define uni-directional many to many, simply omit `mappedBy`
friends: { kind: 'm:n', entity: 'Author' },
},
});
Inverse Side
- reflect-metadata
- ts-morph
- defineEntity
- EntitySchema
@Entity()
export class BookTag {
// inverse side has to point to the owning side via `mappedBy` attribute/parameter
@ManyToMany(() => Book, book => book.tags)
books = new Collection<Book>(this);
}
@Entity()
export class BookTag {
// inverse side has to point to the owning side via `mappedBy` attribute/parameter
@ManyToMany(() => Book, book => book.tags)
books = new Collection<Book>(this);
}
import { defineEntity, InferEntity } from '@mikro-orm/core';
export const BookTag = defineEntity({
name: 'BookTag',
properties: p => ({
id: p.integer().primary(),
// inverse side has to point to the owning side via `mappedBy`
books: () => p.manyToMany(Book).mappedBy('tags'),
}),
});
export interface IBookTag extends InferEntity<typeof BookTag> {}
export interface IBookTag {
id: number;
books: Collection<Book>;
}
export const BookTag = new EntitySchema<IBookTag>({
name: 'BookTag',
properties: {
id: { type: 'number', primary: true },
// inverse side has to point to the owning side via `mappedBy`
books: { kind: 'm:n', entity: () => Book, mappedBy: 'tags' },
},
});
Again, more information about how collections work can be found on collections page.
Relations in ESM projects
If you use ESM in your TypeScript project with reflect-metadata, you might fall into issues with circular dependencies, seeing errors like this:
ReferenceError: Cannot access 'Author' before initialization
To get around them, use the Rel mapped type. It is an identity type, which disables the problematic inference from reflect-metadata, that causes ESM projects to fail.
- reflect-metadata
- ts-morph
- defineEntity
- EntitySchema
import { Rel } from '@mikro-orm/core';
@Entity()
export class Book {
@ManyToOne(() => Author)
author!: Rel<Author>;
}
import { Rel } from '@mikro-orm/core';
@Entity()
export class Book {
@ManyToOne()
author!: Rel<Author>;
}
import { defineEntity, InferEntity } from '@mikro-orm/core';
export const Book = defineEntity({
name: 'Book',
properties: p => ({
id: p.integer().primary(),
author: () => p.manyToOne(Author),
}),
});
export interface IBook extends InferEntity<typeof Book> {}
With
defineEntity, circular dependencies are handled automatically, no need forRelwrapper.
export interface IBook {
id: number;
author: Author;
}
export const Book = new EntitySchema<IBook>({
name: 'Book',
properties: {
id: { type: 'number', primary: true },
author: { kind: 'm:1', entity: () => Author },
},
});
With
EntitySchema, circular dependencies are handled automatically via lazy references.
Custom foreign key constraint name
If you need a greater control on the underlying SQL schema, you can provide a custom name for the foreign key constraint of your relationship on the owning side.
This name overrides the one automatically generated by the current NamingStrategy.
- reflect-metadata
- ts-morph
- defineEntity
- EntitySchema
@Entity()
export class Book {
@ManyToOne(() => Author, { foreignKeyName: 'my_custom_name' })
author: Author;
}
@Entity()
export class Book {
@ManyToOne({ foreignKeyName: 'my_custom_name' })
author: Author;
}
import { defineEntity, InferEntity } from '@mikro-orm/core';
export const Book = defineEntity({
name: 'Book',
properties: p => ({
id: p.integer().primary(),
author: () => p.manyToOne(Author).foreignKeyName('my_custom_name'),
}),
});
export interface IBook extends InferEntity<typeof Book> {}
export interface IBook {
id: number;
author: Author;
}
export const Book = new EntitySchema<IBook>({
name: 'Book',
properties: {
id: { type: 'number', primary: true },
author: { kind: 'm:1', entity: 'Author', foreignKeyName: 'my_custom_name' },
},
});
Disabling foreign key constraint creation
If you need to disable the creation of the underlying SQL foreign key constraint for a specific relation, you can set createForeignKeyConstraint to false on the relation on the owning side.
- reflect-metadata
- ts-morph
- defineEntity
- EntitySchema
@Entity()
export class Book {
@ManyToOne(() => Author, { createForeignKeyConstraint: false })
author: Author;
}
@Entity()
export class Book {
@ManyToOne({ createForeignKeyConstraint: false })
author: Author;
}
import { defineEntity, InferEntity } from '@mikro-orm/core';
export const Book = defineEntity({
name: 'Book',
properties: p => ({
id: p.integer().primary(),
author: () => p.manyToOne(Author).createForeignKeyConstraint(false),
}),
});
export interface IBook extends InferEntity<typeof Book> {}
export interface IBook {
id: number;
author: Author;
}
export const Book = new EntitySchema<IBook>({
name: 'Book',
properties: {
id: { type: 'number', primary: true },
author: { kind: 'm:1', entity: 'Author', createForeignKeyConstraint: false },
},
});
Note that if you globally disable the creation of all foreign key constraints by setting createForeignKeyConstraints to false, then no foreign key constraint is created whatsoever on any relation.
const orm = await MikroORM.init({
...
schemaGenerator: {
createForeignKeyConstraints: false,
},
});