Skip to content

Inheritance and Composition

Mirror offers two mechanisms for modeling richer data structures:

  • @Embedded — composition. Flattens a value object into prefixed columns of the parent table.
  • @ChildEntity — inheritance. Single Table Inheritance (STI): subtypes share the same table, differentiated by a discriminator column.

Use @Embedded when a domain concept has multiple fields that make sense together but don’t deserve their own table.

import { Column, Embedded, Entity, PrimaryColumn } from 'mirror-orm';
class Address {
@Column()
street!: string;
@Column()
city!: string;
@Column()
zipCode!: string;
}
@Entity('customers')
class Customer {
@PrimaryColumn({ strategy: 'identity' })
id!: number;
@Column()
name!: string;
@Embedded(() => Address)
address!: Address;
}

The embedded class (Address) does not need @Entity.

Mirror flattens the embedded class properties using a prefix. By default, the prefix is the property name followed by underscore:

PropertyColumn on the bench
address.streetaddress_street
address.cityaddress_city
address.zipCodeaddress_zip_code

The SQL generated in find():

SELECT "id", "name", "address_street", "address_city", "address_zip_code"
FROM "customers"

Pass the prefix as the second argument:

class Money {
@Column({ type: 'number' })
amount!: number;
@Column()
currency!: string;
}
@Entity('orders')
class Order {
@PrimaryColumn({ strategy: 'identity' })
id!: number;
@Embedded(() => Money, 'price_')
price!: Money;
}
// Colunas: price_amount, price_currency

The embedded object is constructed and accessed normally — Mirror takes care of the flattening and reconstruction:

const repo = conn.getRepository(Customer);
// INSERT — escreva como objeto normal
const customer = new Customer();
customer.name = 'Alice';
customer.address = new Address();
customer.address.street = 'Rua das Flores, 42';
customer.address.city = 'São Paulo';
customer.address.zipCode = '01310-100';
await repo.save(customer);
// INSERT INTO customers (name, address_street, address_city, address_zip_code)
// VALUES ($1, $2, $3, $4)
// SELECT — reconstruído automaticamente
const loaded = await repo.findById(customer.id);
console.log(loaded.address instanceof Address); // true
console.log(loaded.address.city); // 'São Paulo'

Dirty checking works down to the level of internal properties — changing just address.street generates a UPDATE with just that column:

const customer = await repo.findById(1);
customer.address.street = 'Av. Paulista, 1000';
await repo.save(customer);
// UPDATE customers SET address_street = $1, updated_at = $2 WHERE id = $3

Column decorators within the embedded class support type normally:

class Dimensions {
@Column({ type: 'number' })
width!: number;
@Column({ type: 'number' })
height!: number;
}
// O banco retorna strings — o Mirror converte para number automaticamente

Only one level of embedding is supported. It is not possible to have a @Embedded inside another @Embedded.


Use @ChildEntity when different subtypes share the same base structure but have type-specific columns. All subtypes are in the same table, differentiated by a discriminator column.

import { Column, ChildEntity, Entity, PrimaryColumn } from 'mirror-orm';
@Entity({ tableName: 'vehicles', discriminatorColumn: 'kind' })
class Vehicle {
@PrimaryColumn({ strategy: 'identity' })
id!: number;
@Column()
kind!: string; // coluna discriminadora — deve existir no banco
@Column()
brand!: string;
}
@ChildEntity('car')
class Car extends Vehicle {
@Column({ type: 'number' })
doors!: number;
}
@ChildEntity('truck')
class Truck extends Vehicle {
@Column({ type: 'number' })
payload!: number;
}

The vehicles table in the database must have all columns of all subtypes:

CREATE TABLE vehicles (
id SERIAL PRIMARY KEY,
kind TEXT NOT NULL, -- coluna discriminadora
brand TEXT NOT NULL,
doors INTEGER, -- só Car usa
payload INTEGER -- só Truck usa
);

Children automatically inherit all columns and relationships from their parent. You only declare what is unique to the subtype:

// Car herda: id, kind, brand
// Car acrescenta: doors
const meta = registry.getEntity('Car');
// meta.columns = [id, kind, brand, doors]

When saving a child, Mirror automatically injects the discriminator value. You don’t need to set kind manually:

const car = new Car();
car.brand = 'Toyota';
car.doors = 4;
await conn.getRepository(Car).save(car);
// INSERT INTO vehicles (kind, brand, doors)
// VALUES ('car', 'Toyota', 4)
// ^^^^^ injetado automaticamente

When querying through the parent repository (Vehicle), Mirror returns instances of the correct type according to the value of the discriminator column:

const vehicles = await conn.getRepository(Vehicle).findAll();
vehicles[0] instanceof Car // true, se kind = 'car'
vehicles[1] instanceof Truck // true, se kind = 'truck'
vehicles[2] instanceof Vehicle // true, se discriminador desconhecido

If the discriminator value does not match any registered children, Mirror instantiates the parent class — without error.

When querying through a child’s repository, Mirror automatically adds the discriminator filter:

const cars = await conn.getRepository(Car).findAll();
// SELECT * FROM vehicles WHERE kind = 'car'
const trucks = await conn.getRepository(Truck).find({
where: { payload: MoreThan(10000) }
});
// SELECT * FROM vehicles WHERE payload > $1 AND kind = 'truck'

The filter is added to all operations: find, findOne, findById, findAll and count.

Children inherit their father’s lifecycle hooks. If the child defines the same hook, the child’s hook has priority:

@Entity({ tableName: 'vehicles', discriminatorColumn: 'kind' })
class Vehicle {
@BeforeInsert()
validate() {
if (!this.brand) throw new Error('brand obrigatório');
}
}
@ChildEntity('car')
class Car extends Vehicle {
@BeforeInsert()
validateCar() {
if (!this.doors) throw new Error('doors obrigatório');
// O hook do pai (validate) NÃO roda — Car sobrescreve
}
}

If the child doesn’t declare a hook, the parent’s hook is used. If the child declares, the child’s hook completely replaces the parent’s — it’s not a composition, it’s a substitution. Reset parent validation on child if you need both.


@Embedded@ChildEntity
DefaultCompositionInheritance
TableSame table, prefixed columnsSame table, row by subtype
Type identificationThere is none — always the same typeDiscriminator column
Use caseAddress, money, coordinatesUser Types, Event Types, Vehicle Types
Support level1 level deepOnly 1 Inheritance Level
RelationshipsDoes not support relationshipsInherits relationships from father

See also: @ChildEntity hook inheritance behavior is in Lifecycle Hooks. The dirty checking of @Embedded works with save() — see Repository API.