๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Puri๋Š” ํƒ€์ž… ์•ˆ์ „ํ•œ ์ฟผ๋ฆฌ ๋นŒ๋”์ด์ง€๋งŒ, ๋ณต์žกํ•œ SQL ํ‘œํ˜„์‹์ด ํ•„์š”ํ•  ๋•Œ๋Š” Raw SQL์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Raw ํ•จ์ˆ˜ ๊ฐœ์š”

Raw ํƒ€์ž… ํ•จ์ˆ˜

ํƒ€์ž…๋ณ„ Raw ํ•จ์ˆ˜ rawString, rawNumber

WHERE Raw

๋ณต์žกํ•œ ์กฐ๊ฑด๋ฌธ whereRaw

CASE WHEN

์กฐ๊ฑด๋ถ€ ๊ฐ’ ์„ ํƒ CASE ํ‘œํ˜„์‹

์„œ๋ธŒ์ฟผ๋ฆฌ

์ค‘์ฒฉ ์ฟผ๋ฆฌ ์ž‘์„ฑ Subquery

Raw ํƒ€์ž… ํ•จ์ˆ˜

rawString - ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜

const results = await db.table("users").select({
  id: "id",
  fullName: Puri.rawString("CONCAT(first_name, ' ', last_name)"),
  upperName: Puri.upper("username"),
  lowerEmail: Puri.lower("email"),
});

// ํƒ€์ž…: { id: number; fullName: string; upperName: string; lowerEmail: string; }[]

rawNumber - ์ˆซ์ž ๋ฐ˜ํ™˜

const results = await db.table("employees").select({
  id: "id",
  salary: "salary",
  yearsSince: Puri.rawNumber("EXTRACT(YEAR FROM AGE(NOW(), hire_date))"),
  roundedSalary: Puri.rawNumber("ROUND(salary, -3)"),
});

// ํƒ€์ž…: { id: number; salary: string; yearsSince: number; roundedSalary: number; }[]

rawBoolean - ๋ถˆ๋ฆฐ ๋ฐ˜ํ™˜

const results = await db.table("users").select({
  id: "id",
  isActive: "is_active",
  isAdmin: Puri.rawBoolean("role = 'admin'"),
  hasEmail: Puri.rawBoolean("email IS NOT NULL"),
});

// ํƒ€์ž…: { id: number; isActive: boolean; isAdmin: boolean; hasEmail: boolean; }[]

rawDate - ๋‚ ์งœ ๋ฐ˜ํ™˜

const results = await db.table("users").select({
  id: "id",
  createdAt: "created_at",
  nextWeek: Puri.rawDate("created_at + INTERVAL '7 days'"),
});

// ํƒ€์ž…: { id: number; createdAt: Date; nextWeek: Date; }[]

rawStringArray - ๋ฌธ์ž์—ด ๋ฐฐ์—ด ๋ฐ˜ํ™˜

const results = await db
  .table("projects")
  .join("projects__employees", "projects.id", "projects__employees.project_id")
  .join("users", "projects__employees.employee_id", "users.id")
  .select({
    projectId: "projects.id",
    memberNames: Puri.rawStringArray("ARRAY_AGG(users.username)"),
  })
  .groupBy("projects.id");

// ํƒ€์ž…: { projectId: number; memberNames: string[]; }[]

Static SQL ํ•จ์ˆ˜

Puri๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋‚ด์žฅ SQL ํ•จ์ˆ˜๋“ค์ž…๋‹ˆ๋‹ค.

๋ฌธ์ž์—ด ํ•จ์ˆ˜

const results = await db
  .table("users")
  .select({
    fullName: Puri.concat("first_name", "' '", "last_name"),
  });

์ง‘๊ณ„ ํ•จ์ˆ˜

const results = await db.table("employees").select({
  total: Puri.count("id"),
  totalSalary: Puri.sum("salary"),
  avgSalary: Puri.avg("salary"),
  maxSalary: Puri.max("salary"),
  minSalary: Puri.min("salary"),
});

WHERE Raw

๋ณต์žกํ•œ WHERE ์กฐ๊ฑด์„ ์ง์ ‘ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ WHERE Raw

const results = await db
  .table("employees")
  .select({ id: "id", salary: "salary" })
  .whereRaw("salary > ?", [50000])
  .whereRaw("EXTRACT(YEAR FROM hire_date) = ?", [2023]);
SQL Injection ์ฃผ์˜: whereRaw์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ ๋ฐ”์ธ๋”ฉ(?)์„ ์‚ฌ์šฉํ•˜์„ธ์š”. ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ง์ ‘ ๋ฌธ์ž์—ด์— ๋„ฃ์œผ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.

๋ณต์žกํ•œ ์กฐ๊ฑด

const results = await db
  .table("employees")
  .select({ id: "id", name: "username" })
  .whereRaw(
    `
    (department_id = ? AND salary > ?)
    OR (department_id = ? AND salary > ?)
  `,
    [1, 60000, 2, 70000]
  );

๋‚ ์งœ ํ•จ์ˆ˜

// ์ตœ๊ทผ 30์ผ ๋ฐ์ดํ„ฐ
const results = await db
  .table("users")
  .select({ id: "id" })
  .whereRaw("created_at > NOW() - INTERVAL '30 days'");

// ํŠน์ • ์—ฐ๋„
const results = await db
  .table("users")
  .select({ id: "id" })
  .whereRaw("EXTRACT(YEAR FROM created_at) = ?", [2024]);

CASE WHEN - ์กฐ๊ฑด๋ถ€ ๊ฐ’

CASE WHEN ํ‘œํ˜„์‹์œผ๋กœ ์กฐ๊ฑด์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ CASE WHEN

const results = await db.table("employees").select({
  id: "id",
  name: "username",
  salaryLevel: Puri.rawString(`
      CASE
        WHEN salary < 50000 THEN 'Junior'
        WHEN salary < 70000 THEN 'Mid'
        ELSE 'Senior'
      END
    `),
});

์ˆซ์ž ๊ณ„์‚ฐ

const results = await db.table("products").select({
  id: "id",
  name: "name",
  price: "price",
  discountedPrice: Puri.rawNumber(`
      CASE
        WHEN category = 'sale' THEN price * 0.8
        WHEN category = 'clearance' THEN price * 0.5
        ELSE price
      END
    `),
});

Boolean ๊ฒฐ๊ณผ

const results = await db.table("users").select({
  id: "id",
  name: "username",
  isPremium: Puri.rawBoolean(`
      CASE
        WHEN subscription_tier IN ('gold', 'platinum') THEN TRUE
        ELSE FALSE
      END
    `),
});

์„œ๋ธŒ์ฟผ๋ฆฌ์™€ Raw SQL

Scalar ์„œ๋ธŒ์ฟผ๋ฆฌ

const results = await db.table("users").select({
  id: "id",
  name: "username",
  postCount: Puri.rawNumber(`
      (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id)
    `),
});

COALESCE - NULL ์ฒ˜๋ฆฌ

const results = await db.table("employees").select({
  id: "id",
  departmentName: Puri.rawString(`
      COALESCE(
        (SELECT name FROM departments WHERE id = employees.department_id),
        'No Department'
      )
    `),
});

์‹ค์ „ ์˜ˆ์ œ

์‚ฌ์šฉ์ž ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ

async getUserStats(userId: number) {
  const stats = await this.getPuri("r")
    .table("users")
    .select({
      userId: "users.id",
      username: "users.username",

      // ๊ฒŒ์‹œ๊ธ€ ํ†ต๊ณ„
      totalPosts: Puri.rawNumber(`
        (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id)
      `),

      recentPosts: Puri.rawNumber(`
        (SELECT COUNT(*)
         FROM posts
         WHERE posts.user_id = users.id
         AND posts.created_at > NOW() - INTERVAL '30 days')
      `),

      // ํ™œ๋™ ๋ ˆ๋ฒจ
      activityLevel: Puri.rawString(`
        CASE
          WHEN (SELECT COUNT(*) FROM posts WHERE user_id = users.id) > 100 THEN 'High'
          WHEN (SELECT COUNT(*) FROM posts WHERE user_id = users.id) > 10 THEN 'Medium'
          ELSE 'Low'
        END
      `),

      // ๊ฐ€์ž… ๊ฒฝ๊ณผ ์ผ์ˆ˜
      daysSinceJoined: Puri.rawNumber(`
        EXTRACT(DAY FROM AGE(NOW(), users.created_at))
      `),
    })
    .where("users.id", userId)
    .first();

  return stats;
}

