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

Лучший способ реализовать equals, хэш-код и строку с помощью JPA и Hibernate

Автор оригинала: Vlad Mihalcea.

На прошлой неделе Марк Струберг, член Apache Software Foundation и участник OpenJPA, сделал следующее заявление:

Люди, ПОЖАЛУЙСТА, _не_ пишите методы toString() в #JPA сущностях! Это неявно вызовет отложенную загрузку всех полей…

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

Поэтому метод toString может использовать любые базовые атрибуты сущности (которые необходимы для идентификации определенной сущности в журналах), если базовые атрибуты извлекаются при загрузке сущности из базы данных.

Тем не менее, Hibernate позволяет лениво загружать атрибуты , но даже в этом случае улучшение байт-кода не обязательно является лучшим подходом. Использование вложенных сущностей может быть лучшей альтернативой, и это даже не требует улучшения байт-кода.

К сожалению, Марк продолжает эту дискуссию этим очень вводящим в заблуждение заявлением о равных и Хэш-код :

о, и то же самое относится к hashCode() и equals() в #JPA сущностях: также почти всегда ненужно и создает ущерб.

Это утверждение неверно, как будет показано в этом посте очень подробно.

Договор о равенстве

В соответствии со спецификацией Java хорошая равная реализация должна обладать следующими свойствами:

  1. возвратный
  2. симметричный
  3. переходный
  4. последовательный

Первые три довольно интуитивно понятны, но обеспечение согласованности в контексте объектов JPA и Hibernate обычно является самой большой проблемой для разработчиков.

Как уже объяснено , равно и Хэш-код должны вести себя последовательно во всех переходах состояния сущности .

Типы идентификаторов

С точки зрения равного контракта идентификаторы можно разделить на две категории:

  • Присвоенные идентификаторы
  • Идентификаторы, созданные базой данных

Присвоенные идентификаторы

Назначенные идентификаторы выделяются до очистки контекста сохранения, и мы можем дополнительно разделить их на две подкатегории:

  • Естественные идентификаторы
  • База данных-агностические UUIDs

Естественные идентификаторы присваиваются сторонним органом, таким как ISBN книги.

Номера UUID, не зависящие от базы данных, генерируются вне базы данных, например, при вызове java.util. UUID#Случайный идентификатор метод.

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

@Entity(name = "Book")
@Table(name = "book")
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
}

Для получения более подробной информации об аннотации @Natural ознакомьтесь с этой статьей .

Идентификаторы, созданные базой данных

Идентификаторы, созданные базой данных,-это совсем другая история. Поскольку идентификатор присваивается базой данных во время промывки, гарантия согласованности нарушается, если мы реализовали равенство и хэш-код на основе идентификатора, как и для назначенных идентификаторов.

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

Поэтому всякий раз, когда у вас есть идентификатор, созданный базой данных, синтетический ключ (будь то числовой идентификатор или тип UUID базы данных ), вы должны использовать следующее равно и Хэш-код реализация:

@Entity(name = "Post")
@Table(name = "post")
public class Post implements Identifiable {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    public Post() {}

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;

        if (!(o instanceof Post))
            return false;

        Post other = (Post) o;

        return id != null && 
               id.equals(other.getId());
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
 
    //Getters and setters omitted for brevity
}

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

Вот и все!

Единственный случай, когда вы увидите узкое место в производительности из-за одного хэш-блока, – это если у вас большая коллекция из десятков тысяч записей.

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

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

Кроме того, в большинстве случаев вам даже не нужно использовать Set или Карту . Для двунаправленных ассоциаций Список(ы) в любом случае работает лучше.

Марк написал пост в блоге , чтобы оправдать свои убеждения.

В своей статье Маркс говорит, что реализация равенства идентификаторов, созданных базой данных, не работает для поглощать или получить ссылку() .

Даже в продвинутой версии Влада есть дыры. Например, если вы используете em.getReference() или em.merge().

Как реализовать equals и хэш-код с использованием идентификатора сущности JPA (первичный ключ) в статье показано, что реализация equals работает для отдельных объектов. В этом был весь смысл разработки такой реализации. Мы хотим, чтобы он работал во всех переходах состояний сущностей.

Что касается getReference() , то для этого тоже есть проверка. Все это есть на GitHub .

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

К сожалению, Марк продолжает высказывать все больше заблуждений, таких как:

Зачем вам вообще нужны equals() и hashCode ()?

Это хороший вопрос. И мой ответ: “Ты этого не делаешь !”

Ну, а ты ЗНАЕШЬ!

Если вы не реализуете равно и Хэш-код тогда тест на слияние завершится неудачно, что нарушит гарантию согласованности. Все это объясняется в моем Кстати, как реализовать equals и хэш-код с использованием идентификатора сущности (первичного ключа) статья.

И еще одно заблуждение, с точки зрения гибернации

Почему вы не должны хранить управляемые и отдельные объекты в одной коллекции

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

И еще одно неправильное представление, с точки зрения реализации гибернации:

Итак, наличие кэша – действительно отличная идея, но *пожалуйста* не храните объекты JPA в кэше. По крайней мере, до тех пор, пока ими управляют.

Hibernate стремится обеспечить высокую согласованность. Вот почему стратегии параллелизма READ_WRITE и транзакционного кэша позволяют вам не беспокоиться о таких несоответствиях. Это поставщик кэша второго уровня, который гарантирует этот уровень изоляции. Точно так же, как система реляционных баз данных.

Только NONSTRICT_READ_WRITE предлагает более слабый уровень изоляции, но нестрогий выбор имен в конце концов является самоописательным.

Лучший совет, который я могу вам дать, заключается в том, что вы всегда должны подвергать сомнению каждое утверждение, которое вы читаете в Интернете. Вы всегда должны сверять все рекомендации с вашей текущей реализацией поставщика JPA, потому что детали имеют очень большое значение.