英文:
HashCode throws a nullpointer exception
问题
ContainsJPA类的hashCode函数抛出空指针异常,可能是因为在productJpa.getId()上发生了空指针异常。为了解决这个问题,你可以检查以下几点:
-
确保ContainsJPA实例的productJpa属性不为空,否则在调用getId()时会引发空指针异常。
-
确保数据库中的“contains”表中与productJpa相关的id确实存在,并且productJpa引用的是一个有效的数据库实体。
-
检查你的数据库连接和数据完整性,确保表之间的关联正确设置。
如果你仍然无法解决问题,可以提供更多的代码和上下文信息,以便更详细地帮助你排除这个异常。
英文:
I have a puzzle for you.
I am making a herb store web app and this is my database :
- The store can have many products
- A product can contain many herbs
These are my JPA classes :
public class StoreJPA {
...
@OneToMany(mappedBy="storeJpa", cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
private Set<ProductJPA> specialOffers = new HashSet<ProductJPA>();
...
}
public class ProductJPA {
@ManyToOne
@JoinColumn(name="store_id")
private StoreJPA storeJpa;
@OneToMany(mappedBy="productJpa", cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
private Set<ContainsJPA> contains = new HashSet<ContainsJPA>();
...
private Set<HerbJPA> getHerbs(){
return contains.stream().map(h -> h.getHerbJpa()).collect(Collectors.toSet());
}
@Override
public int hashCode(){
long h = 1125899906842597L; // prime
for(ProductHasHerbJPA phh : contains){
h = 31*h + phh.getHerbJpa().getId();
}
return (int)(31*h + storeJpa.getId());
}
@Override
public boolean equals(Object o){
if(o!=null && o instanceof ProductJPA){
if(o==this)
return true;
return ((ProductJPA)o).getStoreJpa().getId()==storeJpa.getId() &&
((ProductJPA)o).getHerbs().equals(getHerbs()) // compare herbs they contain
}
return false;
}
...
}
public class ContainsJPA {
@Id
private Long id;
@ManyToOne
@JoinColumn(name="product_id")
private ProductJPA productJpa;
@ManyToOne
@JoinColumn(name="herb_id")
private HerbJPA herbJpa;
...
@Override
public int hashCode(){
long h = 1125899906842597L + productJpa.getId(); // <-- nullpointer exception
return (int)(31*h + herbJpa.getId());
}
@Override
public boolean equals(Object o){
if( o != null && o instanceof HerbLocaleJPA) {
if(o==this) {
return true;
}
return ((ProductHasHerbJPA)o).getHerbJpa().getId()==herbJpa.getId() &&
((ProductHasHerbJPA)o).getProductJpa().getId()==productJpa.getId();
}
return false;
}
...
}
Adding a new product with a list of herbs works fine.
But when i run this and try to get the products in the store, i get a NullPointerException :
> java.lang.NullPointerException at
> com.green.store.entities.ContainsJPA.hashCode(ContainsJPA.java:64)
> at java.util.HashMap.hash(HashMap.java:339) at
> java.util.HashMap.put(HashMap.java:612) at
> java.util.HashSet.add(HashSet.java:220) at
> java.util.AbstractCollection.addAll(AbstractCollection.java:344) at
> org.hibernate.collection.internal.PersistentSet.endRead(PersistentSet.java:327)
> at
> org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollection(CollectionLoadContext.java:234)
> at
> org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollections(CollectionLoadContext.java:221)
> at
> org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollections(CollectionLoadContext.java:194)
> at
> org.hibernate.loader.plan.exec.process.internal.CollectionReferenceInitializerImpl.endLoading(CollectionReferenceInitializerImpl.java:154)
> at
> org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.finishLoadingCollections(AbstractRowReader.java:249)
> at
> org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.finishUp(AbstractRowReader.java:212)
> at
> org.hibernate.loader.plan.exec.process.internal.ResultSetProcessorImpl.extractResults(ResultSetProcessorImpl.java:133)
> at
> org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:122)
> at
> org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:86)
> at
> org.hibernate.loader.entity.plan.AbstractLoadPlanBasedEntityLoader.load(AbstractLoadPlanBasedEntityLoader.java:167)
> at
> org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:4087)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:508)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:478)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:219)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:278)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:121)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89)
> at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1239)
> at
> org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1122)
> at
> org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:672)
> at org.hibernate.type.EntityType.resolve(EntityType.java:457) at
> org.hibernate.engine.internal.TwoPhaseLoad.doInitializeEntity(TwoPhaseLoad.java:165)
> at
> org.hibernate.engine.internal.TwoPhaseLoad.initializeEntity(TwoPhaseLoad.java:125)
> at
> org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.performTwoPhaseLoad(AbstractRowReader.java:238)
> at
> org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.finishUp(AbstractRowReader.java:209)
> at
> org.hibernate.loader.plan.exec.process.internal.ResultSetProcessorImpl.extractResults(ResultSetProcessorImpl.java:133)
> at
> org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:122)
> at
> org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:86)
> at
> org.hibernate.loader.entity.plan.AbstractLoadPlanBasedEntityLoader.load(AbstractLoadPlanBasedEntityLoader.java:167)
> at
> org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:4087)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:508)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:478)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:219)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:116)
> at
> org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89)
> at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1239)
> at
> org.hibernate.internal.SessionImpl.immediateLoad(SessionImpl.java:1097)
> ...
The hashCode function of ContainsJPA throws this exception when getting the id for the product. Why is it doing that, considering that the 'contains' table in the DB has this id ?
I can't figure out why this is happening. Please help.
答案1
得分: 2
您的hashCode
和equals
实现是不正确的。
问题概括如下:
- 它们不符合“委托”风格(它们不将确定相等性的任务委托给相关的类)
- 它们没有回答对象代表什么的核心问题:数据库中的一行,或者数据库中的行试图表示的概念。
委托相等性检查
hashCode
和equals
都规定“要求”您不要从中抛出空指针异常。对于equals
来说,这意味着您不能只调用a.equals(b)
,您必须将其改为a == null ? b == null : a.equals(b)
(由于这种“永不抛出异常”的特性是传递的,即使b
为null
,a.equals(b)
也是可以的),或者使用助手方法Objects.equal(a, b)
。
对于hashCode
,这意味着空值必须被定义为具有某些预定义值以进行散列。此外,更一般地,每当您有一个“子对象”(例如某种非基本类型的字段),hashCode
和equals
通常要级联:使用productJPA.hashCode()
而不是productJPA.getId()
。
对于equals
也是一样的。不要这样做:
(ProductHasHerbJPA)o).getHerbJpa().getId()==herbJpa.getId()
而是这样做:
Objects.equals(o.getHerbJpa(), herbJpa);
如果两个herb JPA
对象的ID相等,则HerbJPA
类的equals()
方法应相应地进行定义,如果不相等,则不应如此。您的ContainsJPA
类不应知道如何计算两个herbJPA
实例是否相等 - herbJPA
可以自己处理。通过这种方式,您可以避免许多空值问题。
请注意,您可以让 lombok 为您处理所有这些样板代码。
接下来,让我们来谈谈JPA和相等性的一些棘手问题。
在Java生态系统中(除了JPA/Hibernate之外),处理相等性和散列码的_常见策略_是查看对象标识的_所有字段_,通常是所有字段。问题是,这在JPA中不起作用:JPA对象上的大多数getter方法是代理,如果调用它们会导致数据库查询。对于具有足够互相连接的数据库结构(大量引用),这意味着单个equals
调用会查询一半的数据库,占用大量内存,并且需要半小时才能完成,显然不是可行的解决方案。
关键问题是:您的对象实际上代表什么,据我所知,JPA没有提供清晰的指导方针。
HerbsJPA
的实例代表数据库中的一行
然后我们可以得出以下结论:
- 始终按照规范,对象始终等于自身:
if (this == other) return true;
。否则... - 如果其中一个或两者都没有设置
unid
,那么它们之间__不能相等__ - 即使对象的每个字段完全相同,仍然不代表“相同的行”,因此不相等! - 如果两个对象都设置了
unid
,那么它们仅在unid
相等时才相等,否则它们不相等。不管其他所有值如何! - 具有相同值的两个不同行...仍然是两个不同的行。
顺便说一句,这个观点也非常方便,因为您完全可以避免那个“哎呀,它查询了整个数据库”的问题。unid
不昂贵,通常已经预先获取。
HerbsJPA
的实例代表一个“herb”。
如果是这种情况,我可以建议您的类命名错误。它应该是“Herb”,可能是“HerbJpa”(注意:JPA全大写违反了最常见的样式规则)。
那么,最合理的解决方案是_避免_检查unid
,只查看所有其他字段(或者至少查看所有其他表示草药标识的字段。通常是大多数字段,但有时您可以定义一些属性,会引发大量数据库查询,例如“关联草药列表”,在数据库中表示为连接表,作为“标识的一部分”)。毕竟,“数据库中的unid”是“草药”的概念的一个附带实现细节,因此显然不可能是其标识的一部分!
这个观点的缺点当然是那个“大量数据库调用”的问题。
一般来说,我建议您将这些对象视为表示“表中的行”而不是“实际草药”,在这种情况下,您的equals
和hashCode
方法变得相对简单,类名也是可以接受的(好吧,它应该是“Jpa”,而不是“JPA”,但除此之外)。
英文:
Your hashCode and equals implementations are incorrect.
The problems with it, in a nutshell:
- They do not adhere to the 'delegation' style (they do not delegate the job of determining equality to the relevant classes)
- They do not answer the central question of what the object represents: Row in a DB, or the notion that the row in the DB is trying to represent.
Delegate equality checks
Both hashCode and equals are specced to require that you do not throw NPEs out of them. For equals, that means you can't just call, say, a.equals(b)
- you'd have to make that a == null ? b == null : a.equals(b)
(and because this 'never throw' is transitive, a.equals(b)
is fine, even if b is null), or use the helper Objects.equal(a, b)
instead.
For hashcode, it means that null values must be defined as having some predefined value for the sake of hashing. Also, more generally, whenever you have a 'sub object' (e.g. a field of some non-primitive type', the general idea is for hashCode and equals to cascade: Use productJPA.hashCode()
and not productJPA.getId()
.
Same goes for equals. Don't do this:
(ProductHasHerbJPA)o).getHerbJpa().getId()==herbJpa.getId()
but do this:
Objects.equals(o.getHerbJpa(), herbJpa);
And if 2 herb JPAs are to be considered equal if their IDs are equal, then the HerbJPA class's equals()
method should be defined accordingly, and if not, then not. It is not the job of your ContainsJPA class to know how to calculate if 2 herbJPA instances are equal - herbJPA can do that, itself. In passing you avoid a ton of null issues by doing it this way.
Note, you can let lombok take care of all this boilerplate for you.
Next, we get to some hairy issues with JPA and equality in particular.
The common strategy to do equals/hashCode in the java ecosystem (outside of JPA/Hibernate) is to look at all fields that are part of an object's identity, which is usually all of them. The problem is, that doesn't work well with JPA: Most of the getter methods on a JPA object are proxies which cause DB queries if you invoke them. With a sufficiently interconnected db structure (lots of references), that means a single equals
call ends up querying half your DB, takes a ton of memory, and half an hour to complete, obviously not a feasible solution.
The key question is: What does your object actually represent, and as far as I know, JPA does not give clear guidance.
An instance of HerbsJPA represents a row in a database
Then we can draw the following conclusions:
- As always, by spec, an object is always equal to itself:
if (this == other) return true;
. Otherwise... - If either or both of the objects have no set unid, then they cannot be equal to each other - 2 unwritten rows, even if entirely identical for every field in the object, still does not represent 'the same row', therefore, not equal!
- If both objects have a set unid, then they are equal if the unids are, and otherwise, they are not. Regardless of all the other values! - 2 different rows with identical values are... still two different rows.
This view incidentally is also convenient in that you entirely avoid that 'whoops it queries the entire DB' issue. unids are not expensive to fetch, and are usually prefetched already.
An instance of HerbsJPA represents a 'herb'.
If this is the case, may I suggest your class is misnamed? It should be 'Herb', probably. Maybe 'HerbJpa' (NB: JPA in all-caps is a violation of the most common style rule).
Then the most sensical solution is to AVOID checking the unid entirely, and look only at all the other fields (or at least, all the other ones that represent something about the herb's identity. This is usually most of them, but sometimes you can get away with defining some property that would cause a storm of DB queries, such as 'a list of associated herbs', represented in the DB with a join table, as 'not part of the identity'. After all, 'the unid in the db' is an incidental implementation detail of the notion of a 'herb' and therefore couldn't possibly be part of the identity of it!
The downside of this view is of course that 'storm of DB calls' issue.
Generally I advise you treat these objects as representing 'row in a table' and not 'the actual herb', in which case, your equals and hashCode methods become relatively simple, and the name of the class is fine (well, it should be 'Jpa', not 'JPA', but other than that).
@Override public int hashCode() {
return id == null ? super.hashCode() : (int) id;
// note, other answer's id %1000 is silly;
// it is needlessly inefficient, don't do it that way.
}
@Override public boolean equals(Object other) {
if (other == this) return true;
if (other == null || other.getClass() != ContainsJPA.class) return false;
return id == null ? false : id.equals(other.id);
}
答案2
得分: 0
AbstractRowReader首先加载集合,然后“hydrate”相关实体吗?
AbstractRowReader#finishUp()
...
//现在我们可以完成加载集合
完成加载集合(context);
//最后,执行后加载操作
postLoad(postLoadEvent, context, hydratedEntityRegistrations, afterLoadActionList
);
这意味着在创建集合时,已知product_id,但ProductJPA实例尚未被hydrate。
老实说,我认为从关联实体派生哈希码并不是一个很好的做法。我可能会这样做:
public class ContainsJPA {
@Id
private Long id;
@Override
public int hashCode(){
return id == null ? super.hashCode() : id % 1000;
}
以获得一些分布('1000'是一个魔术数字,取决于典型的集合大小)。
英文:
Not 100% sure, but doesn't AbstractRowReader first load the collection and then "hydrate" the associated entities?
AbstractRowReader#finishUp()
...
// now we can finalize loading collections
finishLoadingCollections( context );
// finally, perform post-load operations
postLoad( postLoadEvent, context, hydratedEntityRegistrations, afterLoadActionList
);
Which means when creating the collection, the product_id is known, but the ProductJPA instance hasn't been hydrated yet.
tbh, I think it's not great practice to derive a hashcode from associated entities. I'd probably do something like
public class ContainsJPA {
@Id
private Long id;
@Override
public int hashCode(){
return id == null ? super.hashCode() : id % 1000;
}
to get some distribution (the '1000' is a magic number, depending on what typical collection sizes are).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论