์‹œ๊ฐ„๋Œ€๋ณ„ ์ง‘๊ณ„

async getHourlyStats(date: string) {
  const stats = await this.getPuri("r")
    .table("events")
    .select({
      hour: Puri.rawNumber("EXTRACT(HOUR FROM created_at)"),
      date: Puri.rawDate("DATE(created_at)"),
      eventCount: Puri.count("id"),
      uniqueUsers: Puri.rawNumber("COUNT(DISTINCT user_id)"),
    })
    .whereRaw("DATE(created_at) = ?", [date])
    .groupBy("hour", "date")
    .orderBy("hour", "asc");

  return stats;
}

์ˆœ์œ„ ๊ณ„์‚ฐ

async getTopUsers() {
  const results = await this.getPuri("r")
    .table("users")
    .select({
      userId: "users.id",
      username: "users.username",
      postCount: Puri.rawNumber(`
        (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id)
      `),
      rank: Puri.rawNumber(`
        RANK() OVER (ORDER BY
          (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id) DESC
        )
      `),
    })
    .whereRaw(`
      (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id) > 0
    `)
    .orderBy("rank", "asc")
    .limit(10);

  return results;
}

์œˆ๋„์šฐ ํ•จ์ˆ˜

ROW_NUMBER

const results = await db.table("employees").select({
  id: "id",
  name: "username",
  salary: "salary",
  rowNumber: Puri.rawNumber(`
      ROW_NUMBER() OVER (ORDER BY salary DESC)
    `),
});

RANK / DENSE_RANK

const results = await db.table("employees").select({
  id: "id",
  departmentId: "department_id",
  salary: "salary",
  rankInDept: Puri.rawNumber(`
      RANK() OVER (PARTITION BY department_id ORDER BY salary DESC)
    `),
});

LAG / LEAD - ์ด์ „/๋‹ค์Œ ํ–‰

const results = await db
  .table("sales")
  .select({
    id: "id",
    month: "month",
    amount: "amount",
    previousMonth: Puri.rawNumber(`
      LAG(amount, 1) OVER (ORDER BY month)
    `),
    nextMonth: Puri.rawNumber(`
      LEAD(amount, 1) OVER (ORDER BY month)
    `),
  })
  .orderBy("month", "asc");

JSON ํ•จ์ˆ˜ (PostgreSQL)

JSON ํ•„๋“œ ์ถ”์ถœ

const results = await db.table("users").select({
  id: "id",
  city: Puri.rawString("metadata->>'city'"),
  age: Puri.rawNumber("(metadata->>'age')::integer"),
  tags: Puri.rawStringArray(
    "ARRAY(SELECT jsonb_array_elements_text(metadata->'tags'))"
  ),
});

JSON ์ง‘๊ณ„

const results = await db
  .table("employees")
  .select({
    departmentId: "department_id",
    employees: Puri.rawString(`
      JSON_AGG(JSON_BUILD_OBJECT(
        'id', id,
        'name', username,
        'salary', salary
      ))
    `),
  })
  .groupBy("department_id");

์„ฑ๋Šฅ ์ตœ์ ํ™”

EXPLAIN ์‚ฌ์šฉ

// ์ฟผ๋ฆฌ ์‹คํ–‰ ๊ณ„ํš ํ™•์ธ
const plan = await db
  .table("employees")
  .select({ id: "id" })
  .where("department_id", 1)
  .rawQuery()
  .explain();

console.log(plan);

์ธ๋ฑ์Šค ํžŒํŠธ (PostgreSQL์€ ์ง€์› ์•ˆํ•จ)

