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