공연 예매 사이트 과제 진행중에 고객이 좌석을 결제하다가 이미 예매된 좌석이 되었을땐 결제를 못하고 롤백 시키도록 트랜잭션을 해줬었다.
그런데 좌석 내역은 추가 되지 않고 롤백 되었으나 결제 내역은 추가 되는 상황이 발생하였다.
결국 해결하지 못하고 결제 상태 컬럼을 추가해서 true, false로 저장해둘까 했는데 그렇게 되면 너무 많은 쓸데없는 데이터만 늘어난다고 생각이 들며, typeORM 사이트, 구글 검색을 다 해봤었다.
결국 해답을 찾은건 Nest.js 공식문서에 나와있는 TypeORM Transactions 파트였다.
Nest.js에서 TypeORM 트랜잭션을 쓰려면 데코레이터 트랜잭션이 아닌 DataSource를 가져와서 QueryRunner를 써야한다.
@Injectable()
export class UsersService {
constructor(private dataSource: DataSource) {}
}
내가 트랜잭션을 쓸 부분에 공식 사이트에 나와있는 코드를 적용하면 된다.
// 공식사이트 코드
async createMany(users: User[]) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(users[0]);
await queryRunner.manager.save(users[1]);
await queryRunner.commitTransaction();
} catch (err) {
// since we have errors lets rollback the changes we made
await queryRunner.rollbackTransaction();
} finally {
// you need to release a queryRunner which was manually instantiated
await queryRunner.release();
}
}
@Injectable()
export class PaymentService {
constructor(
@InjectRepository(Payment)
private paymentRepository: Repository<Payment>,
@InjectRepository(Seat)
private seatRepository: Repository<Seat>,
@InjectRepository(Performance)
private performanceRepository: Repository<Performance>,
@InjectRepository(Schedule)
private scheduleRepository: Repository<Schedule>,
@InjectRepository(Point)
private pointRepository: Repository<Point>,
private dataSource: DataSource,
) {}
async create(
user: any,
schedule_id: any,
performance_id: any,
createPaymentDto: CreatePaymentDto,
createSeatDto: CreateSeatDto,
) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const { seats } = createSeatDto;
// 유저가 선택한 공연
const targetPerformance = await this.performanceRepository.findOne({
where: { id: +performance_id },
});
// 유저가 선택한 공연 스케줄
const targetSchedule = await this.scheduleRepository.find({
where: { performance: { id: +performance_id } },
});
const getSheduleWithId = await this.scheduleRepository.find({
where: { id: schedule_id },
});
// 스케쥴 아이디에 따른 좌석들, 좌석 카운트용
const getSeatCountWithScheduleId = await this.seatRepository.find({
where: { schedule: { id: +schedule_id } },
});
let total_price = 0;
if (!targetPerformance) {
// targetPerformance가 null인 경우 예외 처리
throw new Error('해당하는 공연이 없습니다.');
}
// 결제 생성
const newPayment = await queryRunner.manager.save(Payment, {
performance: { id: +performance_id },
total_price,
user_id: user.id,
});
// 등급별 좌석 금액
let totalSeatPrice = 0;
for (let i = 0; i < seats.length; i++) {
const newGrade = seats[i].grade;
const newSeatNum = seats[i].seat_num;
let seatPriceWithGrade: number = 0;
if (
newGrade === 'V' &&
getSeatCountWithScheduleId.length < getSheduleWithId[0].vip_seat_limit
) {
seatPriceWithGrade = targetPerformance.price * 1.75;
} else if (
newGrade === 'R' &&
getSeatCountWithScheduleId.length <
getSheduleWithId[0].royal_seat_limit
) {
seatPriceWithGrade = targetPerformance.price * 1.25;
} else if (
newGrade === 'S' &&
getSeatCountWithScheduleId.length <
getSheduleWithId[0].standard_seat_limit
) {
seatPriceWithGrade = targetPerformance.price;
}
// 좌석이 예매됐는지 확인
// 됐으면 payment도 x
const reservedSeat = await queryRunner.manager.findOne(Seat, {
where: { grade: newGrade, seat_num: newSeatNum },
});
console.log('reservedSeat: ', reservedSeat);
if (reservedSeat !== null) {
throw new Error();
// return { success: false, message: '이미 예약된 좌석입니다.' };
}
const newSeat = await queryRunner.manager.save(Seat, {
payment: { id: newPayment.id },
schedule: schedule_id,
grade: newGrade,
seat_num: newSeatNum,
performance: targetPerformance.id,
seat_price: seatPriceWithGrade, // seat_price 값을 targetPerformance.price로 설정
user: { id: user.id },
});
...
// 트랜잭션 커밋
await queryRunner.commitTransaction();
return { success: true, message: 'Reservation successful', total_price };
} catch (error)
// 에러가 생기면 롤백
await queryRunner.rollbackTransaction();
} finally {
// 사용이 끝난 후에는 항상 queryRunner를 해제
await queryRunner.release();
}
}
...
}