Можно ли сделать entity класс final и почему
Что проверяют
Знаете ли вы, как ORM работает с сущностью изнутри, а не только синтаксис final. Правильный ответ «по спецификации нельзя», и дальше интервьюер ждёт объяснения через прокси и ленивую загрузку. Частый следующий вопрос: как с этим живут в Kotlin, где классы финальные по умолчанию.
Как отвечать
Спецификация JPA прямо требует: класс сущности не final, её методы и сохраняемые поля тоже не final, плюс конструктор без аргументов с видимостью public или protected. Причины практические:
- прокси для ленивой загрузки: для
@ManyToOne(fetch = LAZY)илиgetReference()Hibernate создаёт подкласс сущности через ByteBuddy. От финального класса наследоваться нельзя, и связь загружается сразу, а это лишние запросы; - финальные методы: прокси перехватывает вызов, чтобы подгрузить данные. Финальный геттер перехватить нельзя, он читает поле непроинициализированного прокси и возвращает
null; - финальные поля: провайдер заполняет объект после создания через пустой конструктор, а поле
finalпо спецификации так заполнять нельзя.
Hibernate на практике терпимее спецификации: с финальным классом приложение запустится, но ленивые связи перестанут быть ленивыми. Обходной путь есть, это bytecode enhancement, но он требует настройки сборки.
Для Kotlin стандартная связка: плагин kotlin("plugin.jpa") генерирует конструктор без аргументов, а allOpen с аннотациями @Entity, @MappedSuperclass и @Embeddable снимает финальность. В Spring-проектах рядом обычно стоит ещё kotlin("plugin.spring").
Если нужна неизменяемость, её получают не через final у сущности. record подходит для DTO-проекций и, в свежих версиях Hibernate, для @Embeddable, но сущностью быть не может.
Где спотыкаются
Отвечают «можно, это даже полезно для потокобезопасности». Финальность класса запрещает наследование, но не делает объект неизменяемым.
Говорят, что всё работает, потому что проверяли на связях с EAGER. Проблема проявляется ровно там, где загрузка ленивая.
В Kotlin пишут сущность как data class с val. Помимо финальности, сгенерированные equals() и hashCode() по всем полям ломают поведение в Set и при сравнении с прокси.