Делегирование хэш-функции неинициализированным делегатам в спящем режиме приводит к изменению hashCode - PullRequest
6 голосов
/ 31 декабря 2011

У меня проблема с hashCode(), которая делегирует неинициализированные объекты с помощью hibernate.

Моя модель данных выглядит следующим образом (следующий код сильно сокращен, чтобы подчеркнуть проблему и, следовательно, не работает, не реплицируется!):

class Compound {
  @FetchType.EAGER
  Set<Part> parts = new HashSet<Part>();

  String someUniqueName;

  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((getSomeUniqueName() == null) ? 0 : getSomeUniqueName().hashCode());
    return result;
  }
}

class Part {
  Compound compound;

  String someUniqueName;

  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((getCompound() == null) ? 0 : getCompound().hashCode());
    result = prime * result + ((getSomeUniqueName() == null) ? 0 : getSomeUniqueName().hashCode());
    return result;
  }
}

Обратите внимание, что реализация hashCode() полностью следует рекомендациям, данным в документации по спящему режиму .

Теперь, если я загружаю объект типа Compound, он охотно загружает HasSet деталями.Это вызывает hashCode() на деталях, что, в свою очередь, вызывает hashCode() на соединении.Однако проблема заключается в том, что на этом этапе еще не все значения, которые рассматриваются для создания хэш-кода соединения, все еще доступны.Следовательно, хэш-код частей изменяется после завершения инициализации, что приводит к торможению контракта HashSet и приводит к всевозможным ошибкам, которые трудно отследить (например, наличие одного и того же объекта вдетали устанавливаются дважды).

Итак, мой вопрос: каково самое простое решение, чтобы избежать этой проблемы (я хотел бы избежать написания классов для пользовательской загрузки / инициализации)?Я делаю что-то здесь не так?

Редактировать : Я что-то здесь упускаю?Это кажется основной проблемой, почему я нигде ничего об этом не нахожу?

Вместо использования идентификатора базы данных для сравнения на равенство, вы должны использовать набор свойств для equals ()которые идентифицируют ваши отдельные объекты.[...] Нет необходимости использовать постоянный идентификатор, так называемый «бизнес-ключ» намного лучше.Это естественный ключ, но на этот раз в его использовании нет ничего плохого!( статья из hibernate )

И

Рекомендуется реализовать equals () и hashCode () с использованием равенства бизнес-ключей.Равенство бизнес-ключей означает, что метод equals () сравнивает только те свойства, которые образуют бизнес-ключ.Это ключ, который идентифицирует наш экземпляр в реальном мире (естественный ключ-кандидат).( документация hibernate )

Редактировать: Это трассировка стека, когда происходит загрузка (если это помогает).В этот момент атрибут someUniqueName является нулевым, и, следовательно, хэш-код вычисляется неправильно.

Compound.getSomeUniqueName() line: 263  
Compound.hashCode() line: 286   
Part.hashCode() line: 123   
HashMap<K,V>.put(K, V) line: 372    
HashSet<E>.add(E) line: 200 
HashSet<E>(AbstractCollection<E>).addAll(Collection<? extends E>) line: 305 
PersistentSet.endRead() line: 352   
CollectionLoadContext.endLoadingCollection(LoadingCollectionEntry, CollectionPersister) line: 261   
CollectionLoadContext.endLoadingCollections(CollectionPersister, List) line: 246    
CollectionLoadContext.endLoadingCollections(CollectionPersister) line: 219  
EntityLoader(Loader).endCollectionLoad(Object, SessionImplementor, CollectionPersister) line: 1005  
EntityLoader(Loader).initializeEntitiesAndCollections(List, Object, SessionImplementor, boolean) line: 993  
EntityLoader(Loader).doQuery(SessionImplementor, QueryParameters, boolean) line: 857    
EntityLoader(Loader).doQueryAndInitializeNonLazyCollections(SessionImplementor, QueryParameters, boolean) line: 274 
EntityLoader(Loader).loadEntity(SessionImplementor, Object, Type, Object, String, Serializable, EntityPersister, LockOptions) line: 2037    
EntityLoader(AbstractEntityLoader).load(SessionImplementor, Object, Object, Serializable, LockOptions) line: 86 
EntityLoader(AbstractEntityLoader).load(Serializable, Object, SessionImplementor, LockOptions) line: 76 
SingleTableEntityPersister(AbstractEntityPersister).load(Serializable, Object, LockOptions, SessionImplementor) line: 3293  
DefaultLoadEventListener.loadFromDatasource(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 496    
DefaultLoadEventListener.doLoad(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 477    
DefaultLoadEventListener.load(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 227  
DefaultLoadEventListener.proxyOrLoad(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 269   
DefaultLoadEventListener.onLoad(LoadEvent, LoadEventListener$LoadType) line: 152    
SessionImpl.fireLoad(LoadEvent, LoadEventListener$LoadType) line: 1090  
SessionImpl.internalLoad(String, Serializable, boolean, boolean) line: 1038 
ManyToOneType(EntityType).resolveIdentifier(Serializable, SessionImplementor) line: 630 
ManyToOneType(EntityType).resolve(Object, SessionImplementor, Object) line: 438 
TwoPhaseLoad.initializeEntity(Object, boolean, SessionImplementor, PreLoadEvent, PostLoadEvent) line: 139   
QueryLoader(Loader).initializeEntitiesAndCollections(List, Object, SessionImplementor, boolean) line: 982   
QueryLoader(Loader).doQuery(SessionImplementor, QueryParameters, boolean) line: 857 
QueryLoader(Loader).doQueryAndInitializeNonLazyCollections(SessionImplementor, QueryParameters, boolean) line: 274  
QueryLoader(Loader).doList(SessionImplementor, QueryParameters) line: 2542  
QueryLoader(Loader).listIgnoreQueryCache(SessionImplementor, QueryParameters) line: 2276    
QueryLoader(Loader).list(SessionImplementor, QueryParameters, Set, Type[]) line: 2271   
QueryLoader.list(SessionImplementor, QueryParameters) line: 459 
QueryTranslatorImpl.list(SessionImplementor, QueryParameters) line: 365 
HQLQueryPlan.performList(QueryParameters, SessionImplementor) line: 196 
SessionImpl.list(String, QueryParameters) line: 1268    
QueryImpl.list() line: 102  
<my code where the query is executed>

Ответы [ 6 ]

2 голосов
/ 05 января 2012

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

2 голосов
/ 05 января 2012

У вас есть идеальный законный вариант использования, и он действительно должен работать. Однако у вас будет такая же проблема в обычной Java, если вы установите «части» объекта Compound до того, как вы установите «someUniqueName».

Так что, если бы вы могли убедить hibernate установить свойство 'someUniqueName' перед свойством 'parts'. Вы экспериментировали только с переупорядочением их в классе Java? Или переименование «частей» в «zparts»? Документы в спящем режиме говорят, что порядок не гарантирован. Я бы отправил сообщение об ошибке в спящем режиме, чтобы разрешить выполнение этого порядка ...

Другое решение, которое может быть проще:

class Part {
  public int hashCode() {
    //don't include getCompound().hashCode()
    return getSomeUniqueName() == null ? 0 : getSomeUniqueName().hashCode();
  }

  public boolean equals(Object o)
  {
    if (this == o) return true;
    if (!o instanceof Part) return false;

    Part part = (Part) o;

    if (getCompound() != null ? !getCompound().equals(part.getCompound()) : part.getCompound()!= null) 
       return false;
    if (getSomeUniqueName()!= null ? !getSomeUniqueName().equals(part.getSomeUniqueName()) : part.getSomeUniqueName()!= null)
        return false;

    return true;
  }
}

В Compound.equals () убедитесь, что он также начинается с

public boolean equals(Object o)
{
    if (this == o) return true;

Это должно избежать проблемы, с которой вы сейчас сталкиваетесь.

Каждое свойство в методе hashCode () должно быть в методе equals (), но не обязательно наоборот.

2 голосов
/ 04 января 2012

Я прочитал в одном из комментариев к вопросу, что вы позволяете Eclipse генерировать методы equals и hashCode.

Вы делали это для всех сущностей (Part и Compound)? Я спрашиваю, потому что, если это так, эти методы, как правило, напрямую обращаются к свойствам объекта (то есть без вызова методов получения). Они выглядят следующим образом.

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((prop == null) ? 0 : prop.hashCode());
    return result;
}

При использовании Hibernate это часто приводит к проблемам, подобным той, которую вы описываете, потому что неинициализированные свойства имеют значения по умолчанию (null для объектов, 0 для int s и т. Д.) до соответствующего Метод get называется , поэтому прокси-сервер hibernate получает доступ к базе данных и загружает значения, необходимые для вычисления правильных значений для методов.

Вы можете легко определить проблему, запустив отладчик и проверив свойства при первом вызове hashCode().

Если это произойдет, самый простой способ исправить это - изменить ваши методы для использования методов get, как здесь:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((getProp() == null) ? 0 : getProp().hashCode());
    return result;
}

Еще один момент, на который стоит обратить внимание: метод Equals, созданный Eclipse, выполняет эту проверку getClass() != obj.getClass(), которая не подходит для сущностей Hibernate, расширенных прокси Hibernate. Я бы заменил это проверкой instanceof.

2 голосов
/ 04 января 2012

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

  1. Вызывая методы получения в hashCode() вашего класса модели, поскольку он инициализирует / загружает все свойства модели.
  2. Используя sesstion.get() вместо session.load() метода, так как он не создаст прокси и загрузит все свойства вашей модели.
  3. Установив lazy="false" для всех ваших свойств в отображении.

Надеюсь, что это может решить вашу проблему!

0 голосов
/ 04 января 2012

Я нашел связанный вопрос здесь .Основная идея решения заключается в том, что вся проблема исчезнет, ​​как только вы не начнете охотно доставать детали.Затем соединение уже полностью инициализируется при загрузке деталей.Однако это открывает совсем другую проблему при работе вне сеансов с отсоединенными объектами ...

0 голосов
/ 04 января 2012

Одна возможность, о которой я подумал, - это следовать советам, данным в этой статье . В основном они предлагают не использовать hibernate (или, скорее, базу данных) для генерации идентификаторов, но использовать библиотеку UUID для генерации собственных идентификаторов, а затем использовать эти идентификаторы для equals() и hashCode(). Помимо проблем, упомянутых в этой статье, у меня есть некоторые серьезные недостатки для моей текущей реализации: это сломает мой существующий код! Каждый раз, когда я создавал Part -экземпляр, мне сначала приходилось запрашивать, существует ли он уже в базе данных, и извлекать его, если это так. В моей текущей реализации я просто создаю Parts, как мне нравится, и просто добавляю их к Compound. Если у Compound уже есть такая часть, все получается автоматически ...

...