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

Как реализовать равенство и хэш-код для объектов JPA

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

Каждый объект Java наследует методы equals и хэш-кода, однако они полезны только для объектов значений и бесполезны для объектов, ориентированных на поведение без состояния.

В то время как сравнение ссылок с использованием оператора “==” является простым, для равенства объектов все немного сложнее.

Поскольку вы несете ответственность за определение того, что означает равенство для определенного типа объектов, обязательно, чтобы ваши реализации equals и хэш-кода следовали всем правилам, указанным в java.lang.Объект JavaDoc ( равен и Хэш-код ).

Также важно знать, как ваше приложение (и используемые в нем фреймворки) использует эти два метода.

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

В документации по гибернации перечислены ситуации, когда требуются эти два метода:

  • при добавлении сущностей в наборы коллекций
  • при повторном подключении объектов к новому контексту сохранения

Эти требования вытекают из ограничения Object.equalsсогласованный “, что приводит нас к следующему принципу:

Сущность должна быть равна самой себе во всех состояниях объекта JPA :

  • преходящий
  • прикрепленный
  • отдельный
  • удалено (до тех пор, пока объект помечен для удаления и он все еще находится в куче)

Таким образом, мы можем сделать вывод, что:

  • Мы не можем использовать автоматически увеличивающийся идентификатор базы данных в методе hashCode , поскольку переходная и присоединенная версии объектов больше не будут находиться в одном хэшированном ведре.
  • Мы не можем полагаться на реализации по умолчанию Объект равно и Хэш-код , поскольку две сущности, загруженные в двух разных контекстах сохранения, в конечном итоге будут двумя разными объектами Java, что нарушает правило равенства всех состояний.
  • Итак, если Hibernate использует равенство для уникальной идентификации объекта в течение всего срока его службы , нам нужно найти правильную комбинацию свойств, удовлетворяющую этому требованию.

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

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

Таким образом, бизнес-ключ должен быть установлен с самого момента создания Сущности, а затем никогда не изменять его.

Давайте рассмотрим несколько примеров сущностей в зависимости от их зависимостей и выберем соответствующий бизнес-ключ.

Вариант использования корневой сущности (сущность без какой-либо родительской зависимости)

Вот как реализуются равенства/хэш-код:

@Entity
public class Company {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(unique = true, updatable = false)
    private String name;

    @Override
    public int hashCode() {
        HashCodeBuilder hcb = new HashCodeBuilder();
        hcb.append(name);
        return hcb.toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Company)) {
            return false;
        }
        Company that = (Company) obj;
        EqualsBuilder eb = new EqualsBuilder();
        eb.append(name, that.name);
        return eb.isEquals();
    }
}

Поле имя представляет бизнес-ключ Компании, и поэтому оно объявлено уникальным и не подлежащим обновлению. Таким образом, два объекта компании равны, если у них одно и то же имя, игнорируя любые другие поля, которые оно может содержать.

Дочерние сущности с НЕТЕРПЕЛИВО выбранным родителем

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(updatable = false)
    private String code;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "company_id", 
                nullable = false, updatable = false)
    private Company company;

    @OneToMany(fetch = FetchType.LAZY, 
               cascade = CascadeType.ALL, 
               mappedBy = "product", 
               orphanRemoval = true)
    @OrderBy("index")
    private Set images = new LinkedHashSet();

    @Override
    public int hashCode() {
        HashCodeBuilder hcb = new HashCodeBuilder();
        hcb.append(code);
        hcb.append(company);
        return hcb.toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Product)) {
            return false;
        }
        Product that = (Product) obj;
        EqualsBuilder eb = new EqualsBuilder();
        eb.append(code, that.code);
        eb.append(company, that.company);
        return eb.isEquals();
    }
}

В этом примере мы всегда выбираем Компанию для Продукта, и, поскольку код продукта не является уникальным среди Компаний, мы можем включить родительскую сущность в наш бизнес-ключ. Родительская ссылка помечена как не обновляемая, чтобы предотвратить нарушение контракта equals/hashCode (перемещение продукта из одной компании в другую в любом случае не будет иметь смысла). Но эта модель ломается, если у Родителя есть набор дочерних сущностей, и вы называете что-то вроде:

public void removeChild(Child child) {
    children.remove(child);
    child.setParent(null);
}

Это нарушит контракт equals/hashCode, так как для родителя было установлено значение null, и дочерний объект не будет найден в коллекции children, если бы это был набор. Поэтому будьте осторожны при использовании двунаправленных ассоциаций, имеющих дочерние сущности, использующие этот тип равенства/хэш-кода.

Дочерние сущности с ЛЕНИВЫМ выбранным родителем

@Entity
public class Image {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(updatable = false)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", nullable = false, 
                updatable = false)
    private Product product;

    @Override
    public int hashCode() {
        HashCodeBuilder hcb = new HashCodeBuilder();
        hcb.append(name);
        hcb.append(product);
        return hcb.toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Image)) {
            return false;
        }
        Image that = (Image) obj;
        EqualsBuilder eb = new EqualsBuilder();
        eb.append(name, that.name);
        eb.append(product, that.product);
        return eb.isEquals();
    }
}

Если изображения извлекаются без продукта, а контекст сохранения закрыт, и мы загружаем изображения в набор, мы получим исключение LazyInitializationException, как в следующем примере кода:

List images = transactionTemplate.execute(new TransactionCallback() {
    @Override
    public List doInTransaction(TransactionStatus transactionStatus) {
        return entityManager.createQuery(
            "select i from Image i ", Image.class)
        .getResultList();
    }
});

//Throws LazyInitializationException

Поэтому я бы не рекомендовал этот вариант использования, поскольку он подвержен ошибкам, и для правильного использования equals и хэш-кода нам всегда нужно, чтобы ЛЕНИВЫЕ ассоциации были инициализированы в любом случае.

Дочерние сущности, игнорирующие родителя

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

Если вам понравилась эта статья, держу пари, вам также понравятся моя Книга и Видеокурсы.

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

Использование EqualsBuilder и HashCodeBuilder помогает нам писать краткие реализации равных и хэш-кодов, и, похоже, он также работает с прокси-серверами Hibernate.

Код доступен на GitHub .