Rozmowa Spring Boot: Propagacja Transakcji

Opanuj propagację transakcji w Spring Boot: REQUIRED, REQUIRES_NEW, NESTED i więcej. 12 pytań rekrutacyjnych z kodem i typowymi pułapkami.

Spring Boot Propagacja Transakcji: pytania rekrutacyjne i praktyczne przykłady

Propagacja transakcji stanowi fundamentalne pojęcie w Spring Boot, regularnie sprawdzane podczas rozmów technicznych. Zrozumienie, jak transakcje współdziałają między metodami z adnotacją @Transactional, pomaga unikać subtelnych błędów produkcyjnych i pozwala projektować solidne architektury.

Wskazówka przygotowawcza

Rekruterzy oceniają zdolność wyboru właściwego poziomu propagacji w zależności od kontekstu biznesowego. Umiejętność wyjaśnienia, dlaczego REQUIRES_NEW zamiast REQUIRED w konkretnym przypadku, robi różnicę.

Podstawy propagacji transakcji

Pytanie 1: Czym jest propagacja transakcji w Spring?

Propagacja definiuje zachowanie metody transakcyjnej, gdy jest wywoływana w kontekście istniejącej transakcji. Odpowiada na pytanie: „Co dzieje się, gdy metoda @Transactional wywołuje inną metodę również z adnotacją?"

OrderService.javajava
// Demonstracja koncepcji propagacji
@Service
public class OrderService {

    private final PaymentService paymentService;
    private final OrderRepository orderRepository;

    // Transakcja nadrzędna - rozpoczyna nową transakcję
    @Transactional
    public void createOrder(OrderRequest request) {
        // Zapisuje zamówienie w bieżącej transakcji
        Order order = orderRepository.save(new Order(request));

        // Wywołanie innej metody @Transactional
        // Propagacja decyduje: ta sama transakcja czy nowa?
        paymentService.processPayment(order.getId(), request.getAmount());
    }
}

@Service
public class PaymentService {

    // Domyślna propagacja: REQUIRED
    // Dołącza do istniejącej transakcji z createOrder()
    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        // Wykonuje się w TEJ SAMEJ transakcji co createOrder()
        // Jeśli ta metoda zawiedzie, zamówienie również zostanie cofnięte
    }
}

Spring zapewnia siedem poziomów propagacji, każdy dostosowany do konkretnych potrzeb biznesowych. Wybór wpływa bezpośrednio na spójność danych i wydajność.

Pytanie 2: Opisz zachowanie REQUIRED (domyślna propagacja)

REQUIRED to domyślna propagacja. Jeśli istnieje transakcja, metoda do niej dołącza. W przeciwnym razie tworzona jest nowa. To najczęstsze i najbardziej intuicyjne zachowanie.

UserService.javajava
// REQUIRED: domyślne zachowanie
@Service
public class UserService {

    private final UserRepository userRepository;
    private final AuditService auditService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void updateUser(Long userId, UserUpdateRequest request) {
        // Rozpoczyna transakcję, jeśli żadna nie istnieje
        User user = userRepository.findById(userId).orElseThrow();
        user.setEmail(request.getEmail());
        userRepository.save(user);

        // Audyt dołącza do tej samej transakcji
        auditService.logUpdate(userId, "EMAIL_CHANGED");
    }
}

@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public void logUpdate(Long userId, String action) {
        // Dołącza do transakcji updateUser()
        // Commit lub rollback razem
        auditLogRepository.save(new AuditLog(userId, action, Instant.now()));
    }
}

Poniższy diagram ilustruje przepływ transakcyjny:

text
updateUser() rozpoczyna TX-1
    ├── save(user)          → TX-1
    └── logUpdate()         → dołącza do TX-1 (REQUIRED)
           └── save(audit)  → TX-1

Jeśli logUpdate() zawiedzie → rollback TX-1 → user I audit anulowane

