💡 Момент озарения про JOIN’ы, декартовы произведения, проблему N+1 и трюк с ID

💡 Момент озарения про JOIN’ы, декартовы произведения, проблему N+1 и трюк с ID

Несколько лет я развиваю свой сервис. За это время он оброс множеством интеграций с другими микросервисами. Для этих интеграций приходится делиться данными. 🔄

Проблема в том, что данные не собраны в одной таблице. Они разбросаны по разным, имеют связи:

· «многие ко многим»
· «один ко многим»
· «один к одному»

В разных кейсах их нужно комбинировать и фильтровать. И каждый раз — проблема с оптимизацией запросов. Одни работают быстро, другие — медленно. Где-то нужны дополнительные поля из смежных таблиц, где-то нет. 🐢⚡

В итоге у меня появилась куча контроллеров, которые достают одни и те же данные, но:

· с разным списком полей
· и по-разному обогащённые из смежных таблиц

В какой-то момент я понял: этот зоопарк нужно приводить к одному эффективному виду. 🦁🐧🐪

Но возникла новая проблема:

· одним запросам нужны обогащения из одних таблиц,
· другим — из других,
· третьим — минимальные обогащения (и они просто летают 🚀).

Также нужно использовать разные фильтры. Часто фильтровать приходится по записям из смежных таблиц — до нескольких уровней вложенности.
SQL-запросы при этом выглядят монструозно, подвержены проблемам с декартовым произведением и в целом работают медленно. 📉

⚙️ Первый шаг к оптимизации

Чтобы оптимизировать запросы выборки по фильтрам, я отошёл от встроенных репозиториев CRUD и Spring Data JPA и стал использовать Spring Specification.
Она принимает фильтры и отбирает из базы только то, что нужно.
✅ Чем проще фильтр использует пользователь — тем проще запрос и быстрее работа!

🔥 Но тут новая проблема

Ленивая инициализация полей в DTO, которое я отдаю в ответ.
И снова нужно искать баланс между:

· объёмом передаваемых данных
· и сложностью запроса к БД

Вернуть всё сразу — классно, но получаем либо N+1, либо жуткое декартово произведение.
Не запрашивать всё — маппер падает с ошибкой lazy initialization. 💥


✨ Сегодня меня осенило

Изучая логи запросов к БД, я понял:
Наиболее правильный подход — разделить логику на две части:

🧩 Часть 1. Поиск ID

Ходим в БД, джойним дополнительные таблицы (если требуется по фильтрам).
Фильтруем по ним.
Возвращаем только идентификаторы нужных объектов.

🧩 Часть 2. Обогащение по ID

По этим ID вытаскиваем объекты из базы и обогащаем только той информацией, которую запросил пользователь.
Пользователь передаёт список нужных scopes.
По ним мы вручную ходим в нужные таблицы и одним запросом на таблицу читаем информацию для всего списка ID исходных объектов.
В памяти аттачим извлечённые данные к исходным объектам. 🧠


🎯 Итог

Мне кажется, это лучший способ:

· избежать проблем N+1
· и огромных декартовых произведений при выборке списка объектов

Буду пробовать. 💪

Кто уже так делал — дайте знать, в какие грабли наступили. 🦶🪤


(Просмотрено 1 раз, 1 раз за сегодня)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *