Обновить

Комментарии 10

А мне почему то казалось, что если High-Load то лучше в 2026 стартовать проекты на Rust.
Сам я, Rust пока еще, не использовал, но планирую в ближайшее время, какой нибудь из микросервисов на нем написать. Очень уж восторженные отзывы получаю от коллег.

У меня high-load в контексте масштабируемости и надёжности, а не микросекундных задержек - для этого Java/Spring Boot (особенно с реактивным стеком или современным GC) более чем достаточно. Rust пока не пробовал, но держу в поле зрения.

AbstractEntityUuid#equals, AbstractEntityUuid#hashCode

Уж сколько раз эта тема обсасывалась со всех сторон. В том числе и на Хабре. Сущность мутабельна, согласно спецификации должна иметь конструктор по умолчанию, соответственно, её можно создать в неконсистентном состояннии. Её внутренней состояние может измениться в течение жизни.

Определять equals()/hashCode() в 145% случаев не требуется. А где требуется надо быть очень осторожным и завязывать реализацию этих методов только на идентификатор неправильно. А уж ваша реализация equals() вообще неправильная, т.к. не учитывает прокси.

private LocalDateTime createdAt;

Время создания - это точка на временной оси, какой-то конкретный момент, при чём тут локальное время? А если учесть, что это время меняется одним параметром запуска jvm, то это вообще полный звиздец. Для таких полей надо использовать java.time.Instant.

Спасибо за технический разбор. Критика принимается, однако стоит пояснить логику выбора данных решений в контексте текущего этапа проекта:

Использование insnanceof вместо getClass() это справедливое замечание для работы с Hibernate Proxy. В данном случае реализация намеренно оставлена лаконичной, так как на начальном этапе структура связей прозрачна и не использует сложные сценарии ленивой загрузки в коллекциях. При усложнении объектного графа рефакторинг этих методов под специфику проксирования это стандартный запланированный шаг.

Выбор localDateTime обусловлен тем, что на старте разработки важнее наглядность данных в БД и упрощение отладки логики, завязанной на локальное время. Переход на instant для обеспечения инвариантности относительно часовых поясов целесообразен при переходе к распределенной архитектуре. На текущем этапе выбранный тип полностью закрывает потребности системы, не создавая избыточной сложности при конвертации.

Наличие конструкторов по умолчанию и сеттеров это необходимый компромисс при использовании JPA/Hibernate. Обеспечение полной неизменяемости сущностей (Immutability) зачастую вступает в конфликт с требованиями фреймворков и производительностью маппинга.

Проектирование это всегда баланс между академической чистотой и прагматизмом. Стремление возводить частные практики в статус "единственно верных решений" часто ограничивает гибкость разработки и ведет к оверинжинирингу. Инженерная ценность заключается в том, чтобы понимать, когда инструмент необходим, а когда он становится избыточным усложнением. На текущем этапе выбранные решения полностью обоснованы задачами первой итерации проекта.

Выбор localDateTime обусловлен тем, что на старте разработки важнее наглядность данных в БД

Как одно связано с другим? В БД тип колонки что для LocalDateTime, что для Instant будет одинаковым.

упрощение отладки логики, завязанной на локальное время.

У java.time.Instant вполне задокументированный человекочитаемый toString(). Т.ч. что в sout, что в лог, что просто под дебагером понять какая там именно дата можно без проблем.

Наличие конструкторов по умолчанию и сеттеров это необходимый компромисс

Это не компромисс, а вполне себе требование стандарта JPA. Который, кстати, хибер вполне себе допускает нарушать.

Проектирование это всегда баланс между академической чистотой и прагматизмом.

Как раз с точки зрения прогматизма equals() и hashCode() вообще не надо реализовывать, пока не упёрся в проблему, решить которую без реализации этих методов вообще не получается. У вас же буквально первая сущность UserEntity уже имеет натуральный идентификатор (unique = true) и вот как раз с точки зрения бизнес-логики равенство должно бы проверяться по этому значению.

