Автор оригинала: Vlad Mihalcea.
Как ранее объяснялось , использование бизнес-ключа сущности JPA для равно
и хэш-кода
всегда является лучшим выбором. Однако не все сущности имеют уникальный бизнес-ключ, поэтому нам нужно использовать другой столбец базы данных, который также уникален, в качестве первичного ключа.
Но использование идентификатора сущности для равенства очень сложно, и этот пост покажет вам, как вы можете использовать его без проблем.
Когда дело доходит до реализации равно
и хэш-кода
, есть одно и только одно правило, которое вы должны иметь в виду:
Equals и хэш-код должны вести себя последовательно во всех переходах между состояниями сущностей .
Для проверки эффективности реализации равно
и хэш-кода
можно использовать следующий тест:
protected void assertEqualityConsistency( Classclazz, 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 .