PostgreSQL์€ ์˜ตํ‹ฐ๋งˆ์ด์ €๊ฐ€ ์ž๋™์œผ๋กœ ์ธ๋ฑ์Šค๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์‹  ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ:
ANALYZE employees;

ํƒ€์ž… ์•ˆ์ „์„ฑ

Raw ํ•จ์ˆ˜๋“ค์€ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค.
const results = await db.table("users").select({
  stringValue: Puri.rawString("'test'"), // string
  numberValue: Puri.rawNumber("123"), // number
  boolValue: Puri.rawBoolean("TRUE"), // boolean
  dateValue: Puri.rawDate("NOW()"), // Date
  arrayValue: Puri.rawStringArray("'{}'"), // string[]
});

// ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ์ถ”๋ก ๋จ
results[0].stringValue; // string
results[0].numberValue; // number
results[0].boolValue; // boolean

Raw ์ฟผ๋ฆฌ์™€ Hydrate

Raw SQL์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” **hydrate()**๋ฅผ ์ˆ˜๋™์œผ๋กœ ํ˜ธ์ถœํ•˜๊ฑฐ๋‚˜, ํ•„๋“œ ๋„ค์ด๋ฐ ๊ทœ์น™์„ ๋”ฐ๋ผ์•ผ JOIN๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์กฐํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Subset vs Raw Puri์˜ ์ฐจ์ด

๊ธฐ๋ŠฅSubset ์ฟผ๋ฆฌ (getSubsetQueries + executeSubsetQuery)Raw Puri ์ฟผ๋ฆฌ (getPuri("r"))
Hydrateโœ… ์ž๋™ ํ˜ธ์ถœโŒ ์ˆ˜๋™ ํ˜ธ์ถœ ํ•„์š”
JOINโœ… ์ž๋™ ์„ค์ •โš ๏ธ ์ˆ˜๋™ ์„ค์ •
ํƒ€์ž… ์ถ”๋ก โœ… Subset ํƒ€์ž…โš ๏ธ ์ˆ˜๋™ ์ •์˜
ํ•„๋“œ ๊ตฌ์กฐํ™”โœ… ์ž๋™ (์ค‘์ฒฉ ๊ฐ์ฒด)โŒ ์ˆ˜๋™ (flat)

Hydrate๊ฐ€ ํ•˜๋Š” ์ผ

hydrate()๋Š” flatํ•œ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ์ค‘์ฒฉ๋œ ๊ฐ์ฒด ๊ตฌ์กฐ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Before hydrate (Flat):
{
  id: 1,
  username: "john",
  employee__id: 10,
  employee__salary: "60000",
  employee__department__id: 5,
  employee__department__name: "Engineering"
}
After hydrate (Nested):
{
  id: 1,
  username: "john",
  employee: {
    id: 10,
    salary: "60000",
    department: {
      id: 5,
      name: "Engineering"
    }
  }
}

ํ•„๋“œ ๋„ค์ด๋ฐ ๊ทœ์น™: ์–ธ๋”๋ฐ”(__) ์‚ฌ์šฉ

JOIN๋œ ํ…Œ์ด๋ธ”์˜ ํ•„๋“œ๋Š” ํ…Œ์ด๋ธ”๋ช…__ํ•„๋“œ๋ช… ํ˜•์‹์œผ๋กœ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
// โœ… ์˜ฌ๋ฐ”๋ฅธ ๋„ค์ด๋ฐ
const results = await db
  .table("users")
  .join("employees", "users.id", "employees.user_id")
  .join("departments", "employees.department_id", "departments.id")
  .select({
    id: "users.id",
    username: "users.username",
    employee__id: "employees.id",                    // ์–ธ๋”๋ฐ” 2๊ฐœ
    employee__salary: "employees.salary",
    employee__department__id: "departments.id",      // ์ค‘์ฒฉ ์‹œ ์–ธ๋”๋ฐ” 2๊ฐœ์”ฉ
    employee__department__name: "departments.name",
  });

