Представим что мы реализуем собственный репозиторий для какой-то сложной сущности (ну не хотим по каким-то причинам использовать JPA). Под сложной я имею ввиду сущность у которой есть связанные сущности. Пусть это будет сущность Entity и связанные с ний NotificationRecipient - какие-то получатели уведомлений, которых для каждой Entity может быть несколько. Ну и наша сущность будет выглядеть примерно так:

public class Entity {
    private Long id;
    private String name;
    private List<NotificationRecipient> recipients;

    // getters and setters
}

Теперь мы хотим получить список сущностей со всеми получателями уведомлений. Для этого нам нужно выполнить несколько запросов к БД:

  • получить список всех сущностей
  • для каждой сущности получить список всех получателей уведомлений

Но мы хотим получить этот список одним обращением к репозиторию вызвав какой-нибудь findAllEntitiesWithRecipients(). Не долго думаю собственно идем и реализуем его:

public final class EntityRepository {
    private final JdbcClient jdbcClient;

    public EntityRepository(JdbcClient jdbcClient) {
        this.jdbcClient = jdbcClient;
    }

    public List<Entity> findAllEntitiesWithRecipients() {
        return jdbcClient.query("SELECT * FROM entity", Entity.class)
                .map(this::addNotificationRecipients)
                .collect(Collectors.toList());
    }
    
    private Entity addNotificationRecipients(Entity entity) {
        List<NotificationRecipient> recipients = jdbcClient
                .query("SELECT * FROM notification_recipient WHERE entity_id = ?", entity.getId(), NotificationRecipient.class);
        entity.setRecipients(recipients);
        return entity;
    }
}

И вот тут может вылезти неожиданный побочный эффект. Так как мы используем jdbcClient.query() возвращающий Stream и при обработке этого же Stream мы делаем дополнительные запросы для получения получателей уведомлений. В случае если список наших сущностей довольно большой, в какой-то момент у нас просто закончатся доступные соединения с базой данных. Потому что наш Stream<Entity> не закрывается и соотвественно транзакция с БД тоже и наше соединение не возвращается в пул. При этом для каждой сущности с не пустым списком получателей уведомлений происходит тоже самое, активное соединение не возвращается в пул до тех пор пока не будет вызван терминальный оператор на основном Stream.

Чтобы избежать этой ситуации достаточно просто добавить к нашему публичному методу аннотацию @Transactional, так же можно пометить его как readOnly для дополнительных оптимизаций на стороне Spring. Это позволит нам выполнять все запросы к БД в рамках одной транзакции и одного активного соединения из пула, которое после выполнения всех операций будет возвращено в пул.