Библиотека Hibernate устроена так, что любой объект, хранимый в базе данных, должен иметь идентификатор. В теории, идентификатор, он же первичный ключ соответствующей таблички в БД, может быть как естественным (то есть в качестве идентификатора можно взять какое-то уникальное поле у объекта), так и суррогатным. Но на практике, чаще всего встречается второй случай. То есть у каждого объекта есть вот такое поле:
private Integer id;
Это поле по определению уникально. Это очень полезное свойство. И им можно воспользоваться в методах equals и hashCode. И чтобы не дублировать код, я обычно описываю базовый класс с полем id и методами equals и hashCode.
public class AbstractEntity implements Serializable { public Integer id; protected AbstractEntity() { } protected AbstractEntity(int id) { this.id = id; } public Integer getId() { return id; } @Override public int hashCode() { return id != null ? id : super.hashCode(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof AbstractEntity)) return false; // hibernate(cglib) создает свои классы // и getClass() == o.getClass() // выполняется не корректно не всегда if (!o.getClass().isAssignableFrom(getClass()) && !getClass().isAssignableFrom(o.getClass())) return false; AbstractEntity entity = (AbstractEntity) o; return id!= null && id.equals(entity.id); } }
hashcode очень простой и быстрый. Он возвращает идентификатор для существующих объектов и вызывает супер-функцию для новый объектов, идентификатор для которых еще не проставлен. Такой алгоритм обеспечивает уникальный хэш для разных объектов и быстрое выполнение операций.
equals для начала сверяет равенство ссылок. Затем мы проверяем что объекты относятся к одному типу. Такая проверка необходима в абстрактном классе, от которого будет наследоваться множество классов. И если бы её не было, то разные сущности с одинаковым id были равны, что не верно. Использование равенства в данном случае опасно, так как hibernate создает proxy-объекты которые наследуются от классов сущностей.
После проверки типа остается проверить равенство идентификаторов. Не стоит забывать что у новых объектов идентификатор null. И для новых объектов применима следующая логика:
- если один из сравниваемых объектов имеет идентификатор, а другой нет, то они не равны, так как один объект уже храниться в базе, а другой нет
- если оба объекта новые, то они равны, если ссылаются на один и тот же объект, что обеспечивается первой строчкой нашего метода.
В принципе, такой базовый класс можно использовать и с xml-маппингом и hibernate annotation/JPA. В первом случае, когда мы будем описывать уже конкретную сущность, нам нужно будет указать тэг id
<class name=”ConcreteClass”> <id name="id"> ... </class>
А вот, для случая hibernate annotation/JPA нам придется добавить аннотаций в исходный класс. Например, вот так:
@MappedSuperclass public class AbstractEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "ID", nullable = false) public Integer id; }
Но такой вариант подойдет только если в разных таблицах идентификаторы имеют совершено одинаковое описание - то есть совпадает имя, способ генерации и т.п. Если же поля с идентификаторами в табличках различаются, хотя бы, именем, то описывать идентификатор нужно в классе сущности. Чтобы при этом сохранить наш базовый класс, мы можем схитрить и обращаться к полю id не напрямую, а через абстрактный метод. В этом случае, даже аннотировать базовый класс не нужно:
abstract public class AbstractEntity implements Serializable { abstract public Integer getId(); @Override public int hashCode() { return getId() != null ? getId() : super.hashCode(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AbstractEntity entity = (AbstractEntity) o; return getId() != null && getId().equals(entity.getId()); } }
А сами сущности будут выглядеть примерно так:
@Entity public class SomeEntity extends AbstractEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "SOME_ID", nullable = false) public Integer id; // other fields @Override public T getId() { return id; } // other setters and getters }