// hydrate ํ˜ธ์ถœ
const hydrated = results.map(row => UserModel.hydrate(row));

// ๊ฒฐ๊ณผ: ์ค‘์ฒฉ๋œ ๊ฐ์ฒด ๊ตฌ์กฐ
hydrated[0].employee.department.name; // "Engineering"
// โŒ ์ž˜๋ชป๋œ ๋„ค์ด๋ฐ (์–ธ๋”๋ฐ” 1๊ฐœ)
const results = await db
  .table("users")
  .join("employees", "users.id", "employees.user_id")
  .select({
    id: "users.id",
    username: "users.username",
    employee_id: "employees.id",        // โŒ ์ž˜๋ชป๋จ (์–ธ๋”๋ฐ” 1๊ฐœ)
    employee_salary: "employees.salary", // โŒ ์ž˜๋ชป๋จ
  });

// hydrate ํ˜ธ์ถœํ•ด๋„ ์ค‘์ฒฉ ๊ตฌ์กฐ ์ƒ์„ฑ ์•ˆ๋จ
const hydrated = results.map(row => UserModel.hydrate(row));
hydrated[0].employee; // undefined

์ž๋™ Hydrate vs ์ˆ˜๋™ Hydrate

Subset ์ฟผ๋ฆฌ (์ž๋™ Hydrate)

// Subset ์ฟผ๋ฆฌ๋Š” hydrate ์ž๋™ ํ˜ธ์ถœ
const { qb } = UserModel.getSubsetQueries("P");
qb.where("users.role", "normal");

const result = await UserModel.executeSubsetQuery({
  subset: "P",
  qb,
  params: { num: 20, page: 1 },
});

// ์ด๋ฏธ hydrate๋˜์–ด ์ค‘์ฒฉ ๊ตฌ์กฐ
result.rows[0].employee.department.name; // โœ… OK

Raw Puri ์ฟผ๋ฆฌ (์ˆ˜๋™ Hydrate)

// Raw ์ฟผ๋ฆฌ๋Š” hydrate ์ˆ˜๋™ ํ˜ธ์ถœ ํ•„์š”
const users = await UserModel.getPuri("r")
  .table("users")
  .join("employees", "users.id", "employees.user_id")
  .join("departments", "employees.department_id", "departments.id")
  .select({
    id: "users.id",
    username: "users.username",
    employee__id: "employees.id",
    employee__salary: "employees.salary",
    employee__department__id: "departments.id",
    employee__department__name: "departments.name",
  });

// โŒ hydrate ์ „: flat ๊ตฌ์กฐ
users[0].employee; // undefined
users[0].employee__id; // 10 (flat)

// โœ… hydrate ํ›„: ์ค‘์ฒฉ ๊ตฌ์กฐ
const hydrated = users.map(row => UserModel.hydrate(row));
hydrated[0].employee.id; // 10
hydrated[0].employee.department.name; // "Engineering"

executeSubsetQuery์—์„œ์˜ ์ž๋™ Hydrate

executeSubsetQuery()๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ hydrate๋ฅผ ์ž๋™ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
class UserModelClass extends BaseModelClass<...> {
  async getUsersWithDepartment() {
    const { qb } = this.getSubsetQueries("P");

    qb.where("users.role", "normal");

    // executeSubsetQuery๋Š” hydrate๋ฅผ ์ž๋™ ํ˜ธ์ถœ
    const users = await this.executeSubsetQuery({
      subset: "P",
      qb,
      params: { num: 20, page: 1 },
    });

    // ์ด๋ฏธ hydrate๋˜์–ด ์žˆ์Œ
    return users.rows.map(user => ({
      id: user.id,
      username: user.username,
      department: user.employee?.department?.name, // โœ… OK
    }));
  }
}

Hydrate ํ˜ธ์ถœ ์‹œ์  ์ •๋ฆฌ