Pytanie 3: Kiedy używać REQUIRES_NEW zamiast REQUIRED?

REQUIRES_NEW zawiesza istniejącą transakcję i tworzy nową niezależną transakcję. Przydatne, gdy operacja musi zostać zatwierdzona niezależnie od wyniku transakcji nadrzędnej.

PaymentService.javajava
// REQUIRES_NEW: niezależna transakcja
@Service
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final PaymentAuditService auditService;

    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        Payment payment = new Payment(orderId, amount);
        paymentRepository.save(payment);

        // Audyt MUSI być utrwalony nawet jeśli płatność później zawiedzie
        auditService.logPaymentAttempt(orderId, amount);

        // Symulacja błędu po audycie
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new InvalidAmountException("Negative amount");
        }
    }
}

@Service
public class PaymentAuditService {

    private final PaymentAuditRepository auditRepository;

    // REQUIRES_NEW: zatwierdza niezależnie od transakcji nadrzędnej
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPaymentAttempt(Long orderId, BigDecimal amount) {
        // Utworzona nowa transakcja TX-2
        // TX-1 (processPayment) jest zawieszona
        auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED"));
        // TX-2 zatwierdza tutaj, niezależnie od TX-1
    }
}

Przepływ transakcyjny z REQUIRES_NEW:

text
processPayment() rozpoczyna TX-1
    ├── save(payment)              → TX-1
    ├── logPaymentAttempt()        → TX-1 ZAWIESZONA
    │       └── rozpoczyna TX-2    → nowa transakcja
    │       └── save(audit)        → TX-2
    │       └── COMMIT TX-2        → audit utrwalony
    │       └── TX-1 WZNAWIA
    └── throw InvalidAmountException
        └── ROLLBACK TX-1          → płatność anulowana, ale audit zachowany
Uważaj na deadlocki

REQUIRES_NEW może powodować deadlocki, jeśli nowa transakcja uzyskuje dostęp do tych samych zasobów zablokowanych przez zawieszoną transakcję. Unikaj używania REQUIRES_NEW do modyfikowania tych samych tabel co transakcja nadrzędna.

Pytanie 4: Wyjaśnij propagację NESTED i czym różni się od REQUIRES_NEW

NESTED tworzy savepoint w obrębie bieżącej transakcji. Jeśli metoda zawiedzie, cofane są tylko zmiany od savepointa, a nie cała transakcja nadrzędna.

BatchProcessingService.javajava
// NESTED: savepoint w transakcji nadrzędnej
@Service
public class BatchProcessingService {

    private final ItemRepository itemRepository;
    private final ItemProcessor itemProcessor;

    @Transactional
    public BatchResult processBatch(List<Item> items) {
        BatchResult result = new BatchResult();

        for (Item item : items) {
            try {
                // Każdy element przetwarzany jest z savepointem
                itemProcessor.processItem(item);
                result.addSuccess(item.getId());
            } catch (ProcessingException e) {
                // Rollback tylko tego elementu, nie całego batcha
                result.addFailure(item.getId(), e.getMessage());
            }
        }

        return result; // Commit udanych elementów
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    // NESTED: tworzy savepoint, częściowy rollback możliwy
    @Transactional(propagation = Propagation.NESTED)
    public void processItem(Item item) {
        item.setStatus("PROCESSING");
        itemRepository.save(item);

        // Walidacja biznesowa
        if (!isValid(item)) {
            throw new ProcessingException("Invalid item");
            // Rollback do savepointa → tylko ten element
        }

        item.setStatus("COMPLETED");
        itemRepository.save(item);
    }
}

Porównanie NESTED vs REQUIRES_NEW:

text
NESTED:
├── Używa savepointa w TX nadrzędnej
├── W przypadku błędu → rollback do savepointa
├── Jeśli TX nadrzędna robi rollback → NESTED również się cofa
└── Wydajniejsze (brak nowego połączenia)

REQUIRES_NEW:
├── Tworzy całkowicie niezależną transakcję
├── W przypadku błędu → rollback tylko TX potomnej
├── Jeśli TX nadrzędna robi rollback → TX potomna JUŻ ZATWIERDZONA
└── Wymaga nowego połączenia
Wsparcie NESTED

Propagacja NESTED wymaga wsparcia savepointów JDBC. Większość nowoczesnych baz danych (PostgreSQL, MySQL, Oracle) to wspiera. Sprawdź zgodność przed użyciem.

Zaawansowane typy propagacji

Pytanie 5: Kiedy używać SUPPORTS i NOT_SUPPORTED?

SUPPORTS wykonuje się w istniejącej transakcji, jeśli istnieje, w przeciwnym razie bez transakcji. NOT_SUPPORTED zawiesza istniejącą transakcję i wykonuje się bez transakcji.

ReportingService.javajava
// SUPPORTS: opcjonalna transakcja
@Service
public class ReportingService {

    private final ReportRepository reportRepository;

    // SUPPORTS: działa z transakcją lub bez
    // Przydatne dla odczytów niewymagających gwarancji transakcyjnych
    @Transactional(propagation = Propagation.SUPPORTS)
    public Report generateReport(Long reportId) {
        // Jeśli wywoływana z metody @Transactional → używa jej TX
        // Jeśli wywoływana bezpośrednio → bez transakcji (odczyt OK)
        return reportRepository.generateComplexReport(reportId);
    }
}

@Service
public class ExternalNotificationService {

    private final ExternalApiClient apiClient;

    // NOT_SUPPORTED: nigdy w transakcji
    // Unika blokowania TX podczas wolnego wywołania zewnętrznego
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void sendExternalNotification(String message) {
        // TX nadrzędna zawieszona podczas wywołania
        apiClient.send(message); // Potencjalnie wolne wywołanie HTTP
        // TX nadrzędna wznawia po
    }
}
OrderService.javajava
// Przykład łącznego użycia
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final ReportingService reportingService;
    private final ExternalNotificationService notificationService;

    @Transactional
    public void completeOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("COMPLETED");
        orderRepository.save(order);

        // Generowanie raportu w tej samej TX (SUPPORTS)
        Report report = reportingService.generateReport(orderId);

        // Powiadomienie zewnętrzne POZA transakcją (NOT_SUPPORTED)
        // Unika timeoutu TX, jeśli zewnętrzne API jest wolne
        notificationService.sendExternalNotification(
            "Order " + orderId + " completed"
        );
    }
}

Pytanie 6: Wyjaśnij MANDATORY i NEVER

MANDATORY wymaga istniejącej transakcji i rzuca wyjątek w przeciwnym razie. NEVER wymaga braku transakcji i rzuca wyjątek, jeśli istnieje.

AuditService.javajava
// MANDATORY: musi być wywołane z wnętrza transakcji
@Service
public class AuditService {

    private final AuditRepository auditRepository;

    // MANDATORY: odmawia wykonania bez istniejącej transakcji
    // Gwarantuje, że audyt jest zawsze atomowy z audytowaną operacją
    @Transactional(propagation = Propagation.MANDATORY)
    public void logCriticalAction(String action, Long entityId) {
        // Jeśli wywołane bez transakcji → IllegalTransactionStateException
        auditRepository.save(new AuditLog(action, entityId, Instant.now()));
    }
}

// CacheService.java
// NEVER: nigdy nie może być w transakcji
@Service
public class CacheService {

    private final CacheManager cacheManager;

    // NEVER: cache nie powinien uczestniczyć w transakcjach
    // Unika niespójności między cache a DB po rollbacku
    @Transactional(propagation = Propagation.NEVER)
    public void invalidateCache(String cacheKey) {
        // Jeśli wywołane z @Transactional → IllegalTransactionStateException
        cacheManager.getCache("entities").evict(cacheKey);
    }
}
SecurityService.javajava
// Poprawne użycie MANDATORY
@Service
public class SecurityService {

    private final AuditService auditService;

    @Transactional
    public void changeUserPassword(Long userId, String newPassword) {
        // Wrażliwa operacja...
        updatePassword(userId, newPassword);

        // Audyt MUSI być w tej samej transakcji
        // MANDATORY wymusza to ograniczenie architektoniczne
        auditService.logCriticalAction("PASSWORD_CHANGE", userId);
    }

    // BŁĄD: bezpośrednie wywołanie bez transakcji
    public void badUsage() {
        // Rzuca IllegalTransactionStateException, ponieważ brak TX
        auditService.logCriticalAction("TEST", 1L);
    }
}

Gotowy na rozmowy o Spring Boot?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Typowe pułapki rekrutacyjne

Pytanie 7: Dlaczego @Transactional nie działa przy wywołaniach wewnętrznych?

Jedna z najczęstszych pułapek. Wywołania wewnętrzne metod (self-invocation) omijają proxy Springa, wyłączając zarządzanie transakcjami.

BrokenService.javajava
// KLASYCZNY BŁĄD: self-invocation
@Service
public class BrokenService {

    private final ItemRepository itemRepository;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // PUŁAPKA: wywołanie wewnętrzne → omija proxy
            // @Transactional na processItem() jest IGNOROWANE
            processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // Ta transakcja NIGDY nie jest tworzona przy wywołaniach wewnętrznych
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Rozwiązania, aby uniknąć tej pułapki:

java
// Rozwiązanie 1: Self-injection
@Service
public class FixedServiceWithSelfInjection {

    private final ItemRepository itemRepository;

    @Lazy
    @Autowired
    private FixedServiceWithSelfInjection self; // Self-injection

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Wywołanie przez proxy → @Transactional działa
            self.processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // Transakcja poprawnie utworzona
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

// Rozwiązanie 2: Podział na dwa serwisy (zalecane)
@Service
public class ItemOrchestrator {

    private final ItemProcessor processor;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Wywołanie do innego beana → proxy działa
            processor.processItem(id);
        }
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    @Transactional
    public void processItem(Long itemId) {
        // Transakcja poprawnie zarządzana
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Pytanie 8: Jak obsługiwać wyjątki i rollback?

Domyślnie Spring robi rollback tylko dla RuntimeException i Error. Wyjątki checked NIE wywołują automatycznego rollbacka.

TransactionRollbackDemo.javajava
// Zachowanie rollbacka w zależności od typu wyjątku
@Service
public class TransactionRollbackDemo {

    private final OrderRepository orderRepository;

    // RuntimeException → automatyczny ROLLBACK
    @Transactional
    public void methodWithRuntimeException() {
        orderRepository.save(new Order());
        throw new RuntimeException("Error"); // ROLLBACK
    }

    // Checked Exception → BRAK rollbacka domyślnie
    @Transactional
    public void methodWithCheckedException() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("File error"); // I tak COMMIT!
    }

    // Wymuszenie rollbacka przy checked exception
    @Transactional(rollbackFor = IOException.class)
    public void methodWithRollbackFor() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("Error"); // ROLLBACK dzięki rollbackFor
    }

    // Wykluczenie RuntimeException z rollbacka
    @Transactional(noRollbackFor = BusinessException.class)
    public void methodWithNoRollbackFor() {
        orderRepository.save(new Order());
        throw new BusinessException("Warning"); // COMMIT mimo wyjątku
    }
}

Zalecana konfiguracja dla przypadków biznesowych:

BaseTransactionalService.javajava
// Spójna konfiguracja transakcyjna
@Service
public abstract class BaseTransactionalService {

    // Rollback dla wszystkich wyjątków (checked i unchecked)
    @Transactional(rollbackFor = Exception.class)
    protected void executeInTransaction(Runnable operation) {
        operation.run();
    }
}

// PaymentService.java
@Service
public class PaymentService extends BaseTransactionalService {

