Несколько лет я развиваю свой сервис. За это время он оброс множеством интеграций с другими микросервисами. Для этих интеграций приходится делиться данными. 🔄
Проблема в том, что данные не собраны в одной таблице. Они разбросаны по разным, имеют связи:
· «многие ко многим»
· «один ко многим»
· «один к одному»
В разных кейсах их нужно комбинировать и фильтровать. И каждый раз — проблема с оптимизацией запросов. Одни работают быстро, другие — медленно. Где-то нужны дополнительные поля из смежных таблиц, где-то нет. 🐢⚡
В итоге у меня появилась куча контроллеров, которые достают одни и те же данные, но:
· с разным списком полей
· и по-разному обогащённые из смежных таблиц
В какой-то момент я понял: этот зоопарк нужно приводить к одному эффективному виду. 🦁🐧🐪
Но возникла новая проблема:
· одним запросам нужны обогащения из одних таблиц,
· другим — из других,
· третьим — минимальные обогащения (и они просто летают 🚀).
Также нужно использовать разные фильтры. Часто фильтровать приходится по записям из смежных таблиц — до нескольких уровней вложенности.
SQL-запросы при этом выглядят монструозно, подвержены проблемам с декартовым произведением и в целом работают медленно. 📉
⚙️ Первый шаг к оптимизации
Чтобы оптимизировать запросы выборки по фильтрам, я отошёл от встроенных репозиториев CRUD и Spring Data JPA и стал использовать Spring Specification.
Она принимает фильтры и отбирает из базы только то, что нужно.
✅ Чем проще фильтр использует пользователь — тем проще запрос и быстрее работа!
🔥 Но тут новая проблема
Ленивая инициализация полей в DTO, которое я отдаю в ответ.
И снова нужно искать баланс между:
· объёмом передаваемых данных
· и сложностью запроса к БД
Вернуть всё сразу — классно, но получаем либо N+1, либо жуткое декартово произведение.
Не запрашивать всё — маппер падает с ошибкой lazy initialization. 💥
✨ Сегодня меня осенило
Изучая логи запросов к БД, я понял:
Наиболее правильный подход — разделить логику на две части:
🧩 Часть 1. Поиск ID
Ходим в БД, джойним дополнительные таблицы (если требуется по фильтрам).
Фильтруем по ним.
Возвращаем только идентификаторы нужных объектов.
🧩 Часть 2. Обогащение по ID
По этим ID вытаскиваем объекты из базы и обогащаем только той информацией, которую запросил пользователь.
Пользователь передаёт список нужных scopes.
По ним мы вручную ходим в нужные таблицы и одним запросом на таблицу читаем информацию для всего списка ID исходных объектов.
В памяти аттачим извлечённые данные к исходным объектам. 🧠
🎯 Итог
Мне кажется, это лучший способ:
· избежать проблем N+1
· и огромных декартовых произведений при выборке списка объектов
Буду пробовать. 💪
Кто уже так делал — дайте знать, в какие грабли наступили. 🦶🪤
