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