Claude Skills Guide

Mastering TypeORM Query Builder: Advanced Patterns for Complex Database Operations

The TypeORM Query Builder is one of the most powerful tools in the TypeORM ecosystem, allowing developers to construct complex database queries programmatically. While basic CRUD operations are straightforward, mastering advanced Query Builder patterns will elevate your application’s data access layer to enterprise-grade quality. This guide explores sophisticated techniques for building dynamic, efficient, and maintainable database queries.

Understanding the Query Builder Foundation

Before diving into advanced patterns, let’s establish a solid foundation. The Query Builder in TypeORM allows you to build SQL queries using a fluent, chainable API. Unlike traditional repositories that abstract away SQL complexity, the Query Builder gives you fine-grained control over every aspect of your queries.

const users = await dataSource
  .getRepository(User)
  .createQueryBuilder("user")
  .where("user.age > :age", { age: 18 })
  .getMany();

This basic pattern forms the backbone of all advanced techniques we’ll explore.

Subqueries: Powerful Nested Data Retrieval

Subqueries allow you to nest queries within queries, enabling complex data filtering and aggregation that would be difficult or impossible with simple JOINs. TypeORM’s Query Builder makes subqueries intuitive and type-safe.

Correlated Subqueries for Row-Specific Analysis

Correlated subqueries reference the outer query’s table, enabling row-by-row analysis. This pattern is invaluable for finding records that meet conditions relative to other records in the same dataset.

const usersWithLatestOrder = await dataSource
  .getRepository(User)
  .createQueryBuilder("user")
  .where((qb) => {
    const subQuery = qb
      .subQuery()
      .select("MAX(order.createdAt)")
      .from(Order, "order")
      .where("order.userId = user.id")
      .getQuery();
    return `user.lastOrderDate = ${subQuery}`;
  })
  .getMany();

This query finds all users whose last order matches the maximum order date for each user—a perfect example of correlated subquery power.

Subqueries in WHERE Clauses for Complex Filtering

You can also use subqueries directly in WHERE conditions to filter based on aggregated data:

const highValueCustomers = await dataSource
  .getRepository(User)
  .createQueryBuilder("user")
  .where("user.id IN " + 
    dataSource
      .getRepository(Order)
      .createQueryBuilder("order")
      .select("order.userId")
      .groupBy("order.userId")
      .having("SUM(order.amount) > :threshold", { threshold: 10000 })
      .getQuery()
  )
  .getMany();

This finds all customers whose total order amount exceeds $10,000.

Advanced Join Patterns

Joins are essential for combining data from multiple tables, but TypeORM offers advanced patterns beyond simple inner joins.

Left Join with Custom Conditions

Sometimes you need LEFT JOINs with specific conditions beyond the default relationship mapping:

const usersWithPendingOrders = await dataSource
  .getRepository(User)
  .createQueryBuilder("user")
  .leftJoinAndSelect("user.orders", "order", "order.status = :status", { status: "pending" })
  .loadRelationCountAndMap("user.pendingOrderCount", "user.orders", "order", (qb) =>
    qb.where("order.status = :status", { status: "pending" })
  )
  .getMany();

This retrieves all users with their pending orders (if any) and counts how many pending orders each user has.

Multiple Joins with Aliases

For complex reporting queries, you often need to join the same table multiple times with different conditions:

const userActivityReport = await dataSource
  .getRepository(User)
  .createQueryBuilder("user")
  .leftJoin("user.orders", "completedOrders", "completedOrders.status = 'completed'")
  .leftJoin("user.orders", "returnedOrders", "returnedOrders.status = 'returned'")
  .select([
    "user.id",
    "user.name",
    "COUNT(completedOrders.id) as completedCount",
    "COUNT(returnedOrders.id) as returnedCount"
  ])
  .groupBy("user.id")
  .addGroupBy("user.name")
  .getRawMany();

This pattern is particularly useful for analytics dashboards where you need to aggregate the same related entity under different conditions.

Dynamic Query Construction

Building queries conditionally based on runtime parameters is a common requirement. The Query Builder excels at this through its fluent API.

Conditional Where Clauses

Build WHERE clauses dynamically based on input parameters:

function buildUserSearchQuery(
  builder: SelectQueryBuilder<User>,
  filters: UserSearchFilters
): SelectQueryBuilder<User> {
  if (filters.minAge) {
    builder = builder.andWhere("user.age >= :minAge", { minAge: filters.minAge });
  }
  
  if (filters.maxAge) {
    builder = builder.andWhere("user.age <= :maxAge", { maxAge: filters.maxAge });
  }
  
  if (filters.name) {
    builder = builder.andWhere("LOWER(user.name) LIKE :name", { 
      name: `%${filters.name.toLowerCase()}%` 
    });
  }
  
  if (filters.status) {
    builder = builder.andWhere("user.status = :status", { status: filters.status });
  }
  
  return builder;
}

This pattern allows complete flexibility while maintaining query safety through parameterization.

Dynamic Ordering and Pagination

Implement flexible sorting and pagination:

function applySortingAndPagination<T>(
  builder: SelectQueryBuilder<T>,
  options: QueryOptions
): SelectQueryBuilder<T> {
  const validSortFields = ["createdAt", "updatedAt", "name", "id"];
  const sortField = validSortFields.includes(options.sortBy) ? options.sortBy : "createdAt";
  const sortOrder = options.sortOrder === "ASC" ? "ASC" : "DESC";
  
  return builder
    .orderBy(sortField, sortOrder)
    .skip((options.page - 1) * options.limit)
    .take(options.limit);
}

Transactional Query Building

For operations requiring multiple queries to succeed or fail together, TypeORM’s Query Builder integrates smoothly with transactions:

await dataSource.transaction(async (manager) => {
  // Update user balance
  await manager
    .createQueryBuilder()
    .update(User)
    .set({ balance: () => "balance - :amount" })
    .where("id = :userId", { userId })
    .execute();
  
  // Record transaction
  await manager
    .createQueryBuilder()
    .insert()
    .into(Transaction)
    .values({
      userId,
      amount,
      type: "debit",
      createdAt: new Date()
    })
    .execute();
});

Performance Optimization Patterns

Eager Loading vs Lazy Loading

Choose your loading strategy based on your use case. Eager loading is perfect for known relationships, while lazy loading suits unpredictable access patterns:

// Eager loading - always loads relations
const users = await userRepo.find({ 
  relations: ["orders", "profile"],
  where: { active: true }
});

// Lazy loading via Query Builder - more control
const users = await userRepo
  .createQueryBuilder("user")
  .leftJoinAndSelect("user.orders", "orders")
  .where("user.active = :active", { active: true })
  .andWhere("orders.createdAt > :date", { date: thirtyDaysAgo })
  .getMany();

Query Result Caching

For frequently accessed, rarely changing data, use TypeORM’s built-in caching:

const cachedResults = await dataSource
  .getRepository(Product)
  .createQueryBuilder("product")
  .where("product.category = :category", { category: "electronics" })
  .setCacheable(true)
  .setCacheId("electronics-products-list")
  .getMany();

Enable query caching in your DataSource configuration for significant performance gains on read-heavy applications.

Conclusion

The TypeORM Query Builder transforms database operations from rigid SQL strings into flexible, type-safe, and maintainable code. By mastering these advanced patterns—subqueries, complex joins, dynamic construction, transactions, and optimization techniques—you’ll build data access layers that are both powerful and elegant. Start implementing these patterns in your projects today, and experience the difference enterprise-grade database code makes in application reliability and maintainability.

Built by theluckystrike — More at zovo.one