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.
@Embedded — Value Objects
Seção intitulada “@Embedded — Value Objects”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.
Colunas geradas no banco
Seção intitulada “Colunas geradas no banco”O Mirror achata as propriedades da classe embedded usando um prefixo. Por padrão, o prefixo é o nome da propriedade seguido de underscore:
| Propriedade | Coluna no banco |
|---|---|
address.street | address_street |
address.city | address_city |
address.zipCode | address_zip_code |
O SQL gerado no find():
SELECT "id", "name", "address_street", "address_city", "address_zip_code"FROM "customers"Prefixo customizado
Seção intitulada “Prefixo customizado”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_currencyUso normal
Seção intitulada “Uso normal”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 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 em embedded
Seção intitulada “Dirty checking em embedded”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 = $3Type casting em embedded
Seção intitulada “Type casting em embedded”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 automaticamenteLimite de profundidade
Seção intitulada “Limite de profundidade”Apenas um nível de embedding é suportado. Não é possível ter um @Embedded dentro de outro @Embedded.
@ChildEntity — Single Table Inheritance
Seção intitulada “@ChildEntity — Single Table Inheritance”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);Herança de colunas e relações
Seção intitulada “Herança de colunas e relações”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: doorsconst meta = registry.getEntity('Car');// meta.columns = [id, kind, brand, doors]INSERT automático com discriminador
Seção intitulada “INSERT automático com discriminador”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 automaticamenteQueries polimórficas pelo pai
Seção intitulada “Queries polimórficas pelo pai”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 desconhecidoSe o valor do discriminador não corresponder a nenhum filho registrado, o Mirror instancia a classe pai — sem erros.
Queries por subtipo com filtro automático
Seção intitulada “Queries por subtipo com filtro automático”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.
Lifecycle hooks
Seção intitulada “Lifecycle hooks”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.
Quando usar cada um
Seção intitulada “Quando usar cada um”@Embedded | @ChildEntity | |
|---|---|---|
| Padrão | Composição | Herança |
| Tabela | Mesma tabela, colunas prefixadas | Mesma tabela, linha por subtipo |
| Identificação de tipo | Não há — sempre mesmo tipo | Coluna discriminadora |
| Caso de uso | Endereço, dinheiro, coordenadas | Tipos de usuário, tipos de evento, tipos de veículo |
| Nível de suporte | 1 nível de profundidade | Apenas 1 nível de herança |
| Relações | Não suporta relações | Herda relações do pai |
Ver também: Hooks do
@ChildEntitysão herdados do pai — veja o comportamento completo em Lifecycle Hooks. O dirty checking do@Embeddedfunciona comsave()— veja Repository API.