ведет к оверинжинирингу

Это именно то, что мы наблюдаем на описываемом проекте. Берём простую задачу: найти NicknameOldEntity по идентификатору. Казалось бы, чего проще, и проблем быть не должно, но вместо тупейшего select * from nickname_old where id = ? у нас появится монстр с двумя join-ами на ровном месте.

Какая разработка высоконагруженной системы? Вам бы для начала "How to..." прочесть, посмотреть на аналогичные решения, доступные в сети, обдумать используемые там решения, а потом уже, если останется желание, можно и статью на Хабр написать, которая будет реально полезной.

По существу ваших замечаний:
Про Instant и логи: Согласен, toString() у Instant информативен. Однако выбор в пользу LocalDateTime на данном этапе сделан для упрощения визуального контроля данных в БД при ручных запросах в процессе прототипирования, чтобы банально было просто открыть БД и посмотреть когда и что создалось, а не высчитывать. Безусловно, Instant — более строгий стандарт для продакшена, и этот переход заложен в логику развития проекта.
Про реализацию equals/hashCode: Позиция "не реализовывать, пока нет проблемы" вполне жизнеспособна. Мой подход это создание базового контракта сущности сразу. Что касается использования бизнес-ключей (unique полей) в проверках это дискуссионный вопрос архитектурных паттернов, который заслуживает отдельного разбора.
Про join-ы и структуру: Вы абсолютно правы, такая иерархия связей увеличивает количество join-ов. Это осознанный архитектурный компромисс (trade-off). Я приношу производительность простых запросов в жертву строгой типизации и единообразной структуре отношений на уровне кода. На текущем объеме данных и в рамках задач первой итерации этот приоритет кажется мне более оправданным для поддержки проекта.

Использовать @Getter и не использовать @RequiredArgsConstructor.

Код не сами писали ?

Код, разумеется, авторский. Что касается выбора аннотаций: @RequiredArgsConstructor из Lombok чаще всего используется для генерации конструктора под final поля. Например: при внедрении зависимостей в сервисах.

В JPA-сущностях поля не помечаются как final, так как их состояние управляется Hibernate. Кроме того, спецификация JPA требует наличия конструктора без параметров, поэтому здесь используются @NoArgsConstructor и @AllArgsConstructor. В данной ситуации я счел, что @Getter, @Setter и стандартных конструкторов более чем достаточно для текущих задач модели. Всегда стараюсь придерживаться принципа минимальной достаточности в использовании аннотаций, чтобы не перегружать код там, где в этом нет прямой необходимости

Странное решение выносить связи в именованные классы с наследованием. Чем коллекции как поля плохи ?

Да и в целом столько наследования, чтобы избежать дублирования пары строк кода… ну такое

Что касается структуры наследования и выноса связей в отдельные классы:

Безопасность и типизация: Основная цель здесь это не просто экономия строк кода, а создание жесткого каркаса. Когда проект разрастается до десятков таблиц, наличие базовых классов, вроде UserOwnerOneToOne, гарантирует, что разработчик не забудет прописать нужные связи, индексы или правила удаления. Это минимизирует риск "накосячить" при создании новых сущностей, связанных с пользователем.

Использование коллекций внутри UserEntity, например, List<Post>, удобно, но в высоконагруженных системах это часто ведет к проблемам с производительностью: избыточные Select запросы, раздувание объекта пользователя. Подход с выделением связей в отдельные сущности позволяет работать с данными более точечно и изолированно.

Такая иерархия делает структуру БД и кода предсказуемой. Глядя на определение класса, сразу понятно, какую роль он играет в системе и какие базовые поля: ID, время создания в нем гарантированно есть.

Это, конечно, создает определенную многослойность, но для проекта, который планирует масштабироваться, такой фундамент кажется более надежным и защищенным от человеческого фактора в будущем

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации