Рубрики
Без рубрики

Как реализовать равенство и хэш-код с использованием идентификатора сущности JPA (первичный ключ)

Автор оригинала: 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 .