๋ฉ”์„œ๋“œHydrate ์ž๋™ ํ˜ธ์ถœ์„ค๋ช…
getSubsetQueries + executeSubsetQueryโœ… YesSubset ์ฟผ๋ฆฌ๋Š” ์ž๋™
executeSubsetQuery()โœ… Yes๋‚ด๋ถ€์—์„œ ์ž๋™ ํ˜ธ์ถœ
getPuri("r")โŒ NoRaw ์ฟผ๋ฆฌ๋Š” ์ˆ˜๋™
findById()โœ… YesBaseModel ๋ฉ”์„œ๋“œ๋Š” ์ž๋™
findOne()โœ… YesBaseModel ๋ฉ”์„œ๋“œ๋Š” ์ž๋™
findMany()โœ… YesBaseModel ๋ฉ”์„œ๋“œ๋Š” ์ž๋™

์‹ค์ „ ์˜ˆ์ œ: Raw ์ฟผ๋ฆฌ + Hydrate

class UserModelClass extends BaseModelClass<...> {
  // Raw ์ฟผ๋ฆฌ๋กœ ๋ณต์žกํ•œ ์กฐ์ธ
  async getTopUsersWithStats() {
    const results = await this.getPuri("r")
      .table("users")
      .join("employees", "users.id", "employees.user_id")
      .join("departments", "employees.department_id", "departments.id")
      .select({
        // ๊ธฐ๋ณธ ํ•„๋“œ
        id: "users.id",
        username: "users.username",
        email: "users.email",

        // JOIN ํ•„๋“œ (์–ธ๋”๋ฐ” 2๊ฐœ ๊ทœ์น™)
        employee__id: "employees.id",
        employee__salary: "employees.salary",
        employee__hire_date: "employees.hire_date",
        employee__department__id: "departments.id",
        employee__department__name: "departments.name",

        // ์ง‘๊ณ„ ํ•„๋“œ
        postCount: Puri.rawNumber(`
          (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id)
        `),
      })
      .whereRaw("employees.salary > ?", [60000])
      .orderBy("employees.salary", "desc")
      .limit(10);

    // Hydrate ํ˜ธ์ถœ๋กœ ์ค‘์ฒฉ ๊ตฌ์กฐ ์ƒ์„ฑ
    const hydrated = results.map(row => this.hydrate(row));

    // ์ค‘์ฒฉ ๊ตฌ์กฐ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์ ‘๊ทผ
    return hydrated.map(user => ({
      id: user.id,
      username: user.username,
      department: user.employee.department.name,  // โœ… OK
      salary: user.employee.salary,
      postCount: user.postCount,
    }));
  }
}
Hydrate ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ํ•„๋“œ ๋„ค์ด๋ฐ: JOIN ํ•„๋“œ๋Š” ๋ฐ˜๋“œ์‹œ __(์–ธ๋”๋ฐ” 2๊ฐœ) ์‚ฌ์šฉ
  2. ์ˆ˜๋™ ํ˜ธ์ถœ: Raw Puri ์ฟผ๋ฆฌ๋Š” hydrate() ์ˆ˜๋™ ํ˜ธ์ถœ ํ•„์ˆ˜
  3. ํƒ€์ž… ์•ˆ์ „์„ฑ: hydrate ํ›„ ํƒ€์ž…์€ ์ˆ˜๋™์œผ๋กœ ์ •์˜ ํ•„์š”
  4. ์„ฑ๋Šฅ: hydrate๋Š” ๋Ÿฐํƒ€์ž„ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ, ๊ฐ„๋‹จํ•œ ์ฟผ๋ฆฌ๋Š” Subset ์‚ฌ์šฉ ๊ถŒ์žฅ
๊ถŒ์žฅ ์‚ฌํ•ญ:
  • ๊ฐ€๋Šฅํ•˜๋ฉด Subset ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š” (hydrate ์ž๋™)
  • ๋ณต์žกํ•œ Raw SQL์ด ํ•„์š”ํ•  ๋•Œ๋งŒ ์ˆ˜๋™ hydrate ์‚ฌ์šฉ
  • ํ•„๋“œ ๋„ค์ด๋ฐ ๊ทœ์น™(__)์„ ์ผ๊ด€๋˜๊ฒŒ ์ ์šฉ

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