음식 주문 정보는 다음과 같은 도메인 클래스를 사용한다.
public class Orders {
private Long id;
private long userId;
private int totalPrice;
private LocalDateTime createdAt;
private List<OrderItems> orderItems;
}
public class OrderItems {
private Long id;
private long orderId;
private long foodId;
private int priceAtOrder;
private int quantity;
}
일단 OrderItems가 들어있는 Orders 객체 생성 로직은 완성해놨다.
두 가지를 구현하면 된다.
이 Insert 묶음은 트랜잭션이 적용돼야 한다.
public class OrderRepository {
private final DBConfig dbConfig;
private static final String ORDERS_INSERT_SQL = """
INSERT INTO orders (user_id, total_price, created_at)
VALUES
(?, ?, ?)
""";
private static final String ORDER_ITEMS_INSERT_SQL = """
INSERT INTO order_items (order_id, food_id, price_at_order, quantity)
VALUES
(?, ?, ?, ?)
""";
public OrderRepository(DBConfig dbConfig) {
this.dbConfig = dbConfig;
}
public Orders save(Orders order) {
try(Connection conn = DriverManager.getConnection(dbConfig.getUrl(), dbConfig.getUsername(), dbConfig.getPassword());
PreparedStatement psOrder = conn.prepareStatement(ORDERS_INSERT_SQL, Statement.RETURN_GENERATED_KEYS)) { // Order 삽입 ps
conn.setAutoCommit(false);
psOrder.setLong(1, order.getUserId());
psOrder.setInt(2, order.getTotalPrice());
psOrder.setObject(3, order.getCreatedAt());
psOrder.executeUpdate();
try (ResultSet orderIdResultSet = psOrder.getGeneratedKeys()) { // Order 삽입으로 생성된 키 확인
orderIdResultSet.next();
long orderId = orderIdResultSet.getLong(1);
MyEntityIdInjector.injectId(order, orderId);
try(PreparedStatement psOrderItems = conn.prepareStatement(ORDER_ITEMS_INSERT_SQL, Statement.RETURN_GENERATED_KEYS)) { // OrderItems 삽입
for (OrderItems orderItem : order.getOrderItems()) {
psOrderItems.setLong(1, orderId);
psOrderItems.setLong(2, orderItem.getFoodId());
psOrderItems.setInt(3, orderItem.getPriceAtOrder());
psOrderItems.setInt(4, orderItem.getQuantity());
psOrderItems.executeUpdate();
try (ResultSet orderItemIdResultSet = psOrderItems.getGeneratedKeys()) { // OrderItems의 id, foodId 세팅
orderItemIdResultSet.next();
long orderItemId = orderItemIdResultSet.getLong(1);
MyEntityIdInjector.injectId(orderItem, orderItemId);
MyEntityIdInjector.injectId(orderItem, "orderId", orderId);
}
}
conn.commit();
return order;
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
문제점 및 개선점은 다음과 같다.
public Orders save(Orders order) {
try(Connection conn = DriverManager.getConnection(dbConfig.getUrl(), dbConfig.getUsername(), dbConfig.getPassword())) { // Order 삽입 ps
conn.setAutoCommit(false);
// 메소드로 분리함
insertOrder(conn, order);
insertOrderItems(conn, order.getOrderItems(), order.getId());
conn.commit();
return order;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
// Order 삽입
private void insertOrder(Connection conn, Orders order) throws SQLException {
try(PreparedStatement psOrder = conn.prepareStatement("""
INSERT INTO orders (user_id, total_price, created_at)
VALUES
(?, ?, ?)
""", Statement.RETURN_GENERATED_KEYS)) {
...
}
}
// OrderItems 삽입
private void insertOrderItems(Connection conn, List<OrderItems> orderItems, long orderId) throws SQLException {
try (PreparedStatement psOrderItems = conn.prepareStatement("""
INSERT INTO order_items (order_id, food_id, price_at_order, quantity)
VALUES
(?, ?, ?, ?)
""", Statement.RETURN_GENERATED_KEYS)) {
...
}
}
이제 좀 트랜잭션에서 뭘 하는지 보인다.
public Orders save(Orders order) {
try(Connection conn = DriverManager.getConnection(dbConfig.getUrl(), dbConfig.getUsername(), dbConfig.getPassword())) { // Order 삽입 ps
conn.setAutoCommit(false);
try {
insertOrder(conn, order);
insertOrderItems(conn, order.getOrderItems(), order.getId());
conn.commit();
} catch (SQLException e) {
conn.rollback(); // 롤백 처리
throw e;
}
return order;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
분리하고 나니까 롤백 커밋도 잘보인다.
지금 JPA 처럼 save를 구현하고 있는데 JPA는 ID가 있어야 객체를 관리할 수 있다.
그래서 saveAll을 호출할 때 JPA는 Insert문을 하나 날리고 ID 하나 받아오고를 반복해서 객체에 ID를 넣어준다.
Insert ... values에 여러 값을 한 번에 넣는다면 ID를 모두 받아올 수 있지만 순서를 보장하지 않기 때문에 JPA에서도 어쩔 수 없는 것 같다.
내 글 - JPA IDENTITY 방식은 Batch Insert가 비활성화 된다.
OrderItems N번 Insert 쿼리 발생은 그냥 이대로 둔다. JPA처럼 만든다.
주문의 저장을 확인하기 위해선 findById가 필요하다.
findById는 orders 테이블과 order_items 테이블의 조인이 필요하기 때문에 다음 글로 넘긴다.