TypeORM: Búsqueda relacional (NestJS incluido)

TypeORM: Búsqueda relacional (NestJS incluido)

🤔 ¿Por qué?

Porque no quería realizar una búsqueda completa una vez que obtenía todos los registros. Ya que SQL está optimizado para las queries. Para este caso. Quería filtrar algunos registros basados en sus relaciones con otras tablas.

Voy a utilizar un caso simple para demostrarlo.

💡 Primero, necesitamos dos entidades TypeORM

Para el más sencillo de los casos, supongamos que tenemos dos clases. Una Pet (Mascota) y un Owner (Dueño). Una Mascota pertenece a sólo un Dueño. Pongamos las dos entidades / modelos en la misma carpeta.

./entities/pet.ts
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Owner } from "./owner";
@Entity()
export class Pet {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: "owner_id" })
ownerId: number;
@ManyToOne(() => Owner)
@JoinColumn({ name: "owner_id" })
owner: Owner;
}

Y

./entities/owmer.ts
import { Column, Entity, JoinColumn, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { Pet } from "./pet";
@Entity()
export class Owner {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: "first_name" })
firstName: string;
@Column({ name: "last_name" })
lastName: string;
@OneToMany(() => Pet, () => Owner)
@JoinColumn({ name: "owner_id" })
pets: Pet[];
}

🔨 Creando el método para el controlador

Vamos a usar NestJS para este ejemplo. Un simple controlador NestJS se ve como el siguiente ejemplo.

@Controller("pets")
export default class AppController {
@Get()
async search() {
// TODO: Implementar búsqueda
}
}

Como puedes ver. La URL para la búsqueda será la siguiente /pets/search

🚛 Utilizando repositorios

Este es el método más compatible con Typescript. Prefiero este ya que tendré la ayuda de TS y la validación de tipos si actualizo cualquier campo en el futuro.

./app.ts
import { Controller, Get, Query } from "@nestjs/common";
import { Pet } from "./entities/pet";
import { FindManyOptions, FindOptionsWhere, Like, Repository } from "typeorm";
import { InjectRepository } from "@nestjs/typeorm";
@Controller("pets")
export default class AppController {
constructor(@InjectRepository(Pet) private petRepository: Repository<Pet>) {}
@Get("/repository")
async searchUsingRepository(@Query("search") search?: string) {
// Las opciones por defecto cargan la relación del Dueño
const options: FindManyOptions<Pet> = { relations: { owner: true } };
if (search?.length) {
// Si un query param fue enviado, crear un array de palabras de éste
const searchFormattedText = search.trim().split(" ");
const where: FindOptionsWhere<Pet>[] = [];
// Recorrer el arreglo y generar queries OR WHERE
for (const word of searchFormattedText) {
where.push({ owner: { firstName: Like(`%${word}%`) } });
where.push({ owner: { lastName: Like(`%${word}%`) } });
}
options.where = where;
}
const pets = await this.petRepository.find(options);
return { pets };
}
}

Lo que producirá la siguiente query

SELECT `Pet`.`id` AS `Pet_id`,
`Pet`.`owner_id` AS `Pet_owner_id`,
`Pet__Pet_owner`.`id` AS `Pet__Pet_owner_id`,
`Pet__Pet_owner`.`first_name` AS `Pet__Pet_owner_first_name`,
`Pet__Pet_owner`.`last_name` AS `Pet__Pet_owner_last_name`
FROM `pet` `Pet`
INNER JOIN `owner` `Pet__Pet_owner` ON `Pet__Pet_owner`.`id` = `Pet`.`owner_id`
WHERE ((`Pet__Pet_owner`.`first_name` LIKE ?) OR (`Pet__Pet_owner`.`last_name` LIKE ?))

🚛 Usando Query Builder desde el repositorio

Éste método es el más cercano a SQL. Útil si estás más comodo con SQL.

./app.ts
import { Controller, Get, Query } from "@nestjs/common";
import { Pet } from "./entities/pet";
import { Brackets, SelectQueryBuilder, Like, Repository } from "typeorm";
import { InjectRepository } from "@nestjs/typeorm";
@Controller("pets")
export default class AppController {
constructor(@InjectRepository(Pet) private petRepository: Repository<Pet>) {}
@Get("/querybuilder")
async searchUsingQueryBuilder(@Query("search") search?: string) {
// Así es como cargamos las relaciones
let query = this.petRepository.createQueryBuilder("p").innerJoinAndSelect("p.owner", "o");
if (search?.length) {
// Mismo objetivo, crear un array de palabras
const searchFormattedText = search.trim().split(" ");
// Así es como creamos WHERE queries OR o AND
query = query.andWhere(
new Brackets((queryPart: SelectQueryBuilder<Pet>) => {
for (const word of searchFormattedText) {
queryPart.orWhere("o.first_name like :firstName", {
firstName: `%${word}%`,
});
queryPart.orWhere("o.last_name like :lastName", {
lastName: `%${word}%`,
});
}
}),
);
}
const pets = await query.getMany();
return { pets };
}
}

