Автор оригинала: Vlad Mihalcea.
Как ранее объяснялось , использование бизнес-ключа сущности JPA для равно и хэш-кода всегда является лучшим выбором. Однако не все сущности имеют уникальный бизнес-ключ, поэтому нам нужно использовать другой столбец базы данных, который также уникален, в качестве первичного ключа.
Но использование идентификатора сущности для равенства очень сложно, и этот пост покажет вам, как вы можете использовать его без проблем.
Когда дело доходит до реализации равно и хэш-кода , есть одно и только одно правило, которое вы должны иметь в виду:
Equals и хэш-код должны вести себя последовательно во всех переходах между состояниями сущностей .
Для проверки эффективности реализации равно и хэш-кода можно использовать следующий тест:
protected void assertEqualityConsistency(
Class clazz,
T entity) {
Set tuples = new HashSet<>();
assertFalse(tuples.contains(entity));
tuples.add(entity);
assertTrue(tuples.contains(entity));
doInJPA(entityManager -> {
entityManager.persist(entity);
entityManager.flush();
assertTrue(
"The entity is not found in the Set after it's persisted.",
tuples.contains(entity)
);
});
assertTrue(tuples.contains(entity));
doInJPA(entityManager -> {
T entityProxy = entityManager.getReference(
clazz,
entity.getId()
);
assertTrue(
"The entity proxy is not equal with the entity.",
entityProxy.equals(entity)
);
});
doInJPA(entityManager -> {
T entityProxy = entityManager.getReference(
clazz,
entity.getId()
);
assertTrue(
"The entity is not equal with the entity proxy.",
entity.equals(entityProxy));
});
doInJPA(entityManager -> {
T _entity = entityManager.merge(entity);
assertTrue(
"The entity is not found in the Set after it's merged.",
tuples.contains(_entity)
);
});
doInJPA(entityManager -> {
entityManager.unwrap(Session.class).update(entity);
assertTrue(
"The entity is not found in the Set after it's reattached.",
tuples.contains(entity)
);
});
doInJPA(entityManager -> {
T _entity = entityManager.find(clazz, entity.getId());
assertTrue(
"The entity is not found in the Set after it's loaded in a different Persistence Context.",
tuples.contains(_entity)
);
});
doInJPA(entityManager -> {
T _entity = entityManager.getReference(clazz, entity.getId());
assertTrue(
"The entity is not found in the Set after it's loaded as a proxy in a different Persistence Context.",
tuples.contains(_entity)
);
});
T deletedEntity = doInJPA(entityManager -> {
T _entity = entityManager.getReference(
clazz,
entity.getId()
);
entityManager.remove(_entity);
return _entity;
});
assertTrue(
"The entity is not found in the Set even after it's deleted.",
tuples.contains(deletedEntity)
);
}
Первым вариантом использования для тестирования является сопоставление натуральный идентификатор . Рассматривая следующую сущность:
@Entity public class Book implements Identifiable{ @Id @GeneratedValue private Long id; private String title; @NaturalId private String isbn; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book)) return false; Book book = (Book) o; return Objects.equals(getIsbn(), book.getIsbn()); } @Override public int hashCode() { return Objects.hash(getIsbn()); } //Getters and setters omitted for brevity }
Свойство isbn также является @естественным , поэтому оно должно быть уникальным и не обнуляться. Оба равно и Хэш-код используют свойство isbn в своих реализациях.
Для получения более подробной информации об аннотации @Natural ознакомьтесь с этой статьей .
При выполнении следующего тестового случая:
Book book = new Book();
book.setTitle("High-PerformanceJava Persistence");
book.setIsbn("123-456-7890");
assertEqualityConstraints(Book.class, book);
Все работает нормально, как и ожидалось.
Что делать, если у нашей сущности нет столбца, который можно использовать как @Naturalist ? Первое побуждение-не определять свои собственные реализации равных и хэш-кода , как в следующем примере:
@Entity(name = "Book") public class Book implements Identifiable{ @Id @GeneratedValue private Long id; private String title; //Getters and setters omitted for brevity }
Однако при тестировании этой реализации:
Book book = new Book();
book.setTitle("High-PerformanceJava Persistence");
assertEqualityConstraints(Book.class, book);
Режим гибернации вызывает следующее исключение:
java.lang.AssertionError: The entity is not found after it's merged
Исходная сущность не равна сущности, возвращаемой методом слияния, поскольку два разных объекта не имеют одной и той же ссылки.
Поэтому, если значение по умолчанию равно и хэш-код также никуда не годится, тогда давайте использовать идентификатор сущности для нашей пользовательской реализации. Давайте просто используем нашу среду разработки для генерации равных и хэш-кода и посмотрим, как это работает:
@Entity public class Book implements Identifiable{ @Id @GeneratedValue private Long id; private String title; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book)) return false; Book book = (Book) o; return Objects.equals(getId(), book.getId()); } @Override public int hashCode() { return Objects.hash(getId()); } //Getters and setters omitted for brevity }
При запуске предыдущего тестового набора Hibernate выдает следующее исключение:
java.lang.AssertionError: The entity is not found after it's persisted
Когда сущность была впервые сохранена в Наборе, идентификатор был равен нулю. После сохранения сущности идентификатор был присвоен автоматически сгенерированному значению, поэтому хэш-код отличается. По этой причине сущность не может быть найдена в Наборе после ее сохранения.
Чтобы решить предыдущую проблему, есть только одно решение: хэш-код всегда должен возвращать одно и то же значение:
@Entity public class Book implements Identifiable{ @Id @GeneratedValue private Long id; private String title; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book)) return false; Book other = (Book) o; return id != null && id.equals(other.getId()); } @Override public int hashCode() { return getClass().hashCode(); } //Getters and setters omitted for brevity }
Кроме того, когда идентификатор сущности равен null , мы можем гарантировать равенство только для одних и тех же ссылок на объекты. В противном случае ни один временный объект не будет равен любому другому временному или постоянному объекту. Вот почему проверка на равенство идентификаторов выполняется только в том случае, если текущий Объект идентификатор не равен нулю.
При такой реализации тест равен и хэш-код выполняется нормально для всех переходов состояния сущности. Причина, по которой это работает, заключается в том, что значение хэш-кода не меняется, следовательно, мы можем полагаться на java.lang.Объект равенство ссылок до тех пор, пока идентификатор равен нулю .
Идентификатор сущности может использоваться для равно и хэш-кода , но только в том случае, если Хэш-код все время возвращает одно и то же значение. Это может показаться ужасным поступком, поскольку это противоречит цели использования нескольких сегментов в HashSet или HashMap .
Однако по соображениям производительности всегда следует ограничивать количество сущностей, хранящихся в коллекции. Вы никогда не должны извлекать тысячи сущностей в @OneToMany Set , потому что снижение производительности на стороне базы данных на несколько порядков выше, чем при использовании одного хэшированного ведра.
Все тесты доступны на GitHub .