동적 쿼리 빌딩

기본적으로 Drizzle의 모든 쿼리 빌더는 가능한 한 SQL을 따르려고 하기 때문에, 대부분의 메서드를 한 번만 호출할 수 있습니다. 예를 들어, SELECT 문에는 WHERE 절이 하나만 있을 수 있으므로 .where()를 한 번만 호출할 수 있습니다:

const query = db
	.select()
	.from(users)
	.where(eq(users.id, 1))
	.where(eq(users.name, 'John')); // ❌ Type error - where() can only be invoked once

이전 ORM 버전에서는 이러한 제한이 구현되지 않았을 때, 이 예제는 많은 사용자들에게 혼란의 원인이었습니다. 사용자들은 쿼리 빌더가 여러 .where() 호출을 단일 조건으로 “병합”할 것으로 예상했기 때문입니다.

이 동작은 기존 쿼리 빌딩, 즉 전체 쿼리를 한 번에 생성할 때 유용합니다. 그러나 동적으로 쿼리를 빌드하려고 할 때, 즉 쿼리 빌더를 받아서 향상시키는 공유 함수가 있는 경우 문제가 됩니다. 이 문제를 해결하기 위해 Drizzle은 쿼리 빌더를 위한 특별한 ‘dynamic’ 모드를 제공하며, 이는 메서드를 한 번만 호출해야 하는 제한을 제거합니다. 이를 활성화하려면 쿼리 빌더에서 .$dynamic()을 호출해야 합니다.

제공된 페이지 번호와 선택적 페이지 크기를 기반으로 쿼리에 LIMITOFFSET 절을 추가하는 간단한 withPagination 함수를 구현하여 작동 방식을 살펴보겠습니다:

function withPagination<T extends PgSelect>(
	qb: T,
	page: number = 1,
	pageSize: number = 10,
) {
	return qb.limit(pageSize).offset((page - 1) * pageSize);
}

const query = db.select().from(users).where(eq(users.id, 1));
withPagination(query, 1); // ❌ Type error - the query builder is not in dynamic mode

const dynamicQuery = query.$dynamic();
withPagination(dynamicQuery, 1); // ✅ OK

withPagination 함수는 제네릭이므로, 내부에서 쿼리 빌더의 결과 타입을 수정할 수 있습니다. 예를 들어 조인을 추가할 수 있습니다:

function withFriends<T extends PgSelect>(qb: T) {
	return qb.leftJoin(friends, eq(friends.userId, users.id));
}

let query = db.select().from(users).where(eq(users.id, 1)).$dynamic();
query = withFriends(query);

이것이 가능한 이유는 PgSelect 및 기타 유사한 타입이 동적 쿼리 빌딩에 사용되도록 특별히 설계되었기 때문입니다. 이들은 동적 모드에서만 사용할 수 있습니다.

다음은 동적 쿼리 빌딩에서 제네릭 매개변수로 사용할 수 있는 모든 타입의 목록입니다:

DialectType
QuerySelectInsertUpdateDelete
PostgresPgSelectPgInsertPgUpdatePgDelete
PgSelectQueryBuilder
MySQLMySqlSelectMySqlInsertMySqlUpdateMySqlDelete
MySqlSelectQueryBuilder
SQLiteSQLiteSelectSQLiteInsertSQLiteUpdateSQLiteDelete
SQLiteSelectQueryBuilder

...QueryBuilder 타입은 독립형 쿼리 빌더 인스턴스와 함께 사용하기 위한 것입니다. DB 쿼리 빌더는 이들의 서브클래스이므로, 동일하게 사용할 수 있습니다.

	import { QueryBuilder } from 'drizzle-orm/pg-core';

	function withFriends<T extends PgSelectQueryBuilder>(qb: T) {
		return qb.leftJoin(friends, eq(friends.userId, users.id));
	}

	const qb = new QueryBuilder();
	let query = qb.select().from(users).where(eq(users.id, 1)).$dynamic();
	query = withFriends(query);