UpsertBuilder 개요
트랜잭션 필수
항상 트랜잭션 내에서 사용데이터 일관성 보장
UBRef 참조
외래 키 자동 해결ID 없이 관계 정의
순차 저장
의존 순서대로 저장외래 키 제약 충족
타입 안전성
컴파일 타임 검증테이블 스키마 확인
기본 개념
일반 INSERT의 문제
복사
// ❌ 문제: 외래 키를 미리 알 수 없음
await db.transaction(async (trx) => {
// 1. Company 생성
const [companyId] = await trx
.table("companies")
.insert({ name: "Tech Corp" })
.returning("id");
// 2. Department 생성 (company_id 필요)
const [deptId] = await trx
.table("departments")
.insert({
name: "Engineering",
company_id: companyId, // ← 위에서 받은 ID 사용
})
.returning("id");
// 3. User 생성
const [userId] = await trx
.table("users")
.insert({
email: "[email protected]",
username: "john",
password: "pass",
role: "normal",
})
.returning("id");
// 4. Employee 생성 (user_id, department_id 필요)
await trx.table("employees").insert({
user_id: userId, // ← 위에서 받은 ID
department_id: deptId, // ← 위에서 받은 ID
employee_number: "E001",
});
});
// 코드가 길고 복잡함
// ID를 매번 추적해야 함
UpsertBuilder의 해결
복사
// ✅ 해결: UBRef로 관계 정의
await db.transaction(async (trx) => {
// 1. 데이터 등록 (UBRef 받기)
const companyRef = trx.ubRegister("companies", {
name: "Tech Corp",
});
const deptRef = trx.ubRegister("departments", {
name: "Engineering",
company_id: companyRef, // ← UBRef 사용
});
const userRef = trx.ubRegister("users", {
email: "[email protected]",
username: "john",
password: "pass",
role: "normal",
});
trx.ubRegister("employees", {
user_id: userRef, // ← UBRef 사용
department_id: deptRef, // ← UBRef 사용
employee_number: "E001",
});
// 2. 순서대로 저장
await trx.ubUpsert("companies");
await trx.ubUpsert("departments");
await trx.ubUpsert("users");
await trx.ubUpsert("employees");
});
// 간결하고 명확함
// ID 추적 불필요
ubRegister - 데이터 등록
ubRegister는 저장할 데이터를 등록하고 UBRef를 반환합니다.
기본 사용
복사
await db.transaction(async (trx) => {
// 테이블과 데이터 지정
const userRef = trx.ubRegister("users", {
email: "[email protected]",
username: "john",
password: "hashed_password",
role: "normal",
});
// userRef는 UBRef 타입
// 실제 ID가 아니라 참조 객체
});
타입 안전성
복사
// ✅ OK: 올바른 필드
trx.ubRegister("users", {
email: "[email protected]",
username: "john",
password: "pass",
role: "normal",
});
// ❌ Type Error: 잘못된 필드
trx.ubRegister("users", {
email: "[email protected]",
invalid_field: "value", // Property 'invalid_field' does not exist
});
// ❌ Type Error: 필수 필드 누락
trx.ubRegister("users", {
email: "[email protected]",
// username 누락 → Type Error
});
ubUpsert - 실제 저장
ubUpsert는 등록된 데이터를 DB에 실제로 저장합니다.
기본 사용
복사
await db.transaction(async (trx) => {
trx.ubRegister("users", {
email: "[email protected]",
username: "john",
password: "pass",
role: "normal",
});
// DB에 저장하고 생성된 ID 배열 반환
const userIds = await trx.ubUpsert("users");
console.log(userIds); // [1]
});
반환값
복사
// 단일 레코드
const [userId] = await trx.ubUpsert("users");
console.log(userId); // 1
// 여러 레코드
const userIds = await trx.ubUpsert("users");
console.log(userIds); // [1, 2, 3]
저장 순서
복사
await db.transaction(async (trx) => {
// 등록 (순서 무관)
const userRef = trx.ubRegister("users", { ... });
const companyRef = trx.ubRegister("companies", { ... });
const deptRef = trx.ubRegister("departments", {
company_id: companyRef,
});
const empRef = trx.ubRegister("employees", {
user_id: userRef,
department_id: deptRef,
});
// 저장 (의존성 순서 필수)
await trx.ubUpsert("companies"); // 1. 의존성 없음
await trx.ubUpsert("departments"); // 2. company_id 필요
await trx.ubUpsert("users"); // 3. 의존성 없음
await trx.ubUpsert("employees"); // 4. user_id, department_id 필요
});
저장 순서 중요!외래 키가 참조하는 테이블을 먼저 저장해야 합니다.
잘못된 순서는 외래 키 제약 위반 에러를 발생시킵니다.
실전 예제
사용자 등록
복사
async createUser(data: {
email: string;
username: string;
password: string;
}) {
return await this.getPuri("w").transaction(async (trx) => {
// User 등록
trx.ubRegister("users", {
email: data.email,
username: data.username,
password: data.password,
role: "normal",
is_verified: false,
});
// 저장
const [userId] = await trx.ubUpsert("users");
return userId;
});
}
회사 + 부서 생성
복사
async createCompanyWithDepartment(data: {
companyName: string;
departmentName: string;
}) {
return await this.getPuri("w").transaction(async (trx) => {
// 1. Company 등록
const companyRef = trx.ubRegister("companies", {
name: data.companyName,
});
// 2. Department 등록 (Company 참조)
trx.ubRegister("departments", {
name: data.departmentName,
company_id: companyRef,
});
// 3. 순서대로 저장
const [companyId] = await trx.ubUpsert("companies");
const [departmentId] = await trx.ubUpsert("departments");
return { companyId, departmentId };
});
}
직원 등록 (User + Employee)
복사
async createEmployee(data: {
email: string;
username: string;
password: string;
departmentId: number;
employeeNumber: string;
salary: string;
}) {
return await this.getPuri("w").transaction(async (trx) => {
// 1. User 등록
const userRef = trx.ubRegister("users", {
email: data.email,
username: data.username,
password: data.password,
role: "normal",
});
// 2. Employee 등록
trx.ubRegister("employees", {
user_id: userRef,
department_id: data.departmentId, // 기존 ID 사용
employee_number: data.employeeNumber,
salary: data.salary,
});
// 3. 저장
const [userId] = await trx.ubUpsert("users");
const [employeeId] = await trx.ubUpsert("employees");
return { userId, employeeId };
});
}
UBRef 타입
UBRef는 실제 ID가 아닌 참조 객체입니다:복사
// UBRef 타입
type UBRef = {
_type: "UBRef";
table: string;
index: number;
};
// 사용 예시
const userRef = trx.ubRegister("users", { ... });
console.log(userRef);
// { _type: "UBRef", table: "users", index: 0 }
// ❌ 직접 사용 불가
const userId = userRef; // Type Error
await db.table("posts").insert({
user_id: userRef, // ❌ Runtime Error
});
// ✅ UpsertBuilder 내에서만 사용
trx.ubRegister("employees", {
user_id: userRef, // ✅ OK
});
에러 처리
트랜잭션 롤백
복사
try {
await db.transaction(async (trx) => {
trx.ubRegister("users", { ... });
trx.ubRegister("employees", { ... });
await trx.ubUpsert("users");
// 에러 발생 시 자동 롤백
throw new Error("Something went wrong");
});
} catch (error) {
console.error("Transaction failed:", error);
// users 삽입도 롤백됨
}
외래 키 제약 위반
복사
await db.transaction(async (trx) => {
const userRef = trx.ubRegister("users", { ... });
trx.ubRegister("employees", {
user_id: userRef,
department_id: 999, // 존재하지 않는 ID
});
await trx.ubUpsert("users");
// ❌ 외래 키 제약 위반 에러
await trx.ubUpsert("employees");
// Error: FOREIGN KEY constraint failed
});
