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์ ์ตํฐ๋ง์ด์ ๊ฐ ์๋์ผ๋ก ์ธ๋ฑ์ค๋ฅผ ์ ํํฉ๋๋ค. ๋์ ํต๊ณ ์
๋ฐ์ดํธ:
ํ์
์์ ์ฑ
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โ
Yes Subset ์ฟผ๋ฆฌ๋ ์๋ executeSubsetQuery()โ
Yes ๋ด๋ถ์์ ์๋ ํธ์ถ getPuri("r")โ No Raw ์ฟผ๋ฆฌ๋ ์๋ findById()โ
Yes BaseModel ๋ฉ์๋๋ ์๋ findOne()โ
Yes BaseModel ๋ฉ์๋๋ ์๋ findMany()โ
Yes BaseModel ๋ฉ์๋๋ ์๋
์ค์ ์์ : 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 ์ฌ์ฉ ์ ์ฃผ์์ฌํญ :
ํ๋ ๋ค์ด๋ฐ : JOIN ํ๋๋ ๋ฐ๋์ __(์ธ๋๋ฐ 2๊ฐ) ์ฌ์ฉ
์๋ ํธ์ถ : Raw Puri ์ฟผ๋ฆฌ๋ hydrate() ์๋ ํธ์ถ ํ์
ํ์
์์ ์ฑ : hydrate ํ ํ์
์ ์๋์ผ๋ก ์ ์ ํ์
์ฑ๋ฅ : hydrate๋ ๋ฐํ์ ์ค๋ฒํค๋๊ฐ ์์ผ๋ฏ๋ก, ๊ฐ๋จํ ์ฟผ๋ฆฌ๋ Subset ์ฌ์ฉ ๊ถ์ฅ
๊ถ์ฅ ์ฌํญ :
๊ฐ๋ฅํ๋ฉด Subset ์ฟผ๋ฆฌ ๋ฅผ ์ฌ์ฉํ์ธ์ (hydrate ์๋)
๋ณต์กํ Raw SQL์ด ํ์ํ ๋๋ง ์๋ hydrate ์ฌ์ฉ
ํ๋ ๋ค์ด๋ฐ ๊ท์น(__)์ ์ผ๊ด๋๊ฒ ์ ์ฉ
๋ค์ ๋จ๊ณ