Кейс: @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 — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки