Relacionamentos
O Mirror carrega relações de forma explícita e eficiente. Não há lazy loading via Proxy, sem queries escondidas em getters. Cada decorator define a estratégia de carregamento — e você sempre sabe quantas queries estão sendo disparadas.
A Regra Fundamental: A FK Explícita
Seção intitulada “A Regra Fundamental: A FK Explícita”Este é o gotcha número 1 para quem vem do TypeORM ou Prisma.
No Mirror, a coluna de chave estrangeira precisa existir como @Column separado na entidade. O decorator de relação apenas descreve como navegar por ela — ele não cria a coluna implicitamente.
// ❌ Errado — author_id não existe como coluna, o Mirror não consegue montar o INSERT@Entity('books')class Book { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@ManyToOne(() => Author, 'author_id') author!: Author;}
// ✅ Correto — a FK é uma coluna real, o decorator de relação apenas navega por ela@Entity('books')class Book { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() authorId!: number;
@ManyToOne(() => Author, 'author_id') author!: Author | null;}O foreignKey passado para o decorator é sempre o nome da coluna no banco (snake_case), enquanto a propriedade na classe pode ter qualquer nome.
Relações são unilaterais
Seção intitulada “Relações são unilaterais”No Mirror, você só precisa declarar o lado que vai consultar. O Mirror nunca lê o metadata da entidade relacionada para descobrir o lado oposto — cada decorator é completamente autossuficiente.
Isso contrasta com TypeORM, onde @OneToMany exige @ManyToOne + mappedBy no outro lado. Aqui:
// Você pode ter apenas o @ManyToOne no Book, sem nenhum @OneToMany no Author@Entity('books')class Book { @Column() authorId!: number;
@ManyToOne(() => Author, 'author_id') author!: Author | null;}
@Entity('authors')class Author { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() name!: string; // Sem @OneToMany — e tudo funciona}Declare os dois lados somente quando precisar navegar pela relação nos dois sentidos.
@ManyToOne
Seção intitulada “@ManyToOne”O lado que possui a FK. Carregado com um LEFT JOIN na query principal — sem query extra.
@ManyToOne(target, foreignKey, options?)| Parâmetro | Tipo | Descrição |
|---|---|---|
target | () => typeof Entity | Lambda retornando a entidade alvo — evita referências circulares |
foreignKey | string | Nome da coluna FK no banco (nesta tabela) |
options.cascade | boolean | CascadeType[] | Operações em cascata: 'insert', 'update', 'remove' |
import { Entity, PrimaryColumn, Column, ManyToOne } from 'mirror-orm';
@Entity('books')class Book { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() title!: string;
@Column() authorId!: number;
@ManyToOne(() => Author, 'author_id') author!: Author | null;}SQL gerado
Seção intitulada “SQL gerado”O Mirror adiciona um LEFT JOIN e seleciona as colunas do relacionamento com o prefixo mirror__<propriedade>__:
SELECT "books"."id", "books"."title", "books"."author_id", "authors"."id" AS "mirror__author__id", "authors"."name" AS "mirror__author__name"FROM "books"LEFT JOIN "authors" ON "books"."author_id" = "authors"."id"O prefixo mirror__author__ evita colisão de nomes entre tabelas e é usado pelo JIT Hydrator para instanciar o objeto Author. Se você ver esse padrão em logs de query, é uma relação @ManyToOne sendo carregada.
Carregamento
Seção intitulada “Carregamento”Relações não são carregadas por padrão:
const repo = conn.getRepository(Book);
// Sem relaçãoconst book = await repo.findOne({ where: { id: 1 } });// book.author === undefined
// Com relaçãoconst book = await repo.findOne({ where: { id: 1 }, relations: ['author'],});// book.author instanceof Author (ou null se authorId for null)@OneToMany
Seção intitulada “@OneToMany”O lado que não possui a FK. Carregado via batch query separada, eliminando o problema N+1.
@OneToMany(target, foreignKey, options?)| Parâmetro | Tipo | Descrição |
|---|---|---|
target | () => typeof Entity | Lambda retornando a entidade “muitos” |
foreignKey | string | Nome da coluna FK no banco, na tabela do lado “muitos” |
options.cascade | boolean | CascadeType[] | Operações em cascata |
@Entity('authors')class Author { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() name!: string;
@OneToMany(() => Book, 'author_id') books!: Book[];}SQL gerado
Seção intitulada “SQL gerado”São duas queries. A principal não muda, e o Mirror dispara uma batch query com todos os IDs coletados:
-- Query 1: busca os authorsSELECT "authors"."id", "authors"."name" FROM "authors"
-- Query 2: busca todos os books de uma vezSELECT * FROM "books"WHERE "books"."author_id" = ANY($1)-- $1 = [1, 2, 3, ...]O Mirror agrupa os resultados por author_id e distribui para cada instância de Author. Resultado é sempre T[] — [] se não houver registros.
@OneToOne
Seção intitulada “@OneToOne”O Mirror detecta automaticamente qual lado possui a FK, sem necessidade de mappedBy. Ambos os lados retornam T | null, nunca um array.
@OneToOne(target, foreignKey, options?)| Parâmetro | Tipo | Descrição |
|---|---|---|
target | () => typeof Entity | Lambda retornando a entidade alvo |
foreignKey | string | Nome da coluna FK no banco |
options.cascade | boolean | CascadeType[] | Operações em cascata |
Detecção automática de lado
Seção intitulada “Detecção automática de lado”O Mirror verifica se a coluna foreignKey existe nas colunas da entidade atual:
- Lado owner (tem a FK) → carrega com
LEFT JOIN, como@ManyToOne - Lado inverse (não tem a FK) → carrega com batch query, como
@OneToMany
@Entity('person_profiles')class PersonProfile { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() personId!: number; // FK está aqui → lado owner
@OneToOne(() => Person, 'person_id') person!: Person | null; // LEFT JOIN}
@Entity('persons')class Person { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() name!: string;
@OneToOne(() => PersonProfile, 'person_id') profile!: PersonProfile | null; // batch query (não tem a FK)}Como as relações são unilaterais, você pode declarar apenas o lado owner se só precisar navegar nessa direção:
// Somente o lado que tem a FK — perfeitamente válido@Entity('person_profiles')class PersonProfile { @Column() personId!: number;
@OneToOne(() => Person, 'person_id') person!: Person | null;}@ManyToMany
Seção intitulada “@ManyToMany”Usa uma tabela pivô. A assinatura é posicional — a ordem dos parâmetros importa.
@ManyToMany(target, joinTable, ownerFk, inverseFk)| Parâmetro | Tipo | Descrição |
|---|---|---|
target | () => typeof Entity | Lambda retornando a entidade alvo |
joinTable | string | Nome da tabela pivô no banco |
ownerFk | string | Coluna na pivô que aponta para esta entidade |
inverseFk | string | Coluna na pivô que aponta para a entidade alvo |
@Entity('articles')class Article { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() title!: string;
// alvo pivô ownerFk inverseFk @ManyToMany(() => Tag, 'article_tags', 'article_id', 'tag_id') tags!: Tag[];}
@Entity('tags')class Tag { @PrimaryColumn({ strategy: 'identity' }) id!: number;
@Column() name!: string;
// alvo pivô ownerFk inverseFk @ManyToMany(() => Article, 'article_tags', 'tag_id', 'article_id') articles!: Article[];}No lado inverso (
Tag), os FKs são invertidos:ownerFk = 'tag_id',inverseFk = 'article_id'.
Assim como os outros decorators, @ManyToMany também é unilateral. Declare em Tag somente se for consultar artigos a partir de uma tag.
Tabela pivô esperada
Seção intitulada “Tabela pivô esperada”CREATE TABLE article_tags ( article_id INTEGER NOT NULL REFERENCES articles(id), tag_id INTEGER NOT NULL REFERENCES tags(id), PRIMARY KEY (article_id, tag_id));SQL gerado
Seção intitulada “SQL gerado”São duas queries. A batch query faz INNER JOIN com a pivô e usa o alias interno _mirror_mtm_fk_ para rastrear a qual owner cada resultado pertence:
-- Query 1: busca os articlesSELECT "articles"."id", "articles"."title" FROM "articles"
-- Query 2: busca todas as tags dos articles retornadosSELECT "tags".*, "article_tags"."article_id" AS "_mirror_mtm_fk_"FROM "tags"INNER JOIN "article_tags" ON "article_tags"."tag_id" = "tags"."id"WHERE "article_tags"."article_id" = ANY($1)O alias _mirror_mtm_fk_ é removido antes de hidratar os objetos Tag. Se aparecer em logs de query, é o mecanismo de agrupamento do @ManyToMany funcionando corretamente.
Cascades
Seção intitulada “Cascades”Todos os decorators aceitam cascade como opção:
@ManyToOne(() => Author, 'author_id', { cascade: ['insert', 'update'] })author!: Author | null;
@OneToMany(() => Book, 'author_id', { cascade: true })books!: Book[];| Valor | Comportamento |
|---|---|
'insert' | Salva entidades relacionadas novas junto com o pai |
'update' | Propaga save() para entidades relacionadas existentes |
'remove' | Remove entidades relacionadas ao remover o pai |
Resumo das Estratégias
Seção intitulada “Resumo das Estratégias”| Decorator | Estratégia | Queries | Resultado |
|---|---|---|---|
@ManyToOne | LEFT JOIN | 1 | T | null |
@OneToMany | Batch ANY($1) | 2 | T[] |
@OneToOne (owner) | LEFT JOIN | 1 | T | null |
@OneToOne (inverse) | Batch ANY($1) | 2 | T | null |
@ManyToMany | INNER JOIN + Batch | 2 | T[] |
Ver também: Para ativar o carregamento de relações nas queries, veja a opção
relationsno Repository API. Para usar cascades dentro de transações, veja Transactions.