Pular para o conteúdo

Herança e Composição

O Mirror oferece dois mecanismos para modelar estruturas de dados mais ricas:

  • @Embedded — composição. Achata um objeto de valor (value object) em colunas prefixadas da tabela pai.
  • @ChildEntity — herança. Single Table Inheritance (STI): subtipos compartilham a mesma tabela, diferenciados por uma coluna discriminadora.

Use @Embedded quando um conceito de domínio tem múltiplos campos que fazem sentido juntos, mas não merecem uma tabela própria.

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;
}

A classe embedded (Address) não precisa de @Entity.

O Mirror achata as propriedades da classe embedded usando um prefixo. Por padrão, o prefixo é o nome da propriedade seguido de underscore:

PropriedadeColuna no banco
address.streetaddress_street
address.cityaddress_city
address.zipCodeaddress_zip_code

O SQL gerado no find():

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

Passe o prefixo como segundo argumento:

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

O objeto embedded é construído e acessado normalmente — o Mirror cuida do achatamento e reconstrução:

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'

O dirty checking funciona até o nível das propriedades internas — alterar apenas address.street gera um UPDATE com apenas aquela coluna:

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

Os decorators de coluna dentro da classe embedded suportam type normalmente:

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

Apenas um nível de embedding é suportado. Não é possível ter um @Embedded dentro de outro @Embedded.


Use @ChildEntity quando diferentes subtipos compartilham a mesma estrutura base, mas têm colunas específicas por tipo. Todos os subtipos ficam na mesma tabela, diferenciados por uma coluna discriminadora.

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;
}

A tabela vehicles no banco precisa ter todas as colunas de todos os subtipos:

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
);

Os filhos herdam automaticamente todas as colunas e relações do pai. Você só declara o que é exclusivo do subtipo:

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

Ao salvar um filho, o Mirror injeta automaticamente o valor do discriminador. Você não precisa definir kind manualmente:

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

Ao consultar pelo repositório do pai (Vehicle), o Mirror retorna instâncias do tipo correto de acordo com o valor da coluna discriminadora:

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

Se o valor do discriminador não corresponder a nenhum filho registrado, o Mirror instancia a classe pai — sem erros.

Ao consultar pelo repositório de um filho, o Mirror acrescenta automaticamente o filtro do discriminador:

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'

O filtro é adicionado em todas as operações: find, findOne, findById, findAll e count.

Os filhos herdam os lifecycle hooks do pai. Se o filho definir o mesmo hook, o do filho tem prioridade:

@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
}
}

Se o filho não declarar um hook, o hook do pai é usado. Se o filho declarar, o hook do filho substitui completamente o do pai — não é uma composição, é uma substituição. Redefina a validação do pai no filho se precisar dos dois.


@Embedded@ChildEntity
PadrãoComposiçãoHerança
TabelaMesma tabela, colunas prefixadasMesma tabela, linha por subtipo
Identificação de tipoNão há — sempre mesmo tipoColuna discriminadora
Caso de usoEndereço, dinheiro, coordenadasTipos de usuário, tipos de evento, tipos de veículo
Nível de suporte1 nível de profundidadeApenas 1 nível de herança
RelaçõesNão suporta relaçõesHerda relações do pai

Ver também: Hooks do @ChildEntity são herdados do pai — veja o comportamento completo em Lifecycle Hooks. O dirty checking do @Embedded funciona com save() — veja Repository API.