Joins [SQL]
SQL์ Join ์ ์ ํ ์ด๋ธ ๊ฐ์ ๊ด๋ จ ์ปฌ๋ผ์ ๊ธฐ๋ฐ์ผ๋ก 2๊ฐ ์ด์์ ํ ์ด๋ธ์ ๊ฒฐํฉํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค. Drizzle ORM์ join ๊ตฌ๋ฌธ์ SQL ์ ์ฌ์ฑ๊ณผ ํ์ ์์ ์ฑ ์ฌ์ด์ ๊ท ํ์ ๋ง์ถฅ๋๋ค.
Join types
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;
};
}[];Partial select
ํน์ ํ๋ ํ์ ์งํฉ์ ์ ํํ๊ฑฐ๋ ํ๋ซ ์๋ต ํ์
์ด ํ์ํ ๊ฒฝ์ฐ, Drizzle ORM์
๋ถ๋ถ select๋ฅผ ์ง์ํ๋ join์ ์ ๊ณตํ๋ฉฐ .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์ผ ์ ์์์ ์ ์ ์๋๋ฐ, ์ด๋ left join์ ์ฌ์ฉํ๊ณ ์๊ณ pet์ด ์๋ ์ฌ์ฉ์๊ฐ ์์ ์ ์๊ธฐ ๋๋ฌธ์
๋๋ค.
๋ถ๋ถ ์ ํ ํ๋ ๋ฐ ์ง๊ณ์ 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})`,
//หwe should explicitly tell 'string | null' in type, since we're left joining that field
}).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;
}[];๋ง์ ์ปฌ๋ผ์ด ์๋ ํ ์ด๋ธ์ joinํ ๋ ์๋ง์ nullable ํ๋๋ฅผ ํผํ๊ธฐ ์ํด ์ค์ฒฉ select ๊ฐ์ฒด ๊ตฌ๋ฌธ์ ํ์ฉํ ์ ์์ต๋๋ค. ์ค๋งํธ ํ์ ์ถ๋ก ์ ๋ชจ๋ ํ ์ด๋ธ ํ๋๋ฅผ 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;
}[];Aliases & Selfjoins
Drizzle ORM์ selfjoin์ด ํ์ํ ๋ ๋งค์ฐ ์ ์ฉํ ํ ์ด๋ธ ๋ณ์นญ์ ์ง์ํฉ๋๋ค.
์ฌ์ฉ์์ ๊ทธ๋ค์ ๋ถ๋ชจ๋ฅผ ๊ฐ์ ธ์์ผ ํ๋ค๊ณ ๊ฐ์ ํด๋ด ์๋ค:
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;
}[];Aggregating results
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[];
}>;Many-to-one example
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();Many-to-many example
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),
});
// querying user group with id 1 and all the participants(users)
db.select()
.from(usersToChatGroups)
.leftJoin(users, eq(usersToChatGroups.userId, users.id))
.leftJoin(chatGroups, eq(usersToChatGroups.groupId, chatGroups.id))
.where(eq(chatGroups.id, 1))
.all();