Нужна ли транзакция на чтение в конкретном случае
Что проверяют
Понимаете ли вы, что «чтение без транзакции» это миф: в режиме autocommit каждый оператор выполняется в собственной транзакции. Реальный предмет разговора это согласованность между несколькими чтениями и цена длинной транзакции.
Как отвечать
Задайте два встречных вопроса: сколько запросов входит в это чтение и что произойдёт, если между ними данные изменятся. Одиночный SELECT уже атомарен, оборачивать его в явную транзакцию нечего. Несколько связанных чтений (итог и детализация, баланс и список операций) без общей транзакции могут увидеть разные состояния базы, и отчёт не сойдётся сам с собой.
Дальше уровень изоляции. В PostgreSQL по умолчанию Read Committed: каждый оператор берёт свежий снимок, поэтому повторный SELECT внутри одной транзакции может вернуть другое. Для согласованного отчёта нужен Repeatable Read, который фиксирует снимок на всю транзакцию. Грязных чтений в PostgreSQL не бывает вообще, Read Uncommitted там ведёт себя как Read Committed. В MySQL с InnoDB по умолчанию как раз Repeatable Read, это частый источник расхождений при переезде.
В Spring всё это выражается через @Transactional(readOnly = true): чтения группируются в одну транзакцию, Hibernate отключает грязную проверку сущностей, а маршрутизация может отправить такую транзакцию на реплику. На методе из одного запроса это лишний оверхед, на методе, который подтягивает ленивые связи, наоборот спасение от LazyInitializationException.
Где спотыкаются
Говорят, что транзакция гарантирует актуальные данные. Всё наоборот: она даёт согласованный снимок, и чем выше изоляция, тем дольше вы смотрите в прошлое.
Второе: вешают @Transactional на приватный метод или вызывают соседний метод через this. Прокси не срабатывает, транзакции нет, а код выглядит правильным.
Третье: растягивают читающую транзакцию на внешний вызов или на выгрузку в файл. Соединение занято, старые версии строк не вычищаются, база пухнет, и виноватым назначают «медленный SQL».