조인 [SQL]
SQL의 조인 절은 테이블 간의 관련 컬럼을 기반으로 2개 이상의 테이블을 결합하는 데 사용됩니다. Drizzle ORM의 조인 문법은 SQL 유사성과 타입 안정성 사이의 균형을 맞춥니다.
조인 타입
Drizzle ORM은 INNER JOIN [LATERAL], FULL JOIN, LEFT JOIN [LATERAL], RIGHT JOIN, CROSS JOIN [LATERAL]을 위한 API를 제공합니다.
아래 테이블 스키마를 기반으로 한 예제를 살펴보겠습니다:
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
});
export const pets = pgTable('pets', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
ownerId: integer('owner_id').notNull().references(() => users.id),
})Left Join (왼쪽 조인)
const result = await db.select().from(users).leftJoin(pets, eq(users.id, pets.ownerId))select ... from "users" left join "pets" on "users"."id" = "pets"."owner_id"// result type
const result: {
user: {
id: number;
name: string;
};
pets: {
id: number;
name: string;
ownerId: number;
} | null;
}[];Left Join Lateral (왼쪽 조인 래터럴)
const subquery = db.select().from(pets).where(gte(users.age, 16)).as('userPets')
const result = await db.select().from(users).leftJoinLateral(subquery, sql`true`)select ... from "users" left join lateral (select ... from "pets" where "users"."age" >= 16) "userPets" on true// result type
const result: {
user: {
id: number;
name: string;
};
userPets: {
id: number;
name: string;
ownerId: number;
} | null;
}[];Right Join (오른쪽 조인)
const result = await db.select().from(users).rightJoin(pets, eq(users.id, pets.ownerId))select ... from "users" right join "pets" on "users"."id" = "pets"."owner_id"// result type
const result: {
user: {
id: number;
name: string;
} | null;
pets: {
id: number;
name: string;
ownerId: number;
};
}[];Inner Join (내부 조인)
const result = await db.select().from(users).innerJoin(pets, eq(users.id, pets.ownerId))select ... from "users" inner join "pets" on "users"."id" = "pets"."owner_id"// result type
const result: {
user: {
id: number;
name: string;
};
pets: {
id: number;
name: string;
ownerId: number;
};
}[];Inner Join Lateral (내부 조인 래터럴)
const subquery = db.select().from(pets).where(gte(users.age, 16)).as('userPets')
const result = await db.select().from(users).innerJoinLateral(subquery, sql`true`)select ... from "users" inner join lateral (select ... from "pets" where "users"."age" >= 16) "userPets" on true// result type
const result: {
user: {
id: number;
name: string;
};
userPets: {
id: number;
name: string;
ownerId: number;
};
}[];Full Join (전체 조인)
const result = await db.select().from(users).fullJoin(pets, eq(users.id, pets.ownerId))select ... from "users" full join "pets" on "users"."id" = "pets"."owner_id"// result type
const result: {
user: {
id: number;
name: string;
} | null;
pets: {
id: number;
name: string;
ownerId: number;
} | null;
}[];Cross Join (교차 조인)
const result = await db.select().from(users).crossJoin(pets)select ... from "users" cross join "pets"// result type
const result: {
user: {
id: number;
name: string;
};
pets: {
id: number;
name: string;
ownerId: number;
};
}[];Cross Join Lateral (교차 조인 래터럴)
const subquery = db.select().from(pets).where(gte(users.age, 16)).as('userPets')
const result = await db.select().from(users).crossJoinLateral(subquery)select ... from "users" cross join lateral (select ... from "pets" where "users"."age" >= 16) "userPets"// result type
const result: {
user: {
id: number;
name: string;
};
userPets: {
id: number;
name: string;
ownerId: number;
};
}[];부분 선택
특정 필드의 하위 집합을 선택하거나 플랫한 응답 타입을 원한다면, Drizzle ORM은
부분 선택을 사용한 조인을 지원하며 .select({ ... }) 구조를 기반으로 반환 타입을 자동으로 추론합니다.
await db.select({
userId: users.id,
petId: pets.id,
}).from(user).leftJoin(pets, eq(users.id, pets.ownerId))select "users"."id", "pets"."id" from "users" left join "pets" on "users"."id" = "pets"."owner_id"// result type
const result: {
userId: number;
petId: number | null;
}[];petId가 이제 null이 될 수 있다는 것을 눈치챘을 것입니다. 이는 왼쪽 조인을 사용하고 있어서 반려동물이 없는 사용자가 있을 수 있기 때문입니다.
부분 선택 필드와 집계를 위해 sql 연산자를 사용할 때 매우 중요한 점은,
적절한 결과 타입 추론을 위해 sql<type | null>을 사용해야 한다는 것입니다. 이것은 여러분이 직접 처리해야 합니다!
const result = await db.select({
userId: users.id,
petId: pets.id,
petName1: sql`upper(${pets.name})`,
petName2: sql<string | null>`upper(${pets.name})`,
//˄해당 필드를 왼쪽 조인하고 있으므로, 타입에 'string | null'을 명시적으로 지정해야 합니다
}).from(user).leftJoin(pets, eq(users.id, pets.ownerId))select "users"."id", "pets"."id", upper("pets"."name")... from "users" left join "pets" on "users"."id" = "pets"."owner_id"// result type
const result: {
userId: number;
petId: number | null;
petName1: unknown;
petName2: string | null;
}[];많은 컬럼이 있는 테이블을 조인할 때 수많은 nullable 필드를 피하기 위해 중첩된 선택 객체 문법을 활용할 수 있습니다. 스마트 타입 추론은 모든 테이블 필드를 nullable로 만드는 대신 전체 객체를 nullable로 만듭니다!
await db.select({
userId: users.id,
userName: users.name,
pet: {
id: pets.id,
name: pets.name,
upperName: sql<string>`upper(${pets.name})`
}
}).from(user).fullJoin(pets, eq(users.id, pets.ownerId))select ... from "users" full join "pets" on "users"."id" = "pets"."owner_id"// result type
const result: {
userId: number | null;
userName: string | null;
pet: {
id: number;
name: string;
upperName: string;
} | null;
}[];별칭 & 셀프 조인
Drizzle ORM은 테이블 별칭을 지원하며, 셀프 조인을 수행해야 할 때 매우 유용합니다.
사용자와 그들의 부모를 함께 조회해야 한다고 가정해봅시다:
import { user } from "./schema";
const parent = alias(user, "parent");
const result = db
.select()
.from(user)
.leftJoin(parent, eq(parent.id, user.parentId));select ... from "user" left join "user" "parent" on "parent"."id" = "user"."parent_id"// result type
const result: {
user: {
id: number;
name: string;
parentId: number;
};
parent: {
id: number;
name: string;
parentId: number;
} | null;
}[];결과 집계
Drizzle ORM은 드라이버에서 이름이 매핑된 결과를 구조를 변경하지 않고 전달합니다.
원하는 방식으로 결과를 자유롭게 처리할 수 있으며, 다음은 다대일 관계 데이터를 매핑하는 예제입니다:
type User = typeof users.$inferSelect;
type Pet = typeof pets.$inferSelect;
const rows = db.select({
user: users,
pet: pets,
}).from(users).leftJoin(pets, eq(users.id, pets.ownerId)).all();
const result = rows.reduce<Record<number, { user: User; pets: Pet[] }>>(
(acc, row) => {
const user = row.user;
const pet = row.pet;
if (!acc[user.id]) {
acc[user.id] = { user, pets: [] };
}
if (pet) {
acc[user.id].pets.push(pet);
}
return acc;
},
{}
);
// result type
const result: Record<number, {
user: User;
pets: Pet[];
}>;다대일 예제
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { drizzle } from 'drizzle-orm/better-sqlite3';
const cities = sqliteTable('cities', {
id: integer('id').primaryKey(),
name: text('name'),
});
const users = sqliteTable('users', {
id: integer('id').primaryKey(),
name: text('name'),
cityId: integer('city_id').references(() => cities.id)
});
const db = drizzle();
const result = db.select().from(cities).leftJoin(users, eq(cities.id, users.cityId)).all();다대다 예제
const users = sqliteTable('users', {
id: integer('id').primaryKey(),
name: text('name'),
});
const chatGroups = sqliteTable('chat_groups', {
id: integer('id').primaryKey(),
name: text('name'),
});
const usersToChatGroups = sqliteTable('usersToChatGroups', {
userId: integer('user_id').notNull().references(() => users.id),
groupId: integer('group_id').notNull().references(() => chatGroups.id),
});
// id가 1인 사용자 그룹과 모든 참가자(사용자) 조회
db.select()
.from(usersToChatGroups)
.leftJoin(users, eq(usersToChatGroups.userId, users.id))
.leftJoin(chatGroups, eq(usersToChatGroups.groupId, chatGroups.id))
.where(eq(chatGroups.id, 1))
.all();