En esta página
Repositorios, QueryBuilder y paginación
El Repository de TypeORM en profundidad
El Repository<T> es la interfaz principal para interactuar con la base de datos en TypeORM. Proporciona un conjunto completo de métodos para operaciones CRUD básicas y avanzadas.
Métodos de búsqueda del Repository
find y findBy
// find con opciones detalladas
const productos = await this.repo.find({
where: { activo: true },
select: { id: true, nombre: true, precio: true }, // selección de campos
relations: { categoria: true }, // joins
order: { precio: 'ASC', nombre: 'ASC' }, // ordenación múltiple
skip: 0, // offset
take: 20, // limit
cache: 60000, // caché por 60 segundos
});
// findBy — forma abreviada para filtros simples
const productos = await this.repo.findBy({ activo: true, categoriaId: 3 });findOne y findOneOrFail
// findOne retorna null si no encuentra
const producto = await this.repo.findOne({
where: { id, activo: true },
relations: { imagenes: true },
});
if (!producto) throw new NotFoundException();
// findOneOrFail lanza EntityNotFoundError automáticamente
const producto = await this.repo.findOneOrFail({
where: { id },
}).catch(() => { throw new NotFoundException(`Producto ${id} no encontrado`); });Operadores de Where avanzados
TypeORM proporciona operadores especiales para condiciones complejas:
import {
Like, ILike, Between, In, Not, IsNull, LessThan, MoreThan,
LessThanOrEqual, MoreThanOrEqual, Any, ArrayContains,
} from 'typeorm';
const productos = await this.repo.find({
where: [
// OR se hace con array
{ nombre: ILike('%zapato%') },
{ descripcion: ILike('%zapato%') },
],
});
const enRango = await this.repo.find({
where: {
precio: Between(100, 500),
categoriaId: In([1, 2, 3]),
eliminadoEn: IsNull(),
stock: MoreThan(0),
},
});
// NOT
const noActivos = await this.repo.findBy({
estado: Not('activo'),
});QueryBuilder — Consultas SQL expresivas
Para consultas que van más allá de los filtros simples del find, el QueryBuilder es la herramienta adecuada:
// Consulta con subconsulta y funciones de agregación
const resumen = await this.repo
.createQueryBuilder('producto')
.select('categoria.nombre', 'categoria')
.addSelect('COUNT(producto.id)', 'cantidad')
.addSelect('AVG(producto.precio)', 'precioPromedio')
.addSelect('SUM(producto.stock)', 'stockTotal')
.leftJoin('producto.categoria', 'categoria')
.where('producto.activo = :activo', { activo: true })
.groupBy('categoria.id')
.orderBy('cantidad', 'DESC')
.getRawMany<{
categoria: string;
cantidad: string;
precioPromedio: string;
stockTotal: string;
}>();Paginación basada en cursor
La paginación por cursor (o keyset pagination) es más eficiente que la paginación por offset en tablas grandes, porque no necesita saltar registros:
export class CursorPaginacionDto {
@IsOptional()
@IsString()
cursor?: string; // ID del último elemento recibido
@Type(() => Number)
limite: number = 20;
}
async findAllConCursor(dto: CursorPaginacionDto): Promise<{
datos: Producto[];
proximoCursor: string | null;
}> {
const qb = this.repo.createQueryBuilder('p')
.orderBy('p.creadoEn', 'DESC')
.addOrderBy('p.id', 'DESC')
.take(dto.limite + 1); // pide uno más para saber si hay siguiente página
if (dto.cursor) {
// Decodifica el cursor (base64)
const { fecha, id } = JSON.parse(
Buffer.from(dto.cursor, 'base64').toString('utf-8')
) as { fecha: string; id: string };
qb.where(
'(p.creadoEn < :fecha) OR (p.creadoEn = :fecha AND p.id < :id)',
{ fecha, id }
);
}
const resultados = await qb.getMany();
const tieneSiguiente = resultados.length > dto.limite;
const datos = tieneSiguiente ? resultados.slice(0, dto.limite) : resultados;
const ultimoElemento = datos[datos.length - 1];
const proximoCursor = tieneSiguiente && ultimoElemento
? Buffer.from(JSON.stringify({
fecha: ultimoElemento.creadoEn,
id: ultimoElemento.id,
})).toString('base64')
: null;
return { datos, proximoCursor };
}Actualizaciones masivas eficientes
Para actualizar múltiples registros a la vez sin cargarlos en memoria:
// Actualización masiva sin cargar entidades
await this.repo.update(
{ categoriaId: 5, activo: true },
{ estado: EstadoProducto.INACTIVO }
);
// Con QueryBuilder para condiciones más complejas
await this.repo
.createQueryBuilder()
.update(Producto)
.set({ stock: () => 'stock - :cantidad', estado: EstadoProducto.AGOTADO })
.where('id IN (:...ids)', { ids: ['id1', 'id2', 'id3'] })
.andWhere('stock > 0')
.setParameter('cantidad', 5)
.execute();Consultas de estadísticas con funciones de agregación
async obtenerEstadisticas(): Promise<EstadisticasProducto> {
const resultado = await this.repo
.createQueryBuilder('p')
.select('COUNT(p.id)', 'total')
.addSelect('COUNT(CASE WHEN p.stock = 0 THEN 1 END)', 'agotados')
.addSelect('AVG(p.precio)', 'precioPromedio')
.addSelect('MAX(p.precio)', 'precioMaximo')
.addSelect('MIN(p.precio)', 'precioMinimo')
.addSelect('SUM(p.stock * p.precio)', 'valorInventario')
.where('p.eliminadoEn IS NULL')
.getRawOne<{
total: string;
agotados: string;
precioPromedio: string;
precioMaximo: string;
precioMinimo: string;
valorInventario: string;
}>();
return {
total: parseInt(resultado?.total ?? '0', 10),
agotados: parseInt(resultado?.agotados ?? '0', 10),
precioPromedio: parseFloat(resultado?.precioPromedio ?? '0'),
precioMaximo: parseFloat(resultado?.precioMaximo ?? '0'),
precioMinimo: parseFloat(resultado?.precioMinimo ?? '0'),
valorInventario: parseFloat(resultado?.valorInventario ?? '0'),
};
}Búsqueda full-text con PostgreSQL
Para búsquedas de texto más sofisticadas, puedes aprovechar el full-text search nativo de PostgreSQL:
async busquedaFullText(termino: string, pagina: number, limite: number) {
const tsQuery = termino
.trim()
.split(/\s+/)
.map(t => `${t}:*`)
.join(' & ');
return this.repo
.createQueryBuilder('p')
.where(
`to_tsvector('spanish', p.nombre || ' ' || COALESCE(p.descripcion, '')) @@ to_tsquery('spanish', :tsQuery)`,
{ tsQuery }
)
.orderBy(
`ts_rank(to_tsvector('spanish', p.nombre || ' ' || COALESCE(p.descripcion, '')), to_tsquery('spanish', :tsQuery))`,
'DESC'
)
.skip((pagina - 1) * limite)
.take(limite)
.getManyAndCount();
}En la próxima lección agregaremos autenticación JWT completa a nuestra API, combinando todo lo que hemos aprendido sobre guards, servicios y TypeORM en un flujo de login y registro funcional.
Inicia sesión para guardar tu progreso