    private final PaymentRepository paymentRepository;

    @Transactional(
        rollbackFor = Exception.class,
        noRollbackFor = InsufficientFundsException.class
    )
    public PaymentResult processPayment(PaymentRequest request) {
        Payment payment = new Payment(request);
        paymentRepository.save(payment);

        if (request.getAmount().compareTo(getBalance()) > 0) {
            // BRAK rollbacka - chcemy zachować zapis próby
            throw new InsufficientFundsException("Insufficient balance");
        }

        return new PaymentResult(payment.getId(), "SUCCESS");
    }
}

Pytanie 9: Jak działa izolacja transakcji z propagacją?

Izolacja i propagacja są komplementarne. Izolacja określa widoczność danych między równoległymi transakcjami.

IsolationDemo.javajava
// Połączenie izolacji + propagacji
@Service
public class IsolationDemo {

    private final AccountRepository accountRepository;

    // READ_COMMITTED: widzi dane zatwierdzone przez inne transakcje
    @Transactional(
        isolation = Isolation.READ_COMMITTED,
        propagation = Propagation.REQUIRED
    )
    public BigDecimal getAccountBalance(Long accountId) {
        // Może zobaczyć różne wartości przy ponownym odczycie podczas TX
        return accountRepository.findById(accountId)
            .map(Account::getBalance)
            .orElse(BigDecimal.ZERO);
    }

    // REPEATABLE_READ: gwarantuje ten sam odczyt podczas transakcji
    @Transactional(
        isolation = Isolation.REPEATABLE_READ,
        propagation = Propagation.REQUIRED
    )
    public void transferWithConsistentRead(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();

        // Nawet jeśli inna transakcja zmodyfikuje 'from' w międzyczasie,
        // zawsze widzimy wartość początkową (snapshot)
        from.debit(amount);
        to.credit(amount);

        accountRepository.save(from);
        accountRepository.save(to);
    }

    // SERIALIZABLE: maksymalna izolacja, brak współbieżności
    @Transactional(
        isolation = Isolation.SERIALIZABLE,
        propagation = Propagation.REQUIRES_NEW
    )
    public void criticalOperation(Long accountId) {
        // Blokuje każdą inną transakcję na tych danych
        // Używać oszczędnie - wpływ na wydajność
        Account account = accountRepository.findById(accountId).orElseThrow();
        account.performCriticalUpdate();
        accountRepository.save(account);
    }
}

Tabela podsumowująca poziomy izolacji:

text
| Izolacja         | Dirty Read | Non-Repeatable | Phantom |
|------------------|------------|----------------|---------|
| READ_UNCOMMITTED | Możliwe    | Możliwe        | Możliwe |
| READ_COMMITTED   | Nie        | Możliwe        | Możliwe |
| REPEATABLE_READ  | Nie        | Nie            | Możliwe |
| SERIALIZABLE     | Nie        | Nie            | Nie     |
Wpływ na wydajność

Im bardziej rygorystyczna izolacja, tym bardziej wydajność może być dotknięta przez locki. SERIALIZABLE może powodować znaczącą rywalizację w produkcji o wysokim ruchu.

Zaawansowane wzorce

Pytanie 10: Jak zaimplementować wzorzec „transactional outbox"?

Wzorzec outbox gwarantuje spójność między modyfikacjami w bazie danych a wysyłaniem wiadomości/zdarzeń, nawet w przypadku awarii.

OutboxService.javajava
// Wzorzec Transactional Outbox
@Service
public class OutboxService {

    private final OutboxRepository outboxRepository;

    // Zapisuje zdarzenie w tej samej transakcji co encja biznesowa
    @Transactional(propagation = Propagation.MANDATORY)
    public void saveEvent(String aggregateType, Long aggregateId, String eventType, String payload) {
        OutboxEvent event = OutboxEvent.builder()
            .aggregateType(aggregateType)
            .aggregateId(aggregateId)
            .eventType(eventType)
            .payload(payload)
            .status("PENDING")
            .createdAt(Instant.now())
            .build();
        outboxRepository.save(event);
    }
}

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final OutboxService outboxService;

