Separating Concerns using Embeddables
Support for embeddables was added in version 4.0
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 needs to be discovered just like regular entities, don't forget to add them to the list of entities when initializing the ORM.
Embeddables can contain properties with basic @Property() mapping, nested @Embedded() properties or arrays of @Embedded() properties. From version 5.0 you can also use @ManyToOne() properties.
For the purposes of this tutorial, let's assume that you have a User class in your application, and you would like to store an address in the User class. The Address class will be modeled as an embeddable instead of simply adding the respective columns to the User class.
- defineEntity + class
- defineEntity
- reflect-metadata
- ts-morph
import { defineEntity, p } from '@mikro-orm/core';
const UserSchema = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
address: () => p.embedded(Address),
},
});
const AddressSchema = defineEntity({
name: 'Address',
embeddable: true,
properties: {
street: p.string(),
postalCode: p.string(),
city: p.string(),
country: p.string(),
},
});
export class Address extends AddressSchema.class {}
AddressSchema.setClass(Address);
export class User extends UserSchema.class {}
UserSchema.setClass(User);
import { type InferEntity, defineEntity, p } from '@mikro-orm/core';
export const User = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
address: () => p.embedded(Address),
},
});
export type IUser = InferEntity<typeof User>;
export const Address = defineEntity({
name: 'Address',
embeddable: true,
properties: {
street: p.string(),
postalCode: p.string(),
city: p.string(),
country: p.string(),
},
});
export type IAddress = InferEntity<typeof Address>;
import { Embeddable, Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Embeddable()
export class Address {
@Property()
street!: string;
@Property()
postalCode!: string;
@Property()
city!: string;
@Property()
country!: string;
}
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Embedded(() => Address)
address!: Address;
}
import { Embeddable, Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Embeddable()
export class Address {
@Property()
street!: string;
@Property()
postalCode!: string;
@Property()
city!: string;
@Property()
country!: string;
}
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Embedded()
address!: Address;
}
When using ReflectMetadataProvider, you might need to provide the class in decorator options:
@Embedded(() => Address)or@Embedded({ entity: () => Address }).
In terms of your database schema, MikroORM will automatically inline all columns from the Address class into the table of the User class, just as if you had declared them directly there.
Initializing embeddables
In case all fields in the embeddable are nullable, you might want to initialize the embeddable, to avoid getting a null value instead of the embedded object.
- defineEntity + class
- defineEntity
- reflect-metadata
- ts-morph
const UserSchema = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
address: () => p.embedded(Address)
.onCreate(() => new Address()),
},
});
export class User extends UserSchema.class {}
UserSchema.setClass(User);
const User = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
address: () => p.embedded(Address)
.onCreate(() => new Address()),
},
});
@Embedded(() => Address)
address = new Address();
@Embedded()
address = new Address();
Column Prefixing
By default, MikroORM names your columns by prefixing them, using the value object name.
Following the example above, your columns would be named as address_street, address_postal_code...
You can change this behaviour to meet your needs by changing the prefix attribute in the @Embedded() notation.
The following example shows you how to set your prefix to myPrefix_:
- defineEntity + class
- defineEntity
- reflect-metadata
- ts-morph
const UserSchema = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
address: () => p.embedded(Address).prefix('myPrefix_')
},
});
export class User extends UserSchema.class {}
UserSchema.setClass(User);
const User = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
address: () => p.embedded(Address).prefix('myPrefix_')
},
});
@Entity()
export class User {
@Embedded(() => Address, { prefix: 'myPrefix_' })
address!: Address;
}
@Entity()
export class User {
@Embedded({ prefix: 'myPrefix_' })
address!: Address;
}
You can also decide more precisely how the column name is determined with an explicit prefix. With the example below:
relativemode (default) concatenates the prefix with its parent's prefix (if any), naming themcontact_addr2_city,contact_addr2_street, ...absolutemode sets the prefix at the beginning of the column, naming themaddr_city,addr_street, ...
- defineEntity + class
- defineEntity
- reflect-metadata
- ts-morph
const ContactSchema = defineEntity({
name: 'Contact',
embeddable: true,
properties: {
address: () => p.embedded(Address).prefix('addr_').prefixMode('absolute'),
address2: () => p.embedded(Address).prefix('addr2_').prefixMode('relative'),
},
});
const UserSchema = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
contact: () => p.embedded(Contact),
},
});
export class Contact extends ContactSchema.class {}
ContactSchema.setClass(Contact);
export class User extends UserSchema.class {}
UserSchema.setClass(User);
export const Contact = defineEntity({
name: 'Contact',
embeddable: true,
properties: {
address: () => p.embedded(Address).prefix('addr_').prefixMode('absolute'),
address2: () => p.embedded(Address).prefix('addr2_').prefixMode('relative'),
},
});
export type IContact = InferEntity<typeof Contact>;
export const User = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
contact: () => p.embedded(Contact),
},
});
export type IUser = InferEntity<typeof User>;
@Embeddable()
export class Contact {
@Embedded({ entity: () => Address, prefix: 'addr_', prefixMode: 'absolute' })
address!: Address;
@Embedded({ entity: () => Address, prefix: 'addr2_', prefixMode: 'relative' })
address2!: Address;
}
@Entity()
export class User {
@Embedded(() => Contact)
contact!: Contact;
}
@Embeddable()
export class Contact {
@Embedded({ prefix: 'addr_', prefixMode: 'absolute' })
address!: Address;
@Embedded({ prefix: 'addr2_', prefixMode: 'relative' })
address2!: Address;
}
@Entity()
export class User {
@Embedded()
contact!: Contact;
}
The default behavior can be overridden in the ORM configuration:
MikroORM.init({
embeddables: {
prefixMode: 'absolute', // defaults to `true`
},
})
To have MikroORM drop the prefix and use the value object's property name directly, set prefix: false:
- defineEntity + class
- defineEntity
- reflect-metadata
- ts-morph
const UserSchema = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
address: () => p.embedded(Address).prefix(false),
},
});
export class User extends UserSchema.class {}
UserSchema.setClass(User);
const User = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
address: () => p.embedded(Address).prefix(false),
},
});
@Embedded({ entity: () => Address, prefix: false })
address!: Address;
@Embedded({ prefix: false })
address!: Address;
Storing embeddables as objects
You can also store the embeddable as an object instead of inlining its properties to the owning entity.
- defineEntity + class
- defineEntity
- reflect-metadata
- ts-morph
const UserSchema = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
address: () => p.embedded(Address).object(),
},
});
export class User extends UserSchema.class {}
UserSchema.setClass(User);
const User = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
address: () => p.embedded(Address).object(),
},
});
@Embedded({ entity: () => Address, object: true })
address!: Address;
@Embedded({ object: true })
address!: Address;
In SQL drivers, this will use a JSON column to store the value.
Only MySQL and PostgreSQL drivers support searching by JSON properties currently.
This part of documentation is highly inspired by doctrine tutorial as the behaviour here is pretty much the same.
Array of embeddables
Embedded arrays are always stored as JSON. It is possible to use them inside nested embeddables.
- defineEntity + class
- defineEntity
- reflect-metadata
- ts-morph
const UserSchema = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
addresses: () => p.embedded(Address).array().onCreate(() => []),
},
});
export class User extends UserSchema.class {}
UserSchema.setClass(User);
const User = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
addresses: () => p.embedded(Address).array().onCreate(() => []),
},
});
@Embedded(() => Address, { array: true })
addresses: Address[] = [];
@Embedded()
addresses: Address[] = [];
Querying array embeddables
You can query properties of embedded array elements directly. MikroORM will automatically generate the appropriate SQL using platform-specific JSON array iteration (e.g. jsonb_array_elements for PostgreSQL, json_table for MySQL/MariaDB, json_each for SQLite).
When multiple property conditions are specified, they must all match the same array element (similar to MongoDB's $elemMatch):
// find users who have an address in London
const users = await em.find(User, {
addresses: { city: 'London' },
});
// find users who have an address in London, UK (same element must match both)
const users = await em.find(User, {
addresses: { city: 'London', country: 'UK' },
});
// operators work too
const users = await em.find(User, {
addresses: { number: { $gt: 5 } },
});
// $or/$and within the array element scope
const users = await em.find(User, {
addresses: { $or: [{ city: 'London' }, { city: 'Paris' }] },
});
// $not finds rows where NO element matches the condition (NOT EXISTS)
const users = await em.find(User, {
addresses: { $not: { city: 'London' } },
});
// combine $not with positive conditions:
// at least one element in Paris AND no element in London
const users = await em.find(User, {
addresses: { $not: { city: 'London' }, city: 'Paris' },
});
Note: Top-level
$notusesNOT EXISTSsemantics (no array element matches). Inside$or/$and,$notapplies element-level negation instead (the current element does not match).
Array-level operators like $contains, $contained, and $overlap continue to work as before, operating on the array column as a whole.
Nested embeddables
You can also nest embeddables, both in inline mode and object mode:
- defineEntity + class
- defineEntity
- reflect-metadata
- ts-morph
const UserSchema = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
name: p.string(),
address: () => p.embedded(Address),
},
});
const ProfileSchema = defineEntity({
name: 'Profile',
embeddable: true,
properties: {
username: p.string(),
identity: () => p.embedded(Identity),
},
});
export class Profile extends ProfileSchema.class {}
ProfileSchema.setClass(Profile);
const IdentitySchema = defineEntity({
name: 'Identity',
embeddable: true,
properties: {
email: p.string(),
},
});
export class Identity extends IdentitySchema.class {}
IdentitySchema.setClass(Identity);
export class User extends UserSchema.class {}
UserSchema.setClass(User);
export const User = defineEntity({
name: 'User',
properties: {
id: p.integer().primary(),
name: p.string(),
address: () => p.embedded(Address),
},
});
export type IUser = InferEntity<typeof User>;
export const Profile = defineEntity({
name: 'Profile',
embeddable: true,
properties: {
username: p.string(),
identity: () => p.embedded(Identity),
},
});
export type IProfile = InferEntity<typeof Profile>;
export const Identity = defineEntity({
name: 'Identity',
embeddable: true,
properties: {
email: p.string(),
},
});
export type IIdentity = InferEntity<typeof Identity>;
import { Embeddable, Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Embedded(() => Profile, { object: true, nullable: true })
profile?: Profile;
}
@Embeddable()
export class Profile {
@Property()
username: string;
@Embedded(() => Identity)
identity: Identity;
constructor(username: string, identity: Identity) {
this.username = username;
this.identity = identity;
}
}
@Embeddable()
export class Identity {
@Property()
email: string;
constructor(email: string) {
this.email = email;
}
}
import { Embeddable, Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Embedded({ object: true })
profile?: Profile;
}
@Embeddable()
export class Profile {
@Property()
username: string;
@Embedded()
identity: Identity;
constructor(username: string, identity: Identity) {
this.username = username;
this.identity = identity;
}
}
@Embeddable()
export class Identity {
@Property()
email: string;
constructor(email: string) {
this.email = email;
}
}
Polymorphic embeddables
It is also possible to use polymorphic embeddables. This means you can define multiple classes for a single embedded property and the right one will be used based on the discriminator column, similar to how single table inheritance work.
- defineEntity + class
- defineEntity
- reflect-metadata
- ts-morph
import { defineEntity, p } from '@mikro-orm/core';
export enum AnimalType {
CAT,
DOG,
}
const AnimalSchema = defineEntity({
name: 'Animal',
embeddable: true,
abstract: true,
discriminatorColumn: 'type',
properties: {
type: p.enum(() => AnimalType),
name: p.string(),
},
});
export class Animal extends AnimalSchema.class {}
AnimalSchema.setClass(Animal);
const CatSchema = defineEntity({
name: 'Cat',
embeddable: true,
extends: Animal,
discriminatorValue: AnimalType.CAT,
properties: {
canMeow: p.boolean().nullable(),
},
});
export class Cat extends CatSchema.class {}
CatSchema.setClass(Cat);
const DogSchema = defineEntity({
name: 'Dog',
embeddable: true,
extends: Animal,
discriminatorValue: AnimalType.DOG,
properties: {
canBark: p.boolean().nullable(),
},
});
export class Dog extends DogSchema.class {}
DogSchema.setClass(Dog);
const OwnerSchema = defineEntity({
name: 'Owner',
properties: {
id: p.integer().primary(),
name: p.string(),
pet: () => p.embedded([Cat, Dog]),
},
});
export class Owner extends OwnerSchema.class {}
OwnerSchema.setClass(Owner);
import { defineEntity, p, type InferEntity } from '@mikro-orm/core';
export enum AnimalType {
CAT,
DOG,
}
export const Animal = defineEntity({
name: 'Animal',
embeddable: true,
abstract: true,
discriminatorColumn: 'type',
properties: {
type: p.enum(() => AnimalType),
name: p.string(),
},
});
export const Cat = defineEntity({
name: 'Cat',
embeddable: true,
extends: Animal,
discriminatorValue: AnimalType.CAT,
properties: {
canMeow: p.boolean().nullable(),
},
});
export const Dog = defineEntity({
name: 'Dog',
embeddable: true,
extends: Animal,
discriminatorValue: AnimalType.DOG,
properties: {
canBark: p.boolean().nullable(),
},
});
export const Owner = defineEntity({
name: 'Owner',
properties: {
id: p.integer().primary(),
name: p.string(),
pet: () => p.embedded([Cat, Dog]),
},
});
export type ICat = InferEntity<typeof Cat>;
export type IDog = InferEntity<typeof Dog>;
export type IOwner = InferEntity<typeof Owner>;
import { Embeddable, Embedded, Entity, Enum, PrimaryKey, Property } from '@mikro-orm/core';
export enum AnimalType {
CAT,
DOG,
}
@Embeddable({ abstract: true, discriminatorColumn: 'type' })
export abstract class Animal {
@Enum(() => AnimalType)
type!: AnimalType;
@Property()
name!: string;
}
@Embeddable({ discriminatorValue: AnimalType.CAT })
export class Cat extends Animal {
@Property({ nullable: true })
canMeow?: boolean = true;
constructor(name: string) {
super();
this.type = AnimalType.CAT;
this.name = name;
}
}
@Embeddable({ discriminatorValue: AnimalType.DOG })
export class Dog extends Animal {
@Property({ nullable: true })
canBark?: boolean = true;
constructor(name: string) {
super();
this.type = AnimalType.DOG;
this.name = name;
}
}
@Entity()
export class Owner {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Embedded(() => [Cat, Dog])
pet!: Cat | Dog;
}
import { Embeddable, Embedded, Entity, Enum, PrimaryKey, Property } from '@mikro-orm/core';
export enum AnimalType {
CAT,
DOG,
}
@Embeddable({ abstract: true, discriminatorColumn: 'type' })
export abstract class Animal {
@Enum()
type!: AnimalType;
@Property()
name!: string;
}
@Embeddable({ discriminatorValue: AnimalType.CAT })
export class Cat extends Animal {
@Property()
canMeow? = true;
constructor(name: string) {
super();
this.type = AnimalType.CAT;
this.name = name;
}
}
@Embeddable({ discriminatorValue: AnimalType.DOG })
export class Dog extends Animal {
@Property()
canBark? = true;
constructor(name: string) {
super();
this.type = AnimalType.DOG;
this.name = name;
}
}
@Entity()
export class Owner {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Embedded()
pet!: Cat | Dog;
}