Статьи

Факты гибернации: равно и HashCode

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

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

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

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

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

После просмотра документации Hiberante я наткнулся на эти две ссылки: документы Equals и HashCode и Hiberante 4.3, указывающие контексты, где требуются два метода:

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

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

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

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

Поэтому мы можем сделать вывод, что:

  1. Мы не можем использовать автоинкрементный идентификатор базы данных для сравнения объектов, поскольку временные и присоединенные версии объектов не будут равны друг другу.
  2. Мы не можем полагаться на реализации Object equals / hashCode по умолчанию, так как два объекта, загруженные в двух разных контекстах персистентности, в конечном итоге станут двумя разными объектами Java, что нарушит правило равенства всех состояний.
  3. Поэтому, если Hibernate использует равенство для однозначной идентификации объекта в течение всего срока его службы, нам необходимо найти правильную комбинацию свойств, удовлетворяющую этому требованию.

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

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

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

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

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

Вот как реализован equals / hashCode:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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();
    }
}

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

  • Дочерние объекты с EAGER-выбранным родителем
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@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(name);
        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(name, that.name);
        eb.append(company, that.company);
        return eb.isEquals();
    }
}

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

1
2
3
4
public void removeChild(Child child) {
    children.remove(child);
    child.setParent(null);
}

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

  • Дочерние сущности с ленивым родителем
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@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, как в следующем примере кода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
List images = transactionTemplate.execute(new TransactionCallback<List>() {
    @Override
    public List doInTransaction(TransactionStatus transactionStatus) {
        return entityManager.createQuery(
                "select i from Image i ", Image.class)
                .getResultList();
    }
});
try {
    assertTrue(new HashSet(images).contains(frontImage));
    fail("Should have thrown LazyInitializationException!");
} catch (LazyInitializationException expected) {
 
}

Поэтому я бы не рекомендовал этот вариант использования, так как он подвержен ошибкам, и для правильного использования equals и hashCode нам всегда требуется инициализация LAZY-ассоциаций.

  • Дочерние объекты, игнорирующие родителя

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

Вывод

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

Использование EqualsBuilder и HashCodeBuilder помогает нам писать краткие реализации equals и hashCode, и, похоже, работает также с Hibernate Proxies.