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.
2
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';3
import { Owner } from './owner';7
@PrimaryGeneratedColumn()10
@Column({ name: 'owner_id' })13
@ManyToOne(() => Owner)14
@JoinColumn({ name: 'owner_id' })
And
2
import { Column, Entity, JoinColumn, OneToMany, PrimaryGeneratedColumn } from 'typeorm';3
import { Pet } from './pet';7
@PrimaryGeneratedColumn()10
@Column({ name: 'first_name' })13
@Column({ name: 'last_name' })16
@OneToMany(() => Pet, () => Owner)17
@JoinColumn({ name: 'owner_id' })
🔨 Creating the method for the controller
We will use NestJS. A simple NestJS controller seems like the following example.
2
export default class AppController {
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.
2
import { Controller, Get, Query } from '@nestjs/common';3
import { Pet } from './entities/pet';4
import { FindManyOptions, FindOptionsWhere, Like, Repository } from 'typeorm';5
import { InjectRepository } from '@nestjs/typeorm';8
export default class AppController {9
constructor(@InjectRepository(Pet) private petRepository: Repository<Pet>) {}12
async searchUsingRepository(@Query('search') search?: string) {14
const options: FindManyOptions<Pet> = { relations: { owner: true } };18
const searchFormattedText = search.trim().split(' ');19
const where: FindOptionsWhere<Pet>[] = [];22
for (const word of searchFormattedText) {23
where.push({ owner: { firstName: Like(`%${word}%`) } });24
where.push({ owner: { lastName: Like(`%${word}%`) } });28
options.where = where;31
const pets = await this.petRepository.find(options);
It will produce the following query
1
SELECT `Pet`.`id` AS `Pet_id`,2
`Pet`.`owner_id` AS `Pet_owner_id`,3
`Pet__Pet_owner`.`id` AS `Pet__Pet_owner_id`,4
`Pet__Pet_owner`.`first_name` AS `Pet__Pet_owner_first_name`,5
`Pet__Pet_owner`.`last_name` AS `Pet__Pet_owner_last_name`7
INNER JOIN `owner` `Pet__Pet_owner` ON `Pet__Pet_owner`.`id` = `Pet`.`owner_id`8
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.
2
import { Controller, Get, Query } from '@nestjs/common';3
import { Pet } from './entities/pet';4
import { Brackets, SelectQueryBuilder, Like, Repository } from 'typeorm';5
import { InjectRepository } from '@nestjs/typeorm';8
export default class AppController {9
constructor(@InjectRepository(Pet) private petRepository: Repository<Pet>) {}12
async searchUsingQueryBuilder(@Query('search') search?: string) {14
let query = this.petRepository.createQueryBuilder('p').innerJoinAndSelect('p.owner', 'o');18
const searchFormattedText = search.trim().split(' ');22
query = query.andWhere(23
new Brackets((queryPart: SelectQueryBuilder<Pet>) => {24
for (const word of searchFormattedText) {25
queryPart.orWhere('o.first_name like :firstName', {26
firstName: `%${word}%`,28
queryPart.orWhere('o.last_name like :lastName', {29
lastName: `%${word}%`,36
const pets = await query.getMany();
The query for the function above will result as
1
SELECT `p`.`id` AS `p_id`,2
`p`.`owner_id` AS `p_owner_id`,4
`o`.`first_name` AS `o_first_name`,5
`o`.`last_name` AS `o_last_name`7
INNER JOIN `owner` `o` ON `o`.`id` = `p`.`owner_id`8
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
2
import { Controller, Get, Post, Query } from '@nestjs/common';3
import { Pet } from './entities/pet';13
import { InjectRepository } from '@nestjs/typeorm';14
import { Owner } from './entities/owner';17
export class AppController {19
@InjectRepository(Pet) private petRepository: Repository<Pet>,20
@InjectRepository(Owner) private ownerRepository: Repository<Owner>,24
async searchUsingRepository(@Query('search') search?: string) {25
const options: FindManyOptions<Pet> = { relations: { owner: true } };28
const searchFormattedText = search.trim().split(' ');29
const where: FindOptionsWhere<Pet>[] = [];31
for (const word of searchFormattedText) {32
where.push({ owner: { firstName: Like(`%${word}%`) } });33
where.push({ owner: { lastName: Like(`%${word}%`) } });36
options.where = where;39
const pets = await this.petRepository.find(options);45
async searchUsingQueryBuilder(@Query('search') search?: string) {46
let query = this.petRepository.createQueryBuilder('p').innerJoinAndSelect('p.owner', 'o');49
const searchFormattedText = search.trim().split(' ');51
query = query.andWhere(52
new Brackets((queryPart: SelectQueryBuilder<Pet>) => {53
for (const word of searchFormattedText) {54
queryPart.orWhere('o.first_name like :firstName', {55
firstName: `%${word}%`,57
queryPart.orWhere('o.last_name like :lastName', {58
lastName: `%${word}%`,65
const pets = await query.getMany();72
const ownerPromises: Promise<InsertResult>[] = [];73
let ownerCount = await this.ownerRepository.count();75
for (let i = ownerCount + 1; i <= ownerCount + 100; i++) {76
const owner = new Owner();77
owner.firstName = `first_${i} name_${i}`;78
owner.lastName = `last_${i} name_${i}`;79
ownerPromises.push(this.ownerRepository.insert(owner));82
await Promise.all(ownerPromises);84
ownerCount = await this.ownerRepository.count();86
const petPromises: Promise<InsertResult>[] = [];87
let petCount = await this.petRepository.count();89
for (let j = petCount + 1; j <= petCount + 100; j++) {90
const pet = new Pet();91
pet.ownerId = this.#randomInteger(1, ownerCount);92
petPromises.push(this.petRepository.insert(pet));95
await Promise.all(petPromises);97
petCount = await this.petRepository.count();99
return { ownerCount, petCount };102
#randomInteger(min: number, max: number) {103
min = Math.ceil(min);104
max = Math.floor(max);105
return Math.floor(Math.random() * (max - min) + min);