在hibernate中将散列函数委托给未初始化的委托会导致更改hashCode

问题描述 投票:6回答:6

我有一个问题,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()的实施完全遵循in the hibernate documentation给出的建议。

现在,如果我加载Compound类型的对象,它会急切地加载HasSet和部件。这会调用部件上的hashCode(),然后调用化合物上的hashCode()。但问题是,此时并非所有考虑用于创建复合的hashCode的值都可用。因此,部件的hashCode在初始化完成后改变,从而制动HashSet的契约并导致各种难以跟踪的错误(例如,在部件组中设置两次相同的对象)。

所以我的问题是:避免这个问题的最简单的解决方案是什么(我想避免为自定义加载/初始化编写类)?我完全做错了吗?

编辑:我错过了什么吗?这似乎是一个基本问题,为什么我在任何地方都找不到任何关于它的东西?

您应该使用一组用于标识各个对象的equals()属性,而不是使用数据库标识符进行相等性比较。 [...]无需使用持久性标识符,所谓的“业务密钥”要好得多。这是一个自然的关键,但这次使用它没有错! (article from hibernate

建议您使用Business key equality实现equals()和hashCode()。业务键等式意味着equals()方法仅比较形成业务键的属性。它是识别我们在现实世界中的实例的关键(自然候选键)。 (hibernate documentation

编辑:这是加载发生时的堆栈跟踪(如果这有帮助)。在那个时间点,属性someUniqueName为null,因此hashCode被错误地计算。

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>
java hibernate initialization hashcode
6个回答
2
投票

你有一个完美的合法用例,事实上它应该有效。但是,如果在设置'someUniqueName'之前设置Compound对象的'parts',那么在常规Java中会遇到同样的问题。

因此,如果你能说服hibernate在'parts'属性之前设置'someUniqueName'属性。您是否尝试过在java类中重新排序它们?或者将“部分”重命名为“zparts”? hibernate文档只是说订单不能保证。我在hibernate中提交了一个错误,允许强制执行此命令...

另一种可能更容易的解决方案:

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
投票

从您的问题我明白,所有参与hashCode()方法的模型属性都没有默认加载。在这种情况下,如果您想要加载所有属性,那么您可以按照方法进行操作。

  1. 通过调用模型类的hashCode()中的getter方法,因为它初始化/加载所有模型属性。
  2. 通过使用sesstion.get()而不是session.load()方法,因为它不会创建任何代理并将加载模型的所有属性。
  3. 通过为映射中的所有属性设置lazy="false"

希望这可以解决您的问题!


2
投票

我在其中一条评论中读到了你让Eclipse生成equalshashCode方法的问题。

你是否为所有实体(PartCompound)做到了这一点?我问,因为如果是这种情况,那些方法通常直接访问对象属性(即不调用getter方法)。它们看起来如下。

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

当使用Hibernate时,这通常会导致类似你所描述的问题,因为未初始化的属性具有默认值(对象为nullints为0,依此类推),直到调用适当的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;
}

值得注意的另一点是:Eclipse生成的equals方法包含执行此检查getClass() != obj.getClass(),它不适用于由Hibernate代理扩展的Hibernate实体。我会用instanceof支票替换它。


2
投票

那么为什么不使用列表而不是一组?我知道这将是一个解决方法,而不是一个正确的解决方案,但你根本不必使用哈希码。


0
投票

我想到的一种可能性是遵循this article给出的建议。他们基本上建议不要使用hibernate(或者更确切地说是数据库)来生成ID,而是使用UUID库来生成自己的ID,然后将这些ID用于equals()hashCode()。除了本文中提到的问题,它对我当前的实现有一些严重的缺点:它将破坏我现有的代码!每次我创建一个Part实例时,我首先要查询它是否已经存在于数据库中并检查它是否存在。在我目前的实现中,我只是按照自己喜欢的方式创建Parts,然后将它们添加到Compound中。如果Compound已经有了这样的一部分,一切都会自动完成......


0
投票

我发现了一个相关的问题here。解决方案的基本思想是,一旦您不急切地获取部件,整个问题就会消失。然后在加载零件时,化合物已完全初始化。但是,当在具有分离对象的会话之外工作时,这会打开一个完全不同的问题...

© www.soinside.com 2019 - 2024. All rights reserved.