Как сделать метод безопасным для записи при использовании @Transactional(readOnly = true) в Java
Что проверяют
Понимаете ли вы, что readOnly = true это подсказка менеджеру транзакций и драйверу, а не запрет на уровне Java, и что поведение при записи зависит от стека. Следом почти всегда спрашивают, что будет, если метод записи вызвать изнутри read-only транзакции.
Как отвечать
С JpaTransactionManager и Hibernate флаг переводит сессию в FlushMode.MANUAL и режим read-only для загруженных сущностей, а JDBC-соединению выставляет setReadOnly(true). Отсюда два исхода записи: изменения сущностей молча не уходят в базу, потому что flush не случился, либо база отвечает ошибкой, как PostgreSQL с cannot execute UPDATE in a read-only transaction. Особого исключения Spring при этом не бросает.
Как сделать метод записи безопасным:
- аннотация на методе: если на классе стоит
@Transactional(readOnly = true), пишущему методу нужен свой@Transactional. Аннотация метода перекрывает аннотацию класса, так устроенSimpleJpaRepositoryв Spring Data; - вызов из read-only транзакции: с распространением
REQUIREDвнутренний метод присоединяется к внешней транзакции и наследует её режим, свойreadOnly = falseигнорируется. Нужно либо вынести запись из read-only сценария, либо взятьPropagation.REQUIRES_NEW, понимая, что это отдельное соединение и отдельный коммит; - явная проверка:
setValidateExistingTransaction(true)у менеджера транзакций заставит Spring броситьIllegalTransactionStateException, когда пишущий метод пытается войти в read-only транзакцию; - реплики: если read-only транзакции маршрутизируются на реплику через
AbstractRoutingDataSource, запись в них упадёт.
Где спотыкаются
Вызывают пишущий метод через this из того же класса. Вызов идёт мимо прокси, аннотация не срабатывает, метод живёт в транзакции вызывающего. Нужен отдельный бин или TransactionTemplate.
Меняют сущность внутри метода чтения, например проставляют дату последнего просмотра, и удивляются, что изменение не сохранилось, а ошибки нет. Это и есть FlushMode.MANUAL в действии.
Ставят REQUIRES_NEW не глядя: внутренняя транзакция берёт второе соединение, под нагрузкой пул кончается, а при откате внешней её запись остаётся.