Доступ к данным
Доступ к базам данных из Java — JDBC, statements и параметризованные запросы.
6 вопросов
MiddleПроизводительностьОчень частоИз-за чего в Hibernate возникает проблема N+1 select и как её убрать?
Из-за чего в Hibernate возникает проблема N+1 select и как её убрать?
Один запрос грузит N родителей; обращение к ленивой связи каждого из них добавляет по одному SELECT на родителя — страница из 100 заказов выливается в 101 запрос. Убирается загрузкой связи тем же запросом — JOIN FETCH в JPQL или @EntityGraph на методе репозитория — либо пакетированием (@BatchSize), которое схлопывает N селектов в несколько запросов с IN (...). EAGER — не решение: он выпускает те же N селектов раньше.
Типичные ошибки
- ✗Хвататься за
EAGERкак за решение — оно лишь переносит те же N селектов раньше - ✗Винить отсутствующий индекс, когда проблема в числе запросов, а не в их стоимости
- ✗Вовсе не замечать проблему, потому что она проявляется только на боевом N
Уточняющие вопросы
- →Почему
@BatchSizeуменьшает число запросов, не соединяя таблицы? - →Что
JOIN FETCHделает с постраничностью черезsetMaxResults?
JuniorТеорияЧастоВ чём разница между Statement и PreparedStatement?
В чём разница между Statement и PreparedStatement?
Statement отправляет полную строку SQL каждый раз, собранную конкатенацией — медленно перепланируется и открыт для SQL-инъекций. PreparedStatement отправляет параметризованный SQL-шаблон один раз; база компилирует его в переиспользуемый план, а значения привязываются отдельно как типизированные параметры. Эта привязка делает инъекцию невозможной (данные не парсятся как SQL) и позволяет выполнять один запрос многократно без повторного разбора, поэтому PreparedStatement — выбор по умолчанию.
Типичные ошибки
- ✗Путать, какой из них параметризован и защищён от инъекций
- ✗Считать ручное экранирование кавычек адекватной заменой привязке параметров
- ✗Полагать, что у
PreparedStatementнет выигрыша от переиспользования плана
Уточняющие вопросы
- →Почему привязанный параметр не может быть истолкован как часть синтаксиса SQL?
- →Когда переиспользование одного
PreparedStatementдля многих выполнений помогает сильнее всего?
JuniorТеорияЧастоЧто такое JPA и что добавляет Hibernate как его реализация?
Что такое JPA и что добавляет Hibernate как его реализация?
JPA — это только спецификация: интерфейсы и аннотации jakarta.persistence (EntityManager, @Entity, JPQL), у которых нет собственного поведения во время выполнения. Hibernate — провайдер, который её реализует: отображает сущности на таблицы, генерирует SQL, ведёт persistence context с проверкой изменений и сбрасывает их при коммите. Код, написанный на интерфейсах JPA, переносим между провайдерами; расширения самого Hibernate — нет.
Типичные ошибки
- ✗Называть JPA библиотекой, выполняющей запросы, а не спецификацией
- ✗Считать, что любая возможность Hibernate — это переносимый JPA
- ✗Думать, что сеттер попадает в базу сразу, без сброса на коммите
Уточняющие вопросы
- →Что даёт persistence context такого, чего нет в чистом JDBC?
- →Когда Hibernate на самом деле отправляет
UPDATEдля изменённой сущности?
MiddleДебаггингЧастоРазберите LazyInitializationException, который летит при отрисовке сущности в контроллере
Разберите LazyInitializationException, который летит при отрисовке сущности в контроллере
items — ленивый прокси, который может инициализироваться, только пока открыт породивший его persistence context. @Transactional заканчивается на выходе из findOrder, сессия закрывается, и сериализатор трогает прокси уже вне неё — отсюда no Session. Тест проходит лишь потому, что его собственный @Transactional держит сессию открытой. Лечится загрузкой нужного внутри транзакции (JOIN FETCH, @EntityGraph) и возвратом DTO.
Типичные ошибки
- ✗Винить сам маппинг
LAZYвместо закрытого persistence context - ✗Включить
open-in-viewи считать это исправлением — оно прячет N+1 селектов за рендерингом - ✗Возвращать из контроллера отсоединённую сущность вместо DTO
Уточняющие вопросы
- →Почему
Hibernate.initialize(order.getItems())вне транзакции тоже упадёт? - →Почему
open-in-view— плохое умолчание для REST-сервиса?
MiddleТеорияЧастоКак @OneToMany, @ManyToOne и @ManyToMany ложатся на таблицы базы данных?
Как @OneToMany, @ManyToOne и @ManyToMany ложатся на таблицы базы данных?
@ManyToOne — владеющая сторона: она ложится на колонку-внешний-ключ в таблице самого потомка. @OneToMany обычно обратная к ней и обязана указывать mappedBy — тот же внешний ключ, но прочитанный со стороны родителя; без mappedBy Hibernate молча добавит связующую таблицу. @ManyToMany всегда требует связующую таблицу из двух внешних ключей. При сбросе пишется только владеющая сторона, поэтому в Java нужно выставлять обе.
Типичные ошибки
- ✗Забыть
mappedByна@OneToManyи получить лишнюю связующую таблицу - ✗Выставить в Java только один конец двусторонней связи и ждать, что внешний ключ запишется
- ✗Считать, что
@ManyToOneнужна собственная связующая таблица
Уточняющие вопросы
- →Почему выставление только обратной стороны оставляет колонку внешнего ключа нетронутой?
- →Когда для связи один-ко-многим связующая таблица лучше внешнего ключа?
SeniorДизайнРедкоКоманде нужно разделить колонку customer.full_name у сущности, отображённой Hibernate, на first_name и last_name. Сервис работает на восьми инстансах за балансировщиком и выкатывается плавающим рестартом примерно за двадцать минут, поэтому старые и новые инстансы всё это время обслуживают трафик бок о бок. В таблице сорок миллионов строк, база — единственный primary, Flyway прогоняет миграцию при старте приложения, а Hibernate настроен с ddl-auto: validate — то есть инстанс откажется подниматься, если схема не совпадает с его маппингом. Технического окна нет: запись не останавливается. Разберите последовательность изменений схемы и релизов кода, что каждый шаг вправе предполагать о ещё работающих версиях, как сорок миллионов существующих строк получат значения новых колонок и как откатываться, если новый релиз окажется сломан уже после начала backfill.
Команде нужно разделить колонку customer.full_name у сущности, отображённой Hibernate, на first_name и last_name. Сервис работает на восьми инстансах за балансировщиком и выкатывается плавающим рестартом примерно за двадцать минут, поэтому старые и новые инстансы всё это время обслуживают трафик бок о бок. В таблице сорок миллионов строк, база — единственный primary, Flyway прогоняет миграцию при старте приложения, а Hibernate настроен с ddl-auto: validate — то есть инстанс откажется подниматься, если схема не совпадает с его маппингом. Технического окна нет: запись не останавливается. Разберите последовательность изменений схемы и релизов кода, что каждый шаг вправе предполагать о ещё работающих версиях, как сорок миллионов существующих строк получат значения новых колонок и как откатываться, если новый релиз окажется сломан уже после начала backfill.
Никогда не менять схему и код одним шагом. Сначала миграция, которая только ДОБАВЛЯЕТ nullable-колонки first_name/last_name: аддитивный DDL всё ещё проходит валидацию старой версии. Затем релиз кода, который пишет оба представления, но читает full_name. Backfill идёт ограниченными пачками вне выката, чтобы ни один statement не держал долгую блокировку. Только потом переключается чтение, а full_name удаляется заметно позже. Каждый шаг обратим: схема всегда устраивает обе работающие версии.
Типичные ошибки
- ✗Связывать DDL и изменение кода в один релиз, из-за чего старые инстансы видят схему, которую не могут провалидировать
- ✗Заполнять сорок миллионов строк одним
UPDATEвнутри стартовой миграции — таблица блокируется, выкат встаёт - ✗Удалять старую колонку тем же релизом, что перестаёт её читать, не оставляя безопасного отката
Уточняющие вопросы
- →Как убедиться, что backfill сошёлся, до переключения чтения на новые колонки?
- →Что меняется, если ту же миграцию надо прогнать и на реплике с отставанием?