Кейс: @Transactional****(readOnly=true) не остановил запись

readOnly=true в @Transactional — подсказка Hibernate и, при включённой настройке, JDBC-драйверу, а не гарантия отказа записи на уровне БД.

Метод getOrCreateSettings(userId) был помечен @Transactional(readOnly=true). Внутри — find, если записи нет — new Settings(...), repository.save(...). Тесты проходили: запись создавалась и в проде, потому что Hibernate сам по себе не блокирует INSERT в readOnly-транзакции — он только меняет flush mode и, если настроено, дёргает Connection.setReadOnly(true) у драйвера.

С Postgres через обычный JDBC-драйвер setReadOnly(true) в большинстве конфигураций не запрещает запись на уровне протокола — сервер её просто выполнит. Ошибка не гарантирована и поведение отличается между драйверами и версиями пула соединений.

Что стоило сделать: не полагаться на readOnly как на защиту от записи, а явно разделять read- и write-сервисы, либо ловить нарушение на code review — readOnly с save() внутри метода уже сигнал.

Следом обычно спрашивают, что readOnly даёт кроме «подсказки»: пропуск dirty checking у Hibernate снижает нагрузку — не нужно сравнивать снапшоты сущностей при коммите, это реальная оптимизация, а не только семантика.

readOnly — оптимизация, не constraint. Запрет записи обеспечивают на уровне роли БД или отдельного read-only соединения, а не аннотацией на методе.

Тренажёр: 600 вопросов, мок с таймером, план повторов

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки