๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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);
}

์กฐ์ธ ์กฐ๊ฑด ๊ทธ๋ฃนํ•‘

๋ณต์žกํ•œ ์กฐ์ธ ์กฐ๊ฑด์„ ๊ทธ๋ฃน์œผ๋กœ ๋ฌถ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
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๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ๋‹จ๊ณ„