La consulta SQL de la anterior función resultará como

SELECT `p`.`id` AS `p_id`,
`p`.`owner_id` AS `p_owner_id`,
`o`.`id` AS `o_id`,
`o`.`first_name` AS `o_first_name`,
`o`.`last_name` AS `o_last_name`
FROM `pet` `p`
INNER JOIN `owner` `o` ON `o`.`id` = `p`.`owner_id`
WHERE (`o`.`first_name` like ? OR `o`.`last_name` like ?)

⭐ El resultado final

Éste es el controlador final. Dentro del repositorio viene un archivo docker-compose.yml para probarlo sin instalar Node en tu computadora. Además hay una ruta /fake si quieres crear unos cuantos registros falsos de manera automatizada.

Recuerda, éste es SÓLO el controlador principal. Comprueba mi FULL demo en Github aquí

./app.ts
import { Controller, Get, Post, Query } from "@nestjs/common";
import { Pet } from "./entities/pet";
import {
Brackets,
FindManyOptions,
FindOptionsWhere,
InsertResult,
Like,
Repository,
SelectQueryBuilder,
} from "typeorm";
import { InjectRepository } from "@nestjs/typeorm";
import { Owner } from "./entities/owner";
@Controller("pets")
export class AppController {
constructor(
@InjectRepository(Pet) private petRepository: Repository<Pet>,
@InjectRepository(Owner) private ownerRepository: Repository<Owner>,
) {}
@Get("/repository")
async searchUsingRepository(@Query("search") search?: string) {
const options: FindManyOptions<Pet> = { relations: { owner: true } };
if (search?.length) {
const searchFormattedText = search.trim().split(" ");
const where: FindOptionsWhere<Pet>[] = [];
for (const word of searchFormattedText) {
where.push({ owner: { firstName: Like(`%${word}%`) } });
where.push({ owner: { lastName: Like(`%${word}%`) } });
}
options.where = where;
}
const pets = await this.petRepository.find(options);
return { pets };
}
@Get("/querybuilder")
async searchUsingQueryBuilder(@Query("search") search?: string) {
let query = this.petRepository.createQueryBuilder("p").innerJoinAndSelect("p.owner", "o");
if (search?.length) {
const searchFormattedText = search.trim().split(" ");
query = query.andWhere(
new Brackets((queryPart: SelectQueryBuilder<Pet>) => {
for (const word of searchFormattedText) {
queryPart.orWhere("o.first_name like :firstName", {
firstName: `%${word}%`,
});
queryPart.orWhere("o.last_name like :lastName", {
lastName: `%${word}%`,
});
}
}),
);
}
const pets = await query.getMany();
return { pets };
}
@Post("/fakes")
async fakes() {
const ownerPromises: Promise<InsertResult>[] = [];
let ownerCount = await this.ownerRepository.count();
for (let i = ownerCount + 1; i <= ownerCount + 100; i++) {
const owner = new Owner();
owner.firstName = `first_${i} name_${i}`;
owner.lastName = `last_${i} name_${i}`;
ownerPromises.push(this.ownerRepository.insert(owner));
}
await Promise.all(ownerPromises);
ownerCount = await this.ownerRepository.count();
const petPromises: Promise<InsertResult>[] = [];
let petCount = await this.petRepository.count();
for (let j = petCount + 1; j <= petCount + 100; j++) {
const pet = new Pet();
pet.ownerId = this.#randomInteger(1, ownerCount);
petPromises.push(this.petRepository.insert(pet));
}
await Promise.all(petPromises);
petCount = await this.petRepository.count();
return { ownerCount, petCount };
}
#randomInteger(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min) + min);
}
}
Mis posts no son generados por la IA, sin embargo, podrían estar corregidos por ella. El primer borrador siempre es de mi creación

Tags

Autor

Escrito por Helmer Davila

En otros lenguajes

Using TypeORM and NestJS

TypeORM: search in relationships (ft. NestJS)

En utiliser TypeORM et NestJS

TypeORM: effectuer une recherche sur les relations d’un modèle (NestJS inclus)

Posts relacionados

Y por qué lo usaría para mi siguiente proyecto

¿Por qué NestJS es uno de los mejores frameworks para Node?