장바구니에서 결제로 넘어갈 때, 많은 일들을 한 번에 수행하여야 한다.
예를 들면 order 테이블에 해당 유저의 정보를 입력해야하고, cart 테이블에 남아있던 해당 유저가 결제한 상품들의 정보를 지워야하고, 결제는 어떤 식으로 했으며, 해당 유저가 어떠한 상품들을 결제하였고 등등의 모든 일들이 한 번에 수행되어야만 한다.
만약 장바구니 페이지에서 결제 버튼을 눌러 사용자가 결제를 완료하였고, 결제 정보가 DB에 입력되었지만 어떠한 사정으로 인해 order 테이블에 해당 유저의 정보가 넘어가지 않았고, 장바구니 페이지에서 여전히 이를 확인할 수 있는 경우가 생긴다면 이는 사용자의 서비스의 불만족으로 이어질 뿐만 아니라, DB의 데이터를, 서비스를 믿을 수 없게 되는 상황까지 이어진다.
이를 방지하기 위해 트랜잭션이라는 개념을 알고 있어야만 한다. 분명 SQLD 공부할 때 배웠던 개념이었지만, 이를 프로젝트에 활용할 생각을 하지 못하고 있었다. 처음 PR을 올린 후, 호준님께서 멘토링을 진행해주셨고, 그때 트랜잭션에 대해 다시한번 생각할 수 있게 되었다.
트랜잭션(transaction)이란 데이터베이스 시스템에서 병행 제어 및 회복 작업 시 처리되는 작업의 논리적 단위이며, 하나의 트랜잭션은 commit 되거나 rollback 된다.
즉, 수행하여아 하는 모든 쿼리들을 하나의 단위로 묶고, 이 모든 쿼리들이 수행된다면 commit을, 중간에 오류가 생긴다면 수행된 쿼리들을 rollback하는 것이라고 생각하면 된다. 트랜잭션을 사용하면 쿼리 수행 시 오류가 생겼을 때 어떤 정보는 DB에 넘어가고, 어떤 정보는 DB에 넘어가지 않게 되는 불상사를 방지할 수 있게 된다.
따라서 DB 내에서 하나의 그룹으로 처리되어야 하는 명령문들을 모아 놓은 논리적인 작업 단위로서 여러 개의 명령어 집합이 정상적으로 처리되면 정상 종료되고, 하나의 명령어라도 잘못되면 전체 취소되는 것이 트랜잭션이다.
데이터의 일관성을 유지하면서 안정적으로 데이터를 복수할 수 있어 여러 쿼리들을 한 번에 수행해야 할 때에는 트랜잭션을 꼭 사용하여야 한다.
트랜잭션이 무엇인지는 알았지만 나는 여전히 궁금증이 생겼다. Node.js에서는, TypeORM에서는 어떤 식으로 트랜잭션을 사용해야 하는 것일까? 해당 정보는 이 링크에서 찾아볼 수 있었다.
QueryRunner
TypeORM은 QueryRunner
를 사용하여 트랜잭션을 실행할 수 있다.
QueryRunner
를 생성하고, connect()
를 통해 연결한 뒤, startTransaction()
을 통해 트랜잭션을 시작하고, commitTransaction()
, 에러가 발생되었을 때엔 rollbackTransaction()
을 통해 제어하고, release()
를 통해 QueryRunner
를 해제한다. 아래의 예처럼 쓸 수 있다.
// create a new query runner
const queryRunner = dataSource.createQueryRunner()
// establish real database connection using our new query runner
await queryRunner.connect()
// now we can execute any queries on a query runner, for example:
await queryRunner.query("SELECT * FROM users")
// we can also access entity manager that works with connection created by a query runner:
const users = await queryRunner.manager.find(User)
// lets now open a new transaction:
await queryRunner.startTransaction()
try {
// execute some operations on this transaction:
await queryRunner.manager.save(user1)
await queryRunner.manager.save(user2)
await queryRunner.manager.save(photos)
// commit transaction now:
await queryRunner.commitTransaction()
} catch (err) {
// since we have errors let's rollback changes we made
await queryRunner.rollbackTransaction()
} finally {
// you need to release query runner which is manually created:
await queryRunner.release()
}
const createOrder = async (userId, cartId, products, totalPrice) => {
if (!products.length) throwCustomError("NO_PRODUCTS", 400);
const queryRunner = appDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const createOrder = await queryRunner.query(
`INSERT INTO
orders (user_id, order_status_id)
VALUES
(${userId}, ${OrderStatusId.ORDER_DONE});
`
);
const orderId = createOrder.insertId;
await queryRunner.query(
`DELETE FROM
carts
WHERE
carts.id IN (?);
`,
[cartId]
);
await queryRunner.query(
`UPDATE
users
SET
points = points - ?
WHERE
id = ?
`,
[totalPrice, userId]
);
const query = `INSERT INTO
order_product (order_id, product_id, quantity, order_status_id)
VALUES ?;`;
let values = [];
for (let i = 0; i < products.length; i++) {
values.push([
orderId,
products[i].productId,
products[i].quantity,
`${OrderStatusId.ORDER_DONE}`,
]);
}
await queryRunner.query(query, [values]);
await queryRunner.query(
`INSERT INTO
payments (order_id, total_price, methods)
VALUES (?, ?, ${PaymentMethodId.USER_CREDIT})
`,
[orderId, totalPrice]
);
await queryRunner.commitTransaction();
} catch (err) {
console.log(err);
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
};