이 가이드는 비침해적인 트랜잭션으로 데이터베이스 작업을 래핑하는 프로세스를 안내합니다.
특별한 JDBC 코드를 작성할 필요 없이 트랜잭션 데이터베이스 작업을 수행하는 간단한 JDBC 애플리케이션을 구축합니다.
먼저, BookingService
클래스를 사용하여 이름으로 시스템에 사람들을 예약하는 JDBC 기반 서비스를 만들어야 합니다. 다음 목록(src/main/java/guides/managingtransactions/BookingService.java)에서는 이를 수행하는 방법을 보여줍니다.
package guides.managingtransactions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Component
public class BookingService {
private final static Logger logger = LoggerFactory.getLogger(BookingService.class);
private final JdbcTemplate jdbcTemplate;
public BookingService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Transactional
public void book(String... persons) {
for (String person : persons) {
logger.info("Booking " + person + "in a seat...");
jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", person);
}
}
public List<String> findAllBookings() {
return jdbcTemplate.query("select FIRST_NAME from BOOKINGS", (rs, rowNum) -> rs.getString("FIRST_NAME"));
}
}
코드에는 나머지 코드에 필요한 모든 데이터베이스 상호 작용을 수행하는 편리한 템플릿 클래스인 자동 연결 JdbcTemplate
이 있습니다.
여러 사람을 예약할 수 있는 book
method도 있습니다. 사람 목록을 반복하고 각 사람에 대해 JdbcTemplate
을 사용하여 해당 사람을 BOOKINGS
테이블에 삽입합니다. 이 메서드에는 @Transactional
태그가 지정되어 있습니다. 이는 오류가 발생하면 전체 작업이 이전 상태로 롤백되고 원래 예외가 다시 발생함(re-throw)을 의미합니다. 즉, 한 사람이 추가되지 않으면 어떤 사람도 BOOKINGS
에 추가되지 않습니다.
데이터베이스를 쿼리하는 findAllBookings
메서드도 있습니다. 데이터베이스에서 가져온(fetched) 각 행은 String
로 변환되고 모든 행은 List
으로 결합됩니다.
Spring 초기화는 애플리케이션 클래스를 제공합니다. 이 경우 이 애플리케이션 클래스를 수정할 필요가 없습니다. 다음 목록(src/main/java/guides/managingtransactions/ManagingTransactionsApplication.java에서)은 애플리케이션 클래스를 보여줍니다.
package guides.managingtransactions;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ManagingTransactionsApplication {
public static void main(String[] args) {
SpringApplication.run(ManagingTransactionsApplication.class, args);
}
}
귀하의 애플리케이션에는 실제로 구성이 없습니다. Spring Boot는 classpath에서 spring-jdbc
와 h2
를 감지하고 자동으로 DataSource
와 JdbcTemplate
을 생성합니다. 이제 이 인프라를 사용할 수 있고 전용 구성이 없기 때문에 DataSourceTransactionManager
도 생성됩니다. 이는 @Transactional
로 주석이 달린 메서드(예: BookingService
의 book
메서드)를 가로채는 구성 요소입니다. BookingService
는 클래스 경로 검색을 통해 감지됩니다.
이 가이드에서 설명하는 또 다른 Spring Boot 기능은 시작 시 스키마를 초기화하는 기능입니다. 다음 파일(src/main/resources/schema.sql)은 데이터베이스 스키마를 정의합니다.
drop table BOOKINGS if exists;
create table BOOKINGS(ID serial, FIRST_NAME varchar(5) NOT NULL);
BookingService
를 주입하고 다양한 트랜잭션 사용 사례를 보여주는 CommandLineRunner
도 있습니다. 다음 목록(src/main/java/guides/managingtransactions/AppRunner.java)은 명령줄 실행기를 보여줍니다.
package guides.managingtransactions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@Component
public class AppRunner implements CommandLineRunner {
private final static Logger logger = LoggerFactory.getLogger(AppRunner.class);
private final BookingService bookingService;
public AppRunner(BookingService bookingService) {
this.bookingService = bookingService;
}
@Override
public void run(String... args) throws Exception {
bookingService.book("Alice", "Bob", "Carol");
Assert.isTrue(bookingService.findAllBookings().size() == 3, "First booking should work with no problem");
logger.info("Alice, Bob and Carol have been booked");
try {
bookingService.book("Chris", "Samuel");
} catch (RuntimeException e) {
logger.info("v--- The following exception is expect because 'Samuel' is too "+"big for the DB ---v");
logger.error(e.getMessage());
}
for (String person : bookingService.findAllBookings()) {
logger.info("So far, "+ person +"is booked.");
}
logger.info("You shouldn't see Chris or Samuel. Samuel violated DB constraints. " + "and Chris was rolled back in the same TX");
Assert.isTrue(bookingService.findAllBookings().size()==3, "'Samuel' should have triggered a rollback");
try {
bookingService.book("Buddy", null);
} catch (RuntimeException e) {
logger.info("v--- The following exception is except because null is not "+"valid for the DB ---v");
logger.error(e.getMessage());
}
for (String person : bookingService.findAllBookings()) {
logger.info("So far, " + person + "is booked");
}
logger.info("You shouldn't see Buddy or null. null violated DB constraints, and " + "Buddy was rolled back in the same TX");
Assert.isTrue(bookingService.findAllBookings().size() == 3, "'null' should have triggered a rollback");
}
}
BOOKINGS
테이블에는 first_name
열에 대한 두 가지 제약 조건이 있습니다.
삽입된 처음 세 이름은 Alice
, Bob
및 Carol
입니다. 애플리케이션에서는 해당 테이블에 세 사람이 추가되었다고 주장(Assert.isTrue(...)
)합니다. 이것이 작동하지 않았다면 애플리케이션이 일찍 종료되었을 것입니다.
다음으로 Chris
와 Samuel
에 대한 또 다른 예약이 이루어집니다. Samuel의 이름은 의도적으로 너무 길어서 삽입 오류가 발생했습니다. 트랜잭션 동작은 Chris
와 Samuel
(즉, 이 트랜잭션의 모든 값)이 모두 롤백되어야 함을 규정합니다. 따라서 해당 테이블에는 여전히 3명의 사람만 있어야 하며, 이는 주장(assertion)이 보여줍니다.
마지막으로 Buddy
와 null
이 예약되었습니다. 출력에 표시된 대로 null
로 인해 롤백도 발생하여 동일한 3명이 예약됩니다.