
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.
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
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.
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.
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í
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); }}