TypeORM: search in relationships (ft. NestJS)

TypeORM: search in relationships (ft. NestJS)

🤔 Why?

Because I didn’t want to perform a full search once I brought all the records. Since SQL is really optimized for queries. For this case, I wanted to filter some records based on their relationship attributes.

I’ll use a simple test case.

💡 First, we need two TypeORM entities

For the most simple of cases, let’s suppose we have two classes. A Pet and an Owner. A Pet will belong to only one Owner. Both entities live in the same folder.

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

And

./entities/owner.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[];
}

🔨 Creating the method for the controller

We will use NestJS. A simple NestJS controller seems like the following example.

@Controller("pets")
export default class AppController {
@Get()
async search() {
// TODO: Implement search
}
}

As you see here. The URL for the search will be /pets/search

🚛 Using Repositories

This is the method Typescript friendly. I prefer this since I’ll have type validation if I update any field in the future.

./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) {
// By default we load the relationship in the default options
const options: FindManyOptions<Pet> = { relations: { owner: true } };
if (search?.length) {
// if search request query param was sent. Create an array of words
const searchFormattedText = search.trim().split(" ");
const where: FindOptionsWhere<Pet>[] = [];
// Create OR WHERE queries by each word into first and last name
for (const word of searchFormattedText) {
where.push({ owner: { firstName: Like(`%${word}%`) } });
where.push({ owner: { lastName: Like(`%${word}%`) } });
}
// Adding those where to options
options.where = where;
}
const pets = await this.petRepository.find(options);
return { pets };
}
}

It will produce the following 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 ?))

🚛 Using Query Builder from repositories

This method is more close to SQL. Useful if you’re more comfortable with it.

./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) {
// This is how we load relationships here
let query = this.petRepository.createQueryBuilder("p").innerJoinAndSelect("p.owner", "o");
if (search?.length) {
// By default we load relationship in the default options
const searchFormattedText = search.trim().split(" ");
// With query builder we specify the AND or OR queries
// Brackets is a way to group queries within a ( )
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 };
}
}

The query for the function above will result as

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

⭐ Final result

This is the final controller. I’ll add a docker-compose file if you want to try it by yourself. Also, there is a /fake method to create records if you want to test the search.

Remember, this is ONLY the main controller. Check my FULL demo on Github here

./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);
}
}
My posts are not AI generated, they might be only AI corrected. The first draft is always my creation

Tags

Author

Written by Helmer Davila

In other languages

Usando TypeORM y NestJS

TypeORM: Búsqueda relacional (NestJS incluido)

En utiliser TypeORM et NestJS

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

Related posts

And why I would choose it for my next project

Why Nest JS is one of the best Node backend frameworks?