Pular para o conteúdo

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.


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.


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.


O lado que possui a FK. Carregado com um LEFT JOIN na query principal — sem query extra.

@ManyToOne(target, foreignKey, options?)
ParâmetroTipoDescrição
target() => typeof EntityLambda retornando a entidade alvo — evita referências circulares
foreignKeystringNome da coluna FK no banco (nesta tabela)
options.cascadeboolean | 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;
}

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.

Relações não são carregadas por padrão:

const repo = conn.getRepository(Book);
// Sem relação
const book = await repo.findOne({ where: { id: 1 } });
// book.author === undefined
// Com relação
const book = await repo.findOne({
where: { id: 1 },
relations: ['author'],
});
// book.author instanceof Author (ou null se authorId for null)

O lado que não possui a FK. Carregado via batch query separada, eliminando o problema N+1.

@OneToMany(target, foreignKey, options?)
ParâmetroTipoDescrição
target() => typeof EntityLambda retornando a entidade “muitos”
foreignKeystringNome da coluna FK no banco, na tabela do lado “muitos”
options.cascadeboolean | CascadeType[]Operações em cascata
@Entity('authors')
class Author {
@PrimaryColumn({ strategy: 'identity' })
id!: number;
@Column()
name!: string;
@OneToMany(() => Book, 'author_id')
books!: Book[];
}

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 authors
SELECT "authors"."id", "authors"."name" FROM "authors"
-- Query 2: busca todos os books de uma vez
SELECT * 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.


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âmetroTipoDescrição
target() => typeof EntityLambda retornando a entidade alvo
foreignKeystringNome da coluna FK no banco
options.cascadeboolean | CascadeType[]Operações em cascata

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

Usa uma tabela pivô. A assinatura é posicional — a ordem dos parâmetros importa.

@ManyToMany(target, joinTable, ownerFk, inverseFk)
ParâmetroTipoDescrição
target() => typeof EntityLambda retornando a entidade alvo
joinTablestringNome da tabela pivô no banco
ownerFkstringColuna na pivô que aponta para esta entidade
inverseFkstringColuna 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.

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

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 articles
SELECT "articles"."id", "articles"."title" FROM "articles"
-- Query 2: busca todas as tags dos articles retornados
SELECT
"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.


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[];
ValorComportamento
'insert'Salva entidades relacionadas novas junto com o pai
'update'Propaga save() para entidades relacionadas existentes
'remove'Remove entidades relacionadas ao remover o pai

DecoratorEstratégiaQueriesResultado
@ManyToOneLEFT JOIN1T | null
@OneToManyBatch ANY($1)2T[]
@OneToOne (owner)LEFT JOIN1T | null
@OneToOne (inverse)Batch ANY($1)2T | null
@ManyToManyINNER JOIN + Batch2T[]

Ver também: Para ativar o carregamento de relações nas queries, veja a opção relations no Repository API. Para usar cascades dentro de transações, veja Transactions.