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.
@Embedded — Value Objects
Section titled “@Embedded — Value Objects”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.
Columns generated in the database
Section titled “Columns generated in the database”Mirror flattens the embedded class properties using a prefix. By default, the prefix is the property name followed by underscore:
| Property | Column on the bench |
|---|---|
address.street | address_street |
address.city | address_city |
address.zipCode | address_zip_code |
The SQL generated in find():
SELECT "id", "name", "address_street", "address_city", "address_zip_code"FROM "customers"Custom prefix
Section titled “Custom prefix”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_currencyNormal use
Section titled “Normal use”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 normalconst 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 automaticamenteconst loaded = await repo.findById(customer.id);console.log(loaded.address instanceof Address); // trueconsole.log(loaded.address.city); // 'São Paulo'Dirty checking in embedded
Section titled “Dirty checking in embedded”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 = $3Type casting in embedded
Section titled “Type casting in embedded”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 automaticamenteDepth limit
Section titled “Depth limit”Only one level of embedding is supported. It is not possible to have a @Embedded inside another @Embedded.
@ChildEntity — Single Table Inheritance
Section titled “@ChildEntity — Single Table Inheritance”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);Inheritance of columns and relationships
Section titled “Inheritance of columns and relationships”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: doorsconst meta = registry.getEntity('Car');// meta.columns = [id, kind, brand, doors]Automatic INSERT with discriminator
Section titled “Automatic INSERT with discriminator”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 automaticamentePolymorphic queries by parent
Section titled “Polymorphic queries by parent”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 desconhecidoIf the discriminator value does not match any registered children, Mirror instantiates the parent class — without error.
Queries by subtype with automatic filter
Section titled “Queries by subtype with automatic filter”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.
Lifecycle hooks
Section titled “Lifecycle hooks”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.
When to use each
Section titled “When to use each”@Embedded | @ChildEntity | |
|---|---|---|
| Default | Composition | Inheritance |
| Table | Same table, prefixed columns | Same table, row by subtype |
| Type identification | There is none — always the same type | Discriminator column |
| Use case | Address, money, coordinates | User Types, Event Types, Vehicle Types |
| Support level | 1 level deep | Only 1 Inheritance Level |
| Relationships | Does not support relationships | Inherits relationships from father |
See also:
@ChildEntityhook inheritance behavior is in Lifecycle Hooks. The dirty checking of@Embeddedworks withsave()— see Repository API.