feat: Complete Phase 1 of Prisma to Raw Query migration
Phase 1 완료: Raw Query 기반 데이터베이스 아키텍처 구축 ✅ 구현 완료 내용: - DatabaseManager 클래스 구현 (연결 풀, 트랜잭션 관리) - QueryBuilder 유틸리티 (동적 쿼리 생성) - 타입 정의 및 검증 로직 (database.ts, databaseValidator.ts) - 단위 테스트 작성 및 통과 🔧 전환 완료 서비스: - externalCallConfigService.ts (Raw Query 전환) - multiConnectionQueryService.ts (Raw Query 전환) 📚 문서: - PHASE1_USAGE_GUIDE.md (사용 가이드) - DETAILED_FILE_MIGRATION_PLAN.md (상세 계획) - PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md (Phase 1 완료 표시) 🧪 테스트: - database.test.ts (핵심 기능 테스트) - 모든 테스트 통과 확인 이제 Phase 2 (핵심 서비스 전환)로 진행 가능 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
287
backend-node/src/utils/queryBuilder.ts
Normal file
287
backend-node/src/utils/queryBuilder.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* SQL 쿼리 빌더 유틸리티
|
||||
*
|
||||
* Raw Query 방식에서 안전하고 효율적인 쿼리 생성을 위한 헬퍼
|
||||
*/
|
||||
|
||||
export interface SelectOptions {
|
||||
columns?: string[];
|
||||
where?: Record<string, any>;
|
||||
joins?: JoinClause[];
|
||||
orderBy?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
groupBy?: string[];
|
||||
having?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface JoinClause {
|
||||
type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL';
|
||||
table: string;
|
||||
on: string;
|
||||
}
|
||||
|
||||
export interface InsertOptions {
|
||||
returning?: string[];
|
||||
onConflict?: {
|
||||
columns: string[];
|
||||
action: 'DO NOTHING' | 'DO UPDATE';
|
||||
updateSet?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateOptions {
|
||||
returning?: string[];
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
query: string;
|
||||
params: any[];
|
||||
}
|
||||
|
||||
export class QueryBuilder {
|
||||
/**
|
||||
* SELECT 쿼리 생성
|
||||
*/
|
||||
static select(table: string, options: SelectOptions = {}): QueryResult {
|
||||
const {
|
||||
columns = ['*'],
|
||||
where = {},
|
||||
joins = [],
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
groupBy = [],
|
||||
having = {},
|
||||
} = options;
|
||||
|
||||
let query = `SELECT ${columns.join(', ')} FROM ${table}`;
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// JOIN 절 추가
|
||||
for (const join of joins) {
|
||||
query += ` ${join.type} JOIN ${join.table} ON ${join.on}`;
|
||||
}
|
||||
|
||||
// WHERE 절 추가
|
||||
const whereConditions = Object.keys(where);
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = whereConditions
|
||||
.map((key) => {
|
||||
params.push(where[key]);
|
||||
return `${key} = $${paramIndex++}`;
|
||||
})
|
||||
.join(' AND ');
|
||||
query += ` WHERE ${whereClause}`;
|
||||
}
|
||||
|
||||
// GROUP BY 절 추가
|
||||
if (groupBy.length > 0) {
|
||||
query += ` GROUP BY ${groupBy.join(', ')}`;
|
||||
}
|
||||
|
||||
// HAVING 절 추가
|
||||
const havingConditions = Object.keys(having);
|
||||
if (havingConditions.length > 0) {
|
||||
const havingClause = havingConditions
|
||||
.map((key) => {
|
||||
params.push(having[key]);
|
||||
return `${key} = $${paramIndex++}`;
|
||||
})
|
||||
.join(' AND ');
|
||||
query += ` HAVING ${havingClause}`;
|
||||
}
|
||||
|
||||
// ORDER BY 절 추가
|
||||
if (orderBy) {
|
||||
query += ` ORDER BY ${orderBy}`;
|
||||
}
|
||||
|
||||
// LIMIT 절 추가
|
||||
if (limit !== undefined) {
|
||||
params.push(limit);
|
||||
query += ` LIMIT $${paramIndex++}`;
|
||||
}
|
||||
|
||||
// OFFSET 절 추가
|
||||
if (offset !== undefined) {
|
||||
params.push(offset);
|
||||
query += ` OFFSET $${paramIndex++}`;
|
||||
}
|
||||
|
||||
return { query, params };
|
||||
}
|
||||
|
||||
/**
|
||||
* INSERT 쿼리 생성
|
||||
*/
|
||||
static insert(
|
||||
table: string,
|
||||
data: Record<string, any>,
|
||||
options: InsertOptions = {}
|
||||
): QueryResult {
|
||||
const { returning = [], onConflict } = options;
|
||||
|
||||
const columns = Object.keys(data);
|
||||
const values = Object.values(data);
|
||||
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
|
||||
|
||||
let query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
|
||||
|
||||
// ON CONFLICT 절 추가
|
||||
if (onConflict) {
|
||||
query += ` ON CONFLICT (${onConflict.columns.join(', ')})`;
|
||||
|
||||
if (onConflict.action === 'DO NOTHING') {
|
||||
query += ' DO NOTHING';
|
||||
} else if (onConflict.action === 'DO UPDATE' && onConflict.updateSet) {
|
||||
const updateSet = onConflict.updateSet
|
||||
.map(col => `${col} = EXCLUDED.${col}`)
|
||||
.join(', ');
|
||||
query += ` DO UPDATE SET ${updateSet}`;
|
||||
}
|
||||
}
|
||||
|
||||
// RETURNING 절 추가
|
||||
if (returning.length > 0) {
|
||||
query += ` RETURNING ${returning.join(', ')}`;
|
||||
}
|
||||
|
||||
return { query, params: values };
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATE 쿼리 생성
|
||||
*/
|
||||
static update(
|
||||
table: string,
|
||||
data: Record<string, any>,
|
||||
where: Record<string, any>,
|
||||
options: UpdateOptions = {}
|
||||
): QueryResult {
|
||||
const { returning = [] } = options;
|
||||
|
||||
const dataKeys = Object.keys(data);
|
||||
const dataValues = Object.values(data);
|
||||
const whereKeys = Object.keys(where);
|
||||
const whereValues = Object.values(where);
|
||||
|
||||
let paramIndex = 1;
|
||||
|
||||
// SET 절 생성
|
||||
const setClause = dataKeys
|
||||
.map((key) => `${key} = $${paramIndex++}`)
|
||||
.join(', ');
|
||||
|
||||
// WHERE 절 생성
|
||||
const whereClause = whereKeys
|
||||
.map((key) => `${key} = $${paramIndex++}`)
|
||||
.join(' AND ');
|
||||
|
||||
let query = `UPDATE ${table} SET ${setClause} WHERE ${whereClause}`;
|
||||
|
||||
// RETURNING 절 추가
|
||||
if (returning.length > 0) {
|
||||
query += ` RETURNING ${returning.join(', ')}`;
|
||||
}
|
||||
|
||||
const params = [...dataValues, ...whereValues];
|
||||
|
||||
return { query, params };
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 쿼리 생성
|
||||
*/
|
||||
static delete(table: string, where: Record<string, any>): QueryResult {
|
||||
const whereKeys = Object.keys(where);
|
||||
const whereValues = Object.values(where);
|
||||
|
||||
const whereClause = whereKeys
|
||||
.map((key, index) => `${key} = $${index + 1}`)
|
||||
.join(' AND ');
|
||||
|
||||
const query = `DELETE FROM ${table} WHERE ${whereClause}`;
|
||||
|
||||
return { query, params: whereValues };
|
||||
}
|
||||
|
||||
/**
|
||||
* COUNT 쿼리 생성
|
||||
*/
|
||||
static count(table: string, where: Record<string, any> = {}): QueryResult {
|
||||
const whereKeys = Object.keys(where);
|
||||
const whereValues = Object.values(where);
|
||||
|
||||
let query = `SELECT COUNT(*) as count FROM ${table}`;
|
||||
|
||||
if (whereKeys.length > 0) {
|
||||
const whereClause = whereKeys
|
||||
.map((key, index) => `${key} = $${index + 1}`)
|
||||
.join(' AND ');
|
||||
query += ` WHERE ${whereClause}`;
|
||||
}
|
||||
|
||||
return { query, params: whereValues };
|
||||
}
|
||||
|
||||
/**
|
||||
* EXISTS 쿼리 생성
|
||||
*/
|
||||
static exists(table: string, where: Record<string, any>): QueryResult {
|
||||
const whereKeys = Object.keys(where);
|
||||
const whereValues = Object.values(where);
|
||||
|
||||
const whereClause = whereKeys
|
||||
.map((key, index) => `${key} = $${index + 1}`)
|
||||
.join(' AND ');
|
||||
|
||||
const query = `SELECT EXISTS(SELECT 1 FROM ${table} WHERE ${whereClause}) as exists`;
|
||||
|
||||
return { query, params: whereValues };
|
||||
}
|
||||
|
||||
/**
|
||||
* 동적 WHERE 절 생성 (복잡한 조건)
|
||||
*/
|
||||
static buildWhereClause(
|
||||
conditions: Record<string, any>,
|
||||
startParamIndex: number = 1
|
||||
): { clause: string; params: any[]; nextParamIndex: number } {
|
||||
const keys = Object.keys(conditions);
|
||||
const params: any[] = [];
|
||||
let paramIndex = startParamIndex;
|
||||
|
||||
if (keys.length === 0) {
|
||||
return { clause: '', params: [], nextParamIndex: paramIndex };
|
||||
}
|
||||
|
||||
const clause = keys
|
||||
.map((key) => {
|
||||
const value = conditions[key];
|
||||
|
||||
// 특수 연산자 처리
|
||||
if (key.includes('>>') || key.includes('->')) {
|
||||
// JSON 쿼리
|
||||
params.push(value);
|
||||
return `${key} = $${paramIndex++}`;
|
||||
} else if (Array.isArray(value)) {
|
||||
// IN 절
|
||||
const placeholders = value.map(() => `$${paramIndex++}`).join(', ');
|
||||
params.push(...value);
|
||||
return `${key} IN (${placeholders})`;
|
||||
} else if (value === null) {
|
||||
// NULL 체크
|
||||
return `${key} IS NULL`;
|
||||
} else {
|
||||
// 일반 조건
|
||||
params.push(value);
|
||||
return `${key} = $${paramIndex++}`;
|
||||
}
|
||||
})
|
||||
.join(' AND ');
|
||||
|
||||
return { clause, params, nextParamIndex: paramIndex };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user