Как работает `Propagation.NESTED` в транзакциях Spring?

Ответ

Propagation.NESTED — это вариант распространения транзакций в Spring, который создает вложенную транзакцию (nested transaction) на основе точек сохранения (savepoints) в текущей физической транзакции.

Принцип работы:

  1. Если активной транзакции нет, поведение аналогично Propagation.REQUIRED (создает новую).
  2. Если активная транзакция существует, метод выполняется внутри вложенной транзакции (устанавливается savepoint).
  3. Откат вложенной транзакции откатывает изменения только к этому savepoint, не затрагивая внешнюю транзакцию.
  4. Фиксация вложенной транзакции происходит условно — изменения фиксируются в БД только при успешной фиксации внешней транзакции.

Пример:

@Service
public class OrderService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void processOrder(Order order) {
        // Основная логика заказа
        updateInventory(order); // Вложенная транзакция
        // Если здесь выбросится исключение, откатится ВСЁ (и inventory)
    }

    @Transactional(propagation = Propagation.NESTED)
    public void updateInventory(Order order) {
        // Логика обновления склада
        // Если здесь исключение, откатится ТОЛЬКО updateInventory,
        // но processOrder может продолжить обработку
    }
}

Ключевые ограничения и особенности:

  • Зависит от JDBC-драйвера: Работает только если драйвер БД (PostgreSQL, MySQL, Oracle и др.) поддерживает savepoints.
  • Не поддерживается JTA: Нельзя использовать с распределенными транзакциями (JTA).
  • Ограниченная поддержка в JPA: Поведение может отличаться. Для JPA предпочтительнее использовать отдельные транзакции (REQUIRES_NEW) или компенсирующие действия.
  • Использование: Полезен для сценариев, где часть бизнес-логики может безопасно откатиться независимо от основной операции.

Ответ 18+ 🔞

Да ты послушай, что за дичь творится с этой Propagation.NESTED! Это ж, блядь, не просто какая-то опция, это целый цирк с точками сохранения, ебать мои старые костыли!

Представь себе картину: есть у тебя большая, жирная транзакция — как паровоз по рельсам едет. И тут тебе надо внутри неё сделать какой-то рискованный кусок работы, который может накрыться медным тазом. Так вот, NESTED — это как будто ты ставишь на этих рельсах запасной стрелочный перевод (savepoint). Если твой рискованный вагончик сходит с рельс, он откатывается НАЗАД К ЭТОЙ СТРЕЛКЕ, а основной паровоз-то едет дальше, как ни в чём не бывало! Не то что REQUIRES_NEW, который вообще новый паровоз заказывает, со всеми вытекающими.

Как оно, сука, работает, если по-простому:

  1. Снаружи транзакции нет? Ну, бля, тогда ведёт себя как обычный REQUIRED — создаёт новую. Всё логично.
  2. А если уже есть большая, массивная транзакция? Вот тут магия! Внутри неё создаётся вложенная, подчиненная хуйня (nested transaction) на основе savepoint'а.
  3. Если в этой вложенной хуйне случается пиздец (исключение), то откатывается ТОЛЬКО она сама, до своего savepoint'а. Внешняя транзакция даже не чихнет, может дальше работать.
  4. А вот фиксация... О, это отдельный прикол. Вложенная транзакция как бы "фиксируется", но по-настоящему-то данные в БД попадут только когда ВНЕШНЯЯ транзакция успешно завершится. До того момента всё висит в воздухе.

Глянь на пример, чтобы не быть мудаком:

@Service
public class OrderService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void processOrder(Order order) {
        // Тут основная логика заказа, всё серьёзно
        updateInventory(order); // А вот тут — вложенная авантюра!
        // Если здесь, после updateInventory, вылетит исключение — откатится ВСЁ, включая склад. Пиздец полный.
    }

    @Transactional(propagation = Propagation.NESTED) // ВОТ ОН, ГЕРОЙ!
    public void updateInventory(Order order) {
        // Логика обновления склада. Вдруг товара нет?
        // Если тут случится страшное — откатится ТОЛЬКО этот метод.
        // А processOrder выловит исключение и, возможно, предложит клиенту другой товар. Умно, блядь!
    }
}

Но не обольщайся, пидрила! Тут подводных ебеней — овердохуища:

  • Зависит от драйвера БД, как наркоман от дозы: Работает ТОЛЬКО если твой JDBC-драйвер (для PostgreSQL, MySQL, Oracle) поддерживает savepoints. А то будешь искать, а его нет, как хуй в пальто.
  • JTA — забудь как страшный сон: С распределёнными транзакциями (JTA) эта фигня не работает. Там свои правила, блядь.
  • В JPA — тёмный лес: Поддержка может быть кривая, поведение разное. Многие мудрецы говорят, что для JPA лучше или REQUIRES_NEW делать, или компенсирующие действия писать. Чтоб не было мучительно больно.
  • А смысл-то когда? А вот когда у тебя есть сложная операция, и часть её можно безопасно откатить, не трогая основную. Как черновик в большом документе. Испортил абзац — удалил его, а не всю главу. Удобно, ёпта!