Каждый объект Java наследует методы equals и hashCode, но они полезны только для объектов Value и не используются для объектов, ориентированных на поведение без состояния.
Хотя сравнение ссылок с использованием оператора «==» является простым, для равенства объектов все немного сложнее.
Поскольку вы несете ответственность за указание значения равенства для определенного типа объекта, обязательно, чтобы ваши реализации equals и hashCode следовали всем правилам, указанным в java.lang.Object JavaDoc ( equals () и hashCode () ).
Также важно знать, как ваше приложение (и используемые им фреймворки) используют эти два метода.
К счастью, Hibernate не требует, чтобы они проверяли, изменились ли сущности, для этого предусмотрены специальные механизмы грязной проверки.
После просмотра документации Hiberante я наткнулся на эти две ссылки: документы Equals и HashCode и Hiberante 4.3, указывающие контексты, где требуются два метода:
- при добавлении сущностей в набор коллекций
- при повторном присоединении сущностей к новому контексту постоянства
Эти требования вытекают из « согласованного » ограничения Object.equals, что приводит нас к следующему принципу:
Сущность должна быть равна себе во всех состояниях объекта JPA :
- преходящий
- приложенный
- отдельный
- удалено (пока объект помечен для удаления и все еще живет в куче)
Поэтому мы можем сделать вывод, что:
- Мы не можем использовать автоинкрементный идентификатор базы данных для сравнения объектов, поскольку временные и присоединенные версии объектов не будут равны друг другу.
- Мы не можем полагаться на реализации Object equals / hashCode по умолчанию, так как два объекта, загруженные в двух разных контекстах персистентности, в конечном итоге станут двумя разными объектами Java, что нарушит правило равенства всех состояний.
- Поэтому, если 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
|
@Entitypublic 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
|
@Entitypublic 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
|
@Entitypublic 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.