join๊ณผ leftJoin์ ์ฌ๋ฌ ํ
์ด๋ธ์ ์ฐ๊ฒฐํ์ฌ ๊ด๊ณ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ๋ฉ์๋์
๋๋ค. ํ์
์์ ํ๊ฒ ํ
์ด๋ธ์ ์กฐ์ธํ๊ณ , ๋ณต์กํ ์กฐ์ธ ์กฐ๊ฑด์ ์ง์ํฉ๋๋ค.
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
INNER JOIN
const posts = await puri.table("posts")
.join("users", "posts.user_id", "users.id")
.select({
post_id: "posts.id",
title: "posts.title",
author_name: "users.name"
});
// SELECT posts.id as post_id, posts.title, users.name as author_name
// FROM posts
// INNER JOIN users ON posts.user_id = users.id
LEFT JOIN
const posts = await puri.table("posts")
.leftJoin("users", "posts.user_id", "users.id")
.select({
post_id: "posts.id",
title: "posts.title",
author_name: "users.name" // null ๊ฐ๋ฅ
});
// SELECT posts.id as post_id, posts.title, users.name as author_name
// FROM posts
// LEFT JOIN users ON posts.user_id = users.id
INNER JOIN: ์์ชฝ ํ
์ด๋ธ ๋ชจ๋์ ๋งค์นญ๋๋ ๋ ์ฝ๋๋ง ๋ฐํ
LEFT JOIN: ์ผ์ชฝ ํ
์ด๋ธ์ ๋ชจ๋ ๋ ์ฝ๋ ๋ฐํ (์ค๋ฅธ์ชฝ์ null ๊ฐ๋ฅ)
ํ
์ด๋ธ ๋ณ์นญ (Alias)
๋ณ์นญ ์ฌ์ฉ
const posts = await puri.table("posts")
.join({ u: "users" }, "posts.user_id", "u.id")
.select({
post_id: "posts.id",
author_name: "u.name" // ๋ณ์นญ ์ฌ์ฉ
});
// FROM posts
// INNER JOIN users AS u ON posts.user_id = u.id
๊ฐ์ ํ
์ด๋ธ ์ฌ๋ฌ ๋ฒ ์กฐ์ธ
const posts = await puri.table("posts")
.join({ author: "users" }, "posts.author_id", "author.id")
.leftJoin({ editor: "users" }, "posts.editor_id", "editor.id")
.select({
post_id: "posts.id",
author_name: "author.name",
editor_name: "editor.name" // null ๊ฐ๋ฅ
});
// FROM posts
// INNER JOIN users AS author ON posts.author_id = author.id
// LEFT JOIN users AS editor ON posts.editor_id = editor.id
๋ณต์กํ ์กฐ์ธ ์กฐ๊ฑด
์ฝ๋ฐฑ ํจ์ ์ฌ์ฉ
์ฌ๋ฌ ์กฐ๊ฑด์ ์กฐํฉํ ์ ์์ต๋๋ค.
const result = await puri.table("orders")
.join("users", (j) => {
j.on("orders.user_id", "users.id")
.on("orders.country", "users.country");
})
.select({
order_id: "orders.id",
user_name: "users.name"
});
// FROM orders
// INNER JOIN users ON orders.user_id = users.id
// AND orders.country = users.country
OR ์กฐ๊ฑด
const result = await puri.table("products")
.leftJoin("categories", (j) => {
j.on("products.category_id", "categories.id")
.orOn("products.parent_category_id", "categories.id");
})
.select({
product_name: "products.name",
category_name: "categories.name"
});
// LEFT JOIN categories ON products.category_id = categories.id
// OR products.parent_category_id = categories.id
์กฐ๊ฑด๊ฐ๊ณผ ๋น๊ต
const result = await puri.table("orders")
.join("users", (j) => {
j.on("orders.user_id", "users.id")
.on("users.status", "=", "active") // ๊ณ ์ ๊ฐ ๋น๊ต
.on("orders.created_at", ">", new Date("2024-01-01"));
})
.selectAll();
์๋ธ์ฟผ๋ฆฌ ์กฐ์ธ
์๋ธ์ฟผ๋ฆฌ๋ฅผ ํ
์ด๋ธ์ฒ๋ผ ์กฐ์ธํ ์ ์์ต๋๋ค.
// ์๋ธ์ฟผ๋ฆฌ ์์ฑ
const recentOrders = puri.table("orders")
.where("orders.created_at", ">", new Date("2024-01-01"))
.select({
user_id: "orders.user_id",
order_count: Puri.count()
})
.groupBy("orders.user_id");
// ์๋ธ์ฟผ๋ฆฌ ์กฐ์ธ
const result = await puri.table("users")
.join(
{ ro: recentOrders }, // ๋ณ์นญ ํ์
"users.id",
"ro.user_id"
)
.select({
user_id: "users.id",
user_name: "users.name",
order_count: "ro.order_count"
});
// FROM users
// INNER JOIN (
// SELECT user_id, COUNT(*) as order_count
// FROM orders
// WHERE created_at > '2024-01-01'
// GROUP BY user_id
// ) AS ro ON users.id = ro.user_id
์ฌ๋ฌ ํ
์ด๋ธ ์กฐ์ธ
์์ฐจ์ ์ผ๋ก ์ฌ๋ฌ ํ
์ด๋ธ์ ์กฐ์ธํ ์ ์์ต๋๋ค.
const posts = await puri.table("posts")
.join("users", "posts.user_id", "users.id")
.leftJoin("categories", "posts.category_id", "categories.id")
.leftJoin("tags", "posts.id", "tags.post_id")
.select({
// posts
post_id: "posts.id",
title: "posts.title",
// users
author_name: "users.name",
author_email: "users.email",
// categories (nullable)
category_name: "categories.name",
// tags (nullable)
tag_name: "tags.name"
});
// FROM posts
// INNER JOIN users ON posts.user_id = users.id
// LEFT JOIN categories ON posts.category_id = categories.id
// LEFT JOIN tags ON posts.id = tags.post_id
์ค์ ์์
async function getPostsWithAuthors() {
return puri.table("posts")
.join("users", "posts.user_id", "users.id")
.where("posts.published", true)
.select({
// ๊ฒ์๋ฌผ ์ ๋ณด
id: "posts.id",
title: "posts.title",
content: "posts.content",
created_at: "posts.created_at",
// ์์ฑ์ ์ ๋ณด
author: {
id: "users.id",
name: "users.name",
email: "users.email"
}
})
.orderBy("posts.created_at", "desc")
.limit(20);
}
async function getOrderDetails(orderId: number) {
return puri.table("orders")
.join("users", "orders.user_id", "users.id")
.join("order_items", "orders.id", "order_items.order_id")
.join("products", "order_items.product_id", "products.id")
.where("orders.id", orderId)
.select({
// ์ฃผ๋ฌธ ์ ๋ณด
order_id: "orders.id",
order_status: "orders.status",
order_date: "orders.created_at",
// ์ฌ์ฉ์ ์ ๋ณด
customer: {
name: "users.name",
email: "users.email",
phone: "users.phone"
},
// ์ํ ์ ๋ณด
items: {
product_id: "products.id",
product_name: "products.name",
quantity: "order_items.quantity",
price: "order_items.price"
}
});
}
async function getUserStatistics() {
return puri.table("users")
.leftJoin("posts", "users.id", "posts.user_id")
.leftJoin("comments", "users.id", "comments.user_id")
.select({
user_id: "users.id",
user_name: "users.name",
// ๊ฒ์๋ฌผ ์
post_count: Puri.rawNumber(
"COUNT(DISTINCT posts.id)"
),
// ๋๊ธ ์
comment_count: Puri.rawNumber(
"COUNT(DISTINCT comments.id)"
),
// ๋ง์ง๋ง ํ๋
last_activity: Puri.rawDate(
"GREATEST(MAX(posts.created_at), MAX(comments.created_at))"
)
})
.groupBy("users.id", "users.name")
.having("post_count", ">", 0)
.orderBy("post_count", "desc");
}
async function getUsersWithManagers() {
return puri.table({ emp: "users" })
.leftJoin(
{ mgr: "users" },
"emp.manager_id",
"mgr.id"
)
.select({
// ์ง์ ์ ๋ณด
employee_id: "emp.id",
employee_name: "emp.name",
employee_email: "emp.email",
// ๋งค๋์ ์ ๋ณด (nullable)
manager: {
id: "mgr.id",
name: "mgr.name",
email: "mgr.email"
}
})
.orderBy("emp.name", "asc");
}
async function getTopContributors(limit: number = 10) {
// ์ฌ์ฉ์๋ณ ๊ฒ์๋ฌผ ์ ๊ณ์ฐ (์๋ธ์ฟผ๋ฆฌ)
const postCounts = puri.table("posts")
.where("posts.published", true)
.select({
user_id: "posts.user_id",
post_count: Puri.count()
})
.groupBy("posts.user_id");
// ์ฌ์ฉ์ ์ ๋ณด์ ์กฐ์ธ
return puri.table("users")
.join(
{ pc: postCounts },
"users.id",
"pc.user_id"
)
.select({
id: "users.id",
name: "users.name",
email: "users.email",
post_count: "pc.post_count"
})
.orderBy("pc.post_count", "desc")
.limit(limit);
}
์กฐ์ธ ์กฐ๊ฑด ๊ทธ๋ฃนํ
๋ณต์กํ ์กฐ์ธ ์กฐ๊ฑด์ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ ์ ์์ต๋๋ค.
const result = await puri.table("products")
.leftJoin("discounts", (j) => {
j.on("products.id", "discounts.product_id")
.on((nested) => {
nested.on("discounts.start_date", "<=", new Date())
.on("discounts.end_date", ">=", new Date());
});
})
.select({
product_id: "products.id",
product_name: "products.name",
discount_rate: "discounts.rate"
});
// LEFT JOIN discounts ON products.id = discounts.product_id
// AND (
// discounts.start_date <= NOW()
// AND discounts.end_date >= NOW()
// )
NULL ์ฒ๋ฆฌ (LEFT JOIN)
LEFT JOIN ๊ฒฐ๊ณผ๋ nullable ํ์
์ผ๋ก ์๋ ์ถ๋ก ๋ฉ๋๋ค.
const posts = await puri.table("posts")
.leftJoin("users", "posts.user_id", "users.id")
.select({
post_id: "posts.id",
title: "posts.title",
author_name: "users.name" // string | null
});
// ํ์
์ฒดํฌ
posts.forEach(post => {
if (post.author_name !== null) {
console.log(post.author_name.toUpperCase());
}
});
์ฑ๋ฅ ์ต์ ํ
1. ํ์ํ ์ปฌ๋ผ๋ง ์ ํ
// โ ๋์จ: ๋ชจ๋ ์ปฌ๋ผ
.join("users", ...)
.selectAll()
// โ
์ข์: ํ์ํ ์ปฌ๋ผ๋ง
.join("users", ...)
.select({
post_id: "posts.id",
author_name: "users.name" // ํ์ํ ๊ฒ๋ง
})
2. ์กฐ์ธ ์์
// โ
์ข์: ์์ ํ
์ด๋ธ ๋จผ์ ์กฐ์ธ
.table("posts") // ํฐ ํ
์ด๋ธ
.join("users", ...) // ์์ ํ
์ด๋ธ
.join("categories", ...)
// ์กฐ์ธ ์กฐ๊ฑด์ ์ธ๋ฑ์ค ์ฌ์ฉ
.join("users", "posts.user_id", "users.id") // users.id๋ PK (์ธ๋ฑ์ค)
3. LEFT JOIN vs INNER JOIN
// LEFT JOIN์ INNER JOIN๋ณด๋ค ๋๋ฆผ
// ๋ฐ๋์ ์กด์ฌํ๋ ๊ด๊ณ๋ฉด INNER JOIN ์ฌ์ฉ
// โ
user_id๊ฐ NOT NULL์ด๋ฉด INNER JOIN
.join("users", "posts.user_id", "users.id")
// โ ๏ธ user_id๊ฐ NULL ๊ฐ๋ฅํ๋ฉด LEFT JOIN
.leftJoin("users", "posts.user_id", "users.id")
ํ์
์์ ์ฑ
์กฐ์ธํ ํ
์ด๋ธ์ ์ปฌ๋ผ์ ํ์
์๋ ์์ฑ๊ณผ ๊ฒ์ฆ์ ์ง์ํฉ๋๋ค.
const posts = await puri.table("posts")
.join("users", "posts.user_id", "users.id")
.select({
post_id: "posts.id", // โ
OK
user_name: "users.name", // โ
OK
// โ ํ์
์๋ฌ: ์กด์ฌํ์ง ์๋ ์ปฌ๋ผ
unknown: "posts.unknown_field",
// โ ํ์
์๋ฌ: ์กฐ์ธํ์ง ์์ ํ
์ด๋ธ
category: "categories.name"
});
์ฃผ์์ฌํญ
1. ๋ณ์นญ ํ์ (์๋ธ์ฟผ๋ฆฌ)
// โ ์๋ฌ: ์๋ธ์ฟผ๋ฆฌ๋ ๋ณ์นญ ํ์
.join(subquery, ...)
// โ
์ฌ๋ฐ๋ฆ
.join({ sq: subquery }, ...)
2. ์กฐ์ธ ์กฐ๊ฑด ํ
์ด๋ธ ํ์ธ
// โ ์๋ชป๋จ: ์กฐ์ธํ์ง ์์ ํ
์ด๋ธ ์ฐธ์กฐ
.table("posts")
.join("users", "comments.user_id", "users.id") // comments ํ
์ด๋ธ ์์
// โ
์ฌ๋ฐ๋ฆ
.table("posts")
.join("users", "posts.user_id", "users.id")
3. ์ค๋ณต ์ปฌ๋ผ๋ช
// โ ๏ธ ์ฃผ์: ์ปฌ๋ผ๋ช
์ถฉ๋
.table("posts")
.join("users", ...)
.select({
id: "posts.id", // โ ๋์ค์ ๋ฎ์ด์์์ง
id: "users.id" // โ
})
// โ
์ฌ๋ฐ๋ฆ: ๋ณ์นญ ์ฌ์ฉ
.select({
post_id: "posts.id",
user_id: "users.id"
})
4. N+1 ๋ฌธ์
// โ ๋์จ: N+1 ์ฟผ๋ฆฌ
for (const post of posts) {
const user = await puri.table("users")
.where("users.id", post.user_id)
.first();
}
// โ
์ข์: JOIN์ผ๋ก ํ ๋ฒ์
const posts = await puri.table("posts")
.join("users", "posts.user_id", "users.id")
.select({
post_id: "posts.id",
user_name: "users.name"
});
JOIN vs Loader
Puri๋ ๋ ๊ฐ์ง ๊ด๊ณ ๋ฐ์ดํฐ ๋ก๋ฉ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค.
JOIN (1:1 ๊ด๊ณ)
// โ
์ ํฉ: 1:1 ๋๋ N:1
.table("posts")
.join("users", "posts.user_id", "users.id")
Loader (1:N ๊ด๊ณ)
// โ
์ ํฉ: 1:N ๋๋ N:M
// Subset Loader๋ก ์ฒ๋ฆฌ (๋ณ๋ ์ฟผ๋ฆฌ)
JOIN์ ํ์ด ๊ณฑํด์ง๋ฏ๋ก, 1:N ๊ด๊ณ์๋ Loader๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๋ค์ ๋จ๊ณ