Вопрос или проблема
Я использую Hibernate и JPA. У меня есть сущность, которая выглядит следующим образом:
@Data
@Entity
@Table(name = "MY_ENTITY")
public class MyEntity {
@Id
@Column(name = "ID")
public Long id;
@Version
@Column(name = "VERSION")
public Long version = 1L;
@Embedded
@AttributeOverride(name = "timestamp", column = @Column(name = "UPDATED_TIME"))
public CustomTimestamp updatedTime;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "ENTITY_CONNECTION", joinColumns = @JoinColumn(name = "ENTITY_ID"))
@OrderColumn(name = "CONNECTION_ORDER")
@Fetch(FetchMode.SUBSELECT)
@JsonDeserialize(using = ConnectionDeserializer.class)
public List<Connection> connections = new ArrayList<>();
@PreUpdate
public void preUpdate() {
this.updatedTime = new CustomTimestamp();
}
}
@Data
@Embeddable
public class Connection {
@Column(name = "CONNECTED_ENTITY_ID")
public String connectedEntityId;
@Column(name = "CONNECTED_ENTITY_TYPE")
public String connectedEntityType;
@Column(name = "NOTE")
public String note;
}
Входные данные для сущности включают все поля с флагом удаления. Упомянутый десериализатор берет существующий список и выполняет сравнение с обновлением, которое было отправлено. Обновляет существующие сущности в списке, добавляет новые сущности и удаляет сущности, отмеченные флагом “удалить”.
В API я предоставляю путь для “patch” сущности, что означает, что пользователь передает поля, которые он хочет обновить. И я выполняю следующую логику:
MyEntity originalEntity = repository.findById(id);
ObjectReader entityForUpdate = objectMapper.readerForUpdating(originalEntity);
MyEntity updatedEntity = objectMapper.update(entityForUpdate, updateInput);
repository.saveAndFlush(updatedEntity);
Пример входных данных для “patch” будет выглядеть примерно так:
patch /entity/{id}
body:
{
"connections": [
{
"connectedEntityId": "test",
"connectedEntityType": "testType"
}
]
}
При отправке его дважды, я ожидал, что во второй раз ничего не будет обнаружено как “грязное”, и, следовательно, версия не будет увеличена, однако это происходит.
Смотря на логи Hibernate, похоже, что коллекция “connection” грязная.
Как Hibernate определяет, является ли коллекция грязной?
Пробовал использовать Set вместо ArrayList – коллекция все равно была грязной.
Пробовал использовать отображение OneToMany – проблема была обратной – родительская сущность никогда не обновляется. Насколько я понимаю, это не лучший вариант, так как “Connection” не является независимой сущностью.
Пробовал добавить сгенерированный ID для сущности Connection – коллекция все равно была грязной.
Проверил, что все сущности реализуют equals()
так, как ожидалось
Ответ или решение
Почему Hibernate помечает сущности как "грязные", даже когда изменений нет?
Ваша проблема актуальна и касается одного из основных аспектов работы с Hibernate и JPA — проверки чистоты объектов (dirty checking). Давайте подробно разберем, почему Hibernate может помечать вашу сущность как "грязную", даже несмотря на то, что вы не вносите видимых изменений.
1. Особенности Dirty Checking в Hibernate
Hibernate использует механизм отслеживания состояния объектов для управления изменениями. При загрузке сущности Hibernate хранит её исходное состояние и сравнивает его с текущим при каждом обновлении сессии. Если Hibernate обнаруживает различия, он помечает сущность как "грязную" и готовит её к обновлению в базе данных.
2. Ваша проблема
Вы описали ситуацию, когда коллекция connections
вашего объекта MyEntity
помечается как "грязная", даже если вы не вносите изменений. Это может быть связано с несколькими факторами:
- Сравнение подлежащих объектов: При выполнении обновления коллекции объект
Connection
вы, возможно, не меняете ссылки на объекты вList<Connection>
или не подмениваете сами объекты, но Hibernate все равно считает, что состояние коллекции изменилось. Это связано с тем, как осуществляется сравнение объектов внутри коллекции. - Метод equals() и hashCode(): Вы упомянули, что метод
equals()
реализован. Однако стоит убедиться, что он также корректно реализован во всех связанных классах. Любая несогласованность в реализации может привести к тому, что Hibernate не сможет правильно сравнить объекты и определит, что они изменились. - Жизненный цикл обновления: Метод
preUpdate()
всегда создает новый объектCustomTimestamp
, когда выполняется операция обновления. Это автоматически помечает ваш объект как измененный (dirty), даже если другие поля не были изменены. Каждый раз, когда вы вызываетеpreUpdate
, полеupdatedTime
получает новый объект, что вызывает необходимость обновления сущности.
3. Как избежать проблемы
Для того чтобы избежать этой проблемы, рассмотрите следующие рекомендации:
-
Переосмотрите метод
preUpdate
: Если вы хотите минимизировать изменения в вашем объекте, попробуйте убрать автоматическое обновление поляupdatedTime
изpreUpdate
. Вместо этого обновляйте это поле только в том случае, если есть действительно изменения в экземпляреMyEntity
. -
Используйте
Set
для коллекций: Вы уже пробовали использоватьSet
, однако важно обращать внимание на реализациюequals
иhashCode
в ваших классах. ДляSet
ключевыми аспектами являются уникальность и отсутствие дубликатов. Обновление объектов, когда они уже есть вSet
, не должно помечать коллекцию как "грязную". -
Смотрите на журнал изменений: Используйте логи Hibernate для детального анализа того, какие поля помечаются как измененные. Это даст вам понимание, что именно происходит при каждой операции сохранения.
-
Проверка изменений в коллекции: При патчинге коллекции из
updateInput
, убедитесь, что вы не создаете новые экземпляры объектов без необходимости. Лучше обновляйте существующие объекты, которые уже находятся в вашей коллекции, чтобы избежать ненужных изменений.
Заключение
Система "грязных" флагов в Hibernate позволяет эффективно управлять изменениями, но при этом требует четкого понимания того, как именно она работает. Обратите внимание на реализацию методов equals
и hashCode
, жизненный цикл объектов и управление изменениями состояния. Это поможет вам избежать ненужных пометок в качестве "грязных" и оптимизировать работу с Hibernate и JPA.