    @Transactional
    public Order createOrder(OrderRequest request) {
        // Utworzenie zamówienia
        Order order = new Order(request);
        orderRepository.save(order);

        // Zdarzenie outbox w TEJ SAMEJ transakcji (MANDATORY)
        // Jeśli commit się powiedzie → oba są utrwalone
        // Jeśli rollback → żadne nie jest utrwalone
        outboxService.saveEvent(
            "ORDER",
            order.getId(),
            "ORDER_CREATED",
            toJson(new OrderCreatedEvent(order))
        );

        return order;
    }
}

// OutboxPublisher.java
// Oddzielny proces publikujący zdarzenia
@Service
public class OutboxPublisher {

    private final OutboxRepository outboxRepository;
    private final MessageBroker messageBroker;

    // Niezależna transakcja dla każdej publikacji
    @Scheduled(fixedDelay = 1000)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void publishPendingEvents() {
        List<OutboxEvent> events = outboxRepository
            .findByStatusOrderByCreatedAt("PENDING");

        for (OutboxEvent event : events) {
            try {
                messageBroker.publish(event.getEventType(), event.getPayload());
                event.setStatus("PUBLISHED");
                event.setPublishedAt(Instant.now());
            } catch (Exception e) {
                event.setStatus("FAILED");
                event.setError(e.getMessage());
            }
            outboxRepository.save(event);
        }
    }
}

Pytanie 11: Jak testować różne zachowania propagacji?

Testy propagacji wymagają szczególnej uwagi, aby zweryfikować oczekiwane zachowanie transakcyjne.

TransactionPropagationTest.javajava
// Testowanie zachowań propagacji
@SpringBootTest
@Transactional
class TransactionPropagationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void required_shouldShareTransaction() {
        // Given
        Order order = orderService.createOrder(new OrderRequest());

        // When - płatność w tej samej transakcji (REQUIRED)
        paymentService.processPayment(order.getId(), BigDecimal.TEN);

        // Then - oba widoczne przed commitem
        entityManager.flush();
        assertThat(entityManager.find(Order.class, order.getId())).isNotNull();
        assertThat(entityManager.find(Payment.class, order.getId())).isNotNull();
    }

    @Test
    void requiresNew_shouldCommitIndependently() {
        // Given
        Long orderId = null;

        try {
            // When - audit w REQUIRES_NEW
            orderId = orderService.createOrderWithAudit(new OrderRequest());
            throw new RuntimeException("Simulated failure after audit");
        } catch (RuntimeException e) {
            // Główna transakcja robi rollback
        }

        // Then - audit (REQUIRES_NEW) nadal zatwierdzony
        assertThat(findAuditLog(orderId)).isNotNull();
        // Ale zamówienie zostało cofnięte
        assertThat(findOrder(orderId)).isNull();
    }

    @Test
    void mandatory_shouldThrowWithoutTransaction() {
        // Given - brak aktywnej transakcji

        // When/Then - musi rzucić wyjątek
        assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L))
            .isInstanceOf(IllegalTransactionStateException.class)
            .hasMessageContaining("No existing transaction");
    }
}
PropagationIntegrationTest.javajava
// Test integracyjny z rzeczywistym rollbackiem i commitem
@SpringBootTest
class PropagationIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private AuditLogRepository auditLogRepository;

    @Test
    void nested_shouldRollbackOnlyNestedOnFailure() {
        // Given
        int initialCount = orderRepository.findAll().size();

        // When - przetwarzanie batchowe z NESTED
        BatchResult result = orderService.processBatchWithNested(
            List.of(validItem(), invalidItem(), validItem())
        );

        // Then - tylko poprawne elementy są utrwalone
        assertThat(result.getSuccessCount()).isEqualTo(2);
        assertThat(result.getFailureCount()).isEqualTo(1);
        assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2);
    }
}

Pytanie 12: Jakie są najlepsze praktyki konfiguracji transakcji?

Spójna konfiguracja transakcyjna na poziomie projektu unika niespodzianek i ułatwia utrzymanie.

TransactionConfig.javajava
// Scentralizowana konfiguracja transakcji
@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    // Domyślny timeout dla wszystkich transakcji
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
        tm.setDefaultTimeout(30); // 30 sekund maksymalnie na transakcję
        return tm;
    }
}

// BaseService.java
// Domyślne adnotacje dla serwisów
@Service
@Transactional(
    readOnly = true, // Tylko do odczytu domyślnie
    rollbackFor = Exception.class // Rollback przy każdym wyjątku
)
public abstract class BaseService {
    // Metody odczytu dziedziczą readOnly = true
}

// OrderService.java
// Serwis ze spójną konfiguracją
@Service
public class OrderService extends BaseService {

    private final OrderRepository orderRepository;

    // Dziedziczy readOnly = true
    public Order findById(Long id) {
        return orderRepository.findById(id).orElseThrow();
    }

    // Override dla zapisów
    @Transactional(readOnly = false)
    public Order createOrder(OrderRequest request) {
        return orderRepository.save(new Order(request));
    }

    // Jawna konfiguracja dla przypadków krytycznych
    @Transactional(
        readOnly = false,
        propagation = Propagation.REQUIRES_NEW,
        isolation = Isolation.SERIALIZABLE,
        timeout = 10
    )
    public void criticalOperation(Long orderId) {
        // Operacja z maksymalną izolacją i krótkim timeoutem
    }
}

Checklista najlepszych praktyk:

text
Konfiguracja transakcji - Checklista

✅ readOnly = true domyślnie, jawny override dla zapisów
✅ rollbackFor = Exception.class aby uwzględnić checked exceptions
✅ Odpowiedni timeout w zależności od typu operacji
✅ Unikać wywołań wewnętrznych (@Transactional ignorowane)
✅ REQUIRES_NEW tylko gdy niezależny commit jest konieczny
✅ MANDATORY aby zagwarantować kontekst transakcyjny
✅ Jawne testy zachowań rollbacka
✅ Monitorować długotrwałe transakcje
✅ Dokumentować niestandardowe wybory propagacji

Gotowy na rozmowy o Spring Boot?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Podsumowanie

Propagacja transakcji to fundamentalne pojęcie oceniane w rozmowach Spring Boot. Kluczowe punkty do zapamiętania:

Typowe rodzaje propagacji:

  • ✅ REQUIRED (domyślny): dołącza lub tworzy transakcję
  • ✅ REQUIRES_NEW: niezależna transakcja, oddzielny commit
  • ✅ NESTED: savepoint dla częściowego rollbacka
  • ✅ MANDATORY: wymaga istniejącej transakcji

Pułapki do uniknięcia:

  • ✅ Self-invocation: omija proxy, @Transactional ignorowane
  • ✅ Checked exceptions: brak rollbacka domyślnie
  • ✅ REQUIRES_NEW na tych samych danych: ryzyko deadlocka
  • ✅ Brak timeoutu: transakcje zablokowane bezterminowo

Najlepsze praktyki:

  • ✅ readOnly = true domyślnie
  • ✅ rollbackFor = Exception.class systematycznie
  • ✅ Oddzielne serwisy aby uniknąć self-invocation
  • ✅ Jawnie testować zachowania rollbacka

Opanowanie propagacji transakcji pokazuje głębokie zrozumienie Springa i zarządzania danymi. Te koncepcje są niezbędne do projektowania solidnych aplikacji i pomyślnego przechodzenia rozmów technicznych.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#spring boot
#transactions
#propagation
#java
#interview

Udostępnij

Powiązane artykuły