Hibernate: Lazy initialization vs broken hashcode/equals conundrum
Asked Answered
I

3

12

I'm quite new to JPA and Hibernate (I'm studying hard though!) and I am struggling with a problem that I can't seem to find a trivial solution for, so here it is.

I have an entity that looks kinda like the following:

@Entity
@Table(name = "mytable1")
public class EntityOne {
  // surrogate key, database generated
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Long id;

  // business key
  @Column(name = "identifier", nullable = false, unique = true)
  private String identifier;

  @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH)
  @JoinColumn(name = "twoId", nullable = false)
  private EntityTwo two;

  @OneToMany(mappedBy = "entityOne", fetch = FetchType.EAGER, 
    cascade = {CascadeType.ALL}, orphanRemoval = true)
  private Set<EntityThree> resources = new HashSet<>();

  // getters/setters omitted

  @Override
  public int hashCode() {
    // the business key should always be defined (through constructor/query)
    // if this is null the class violates the general hashcode contract 
    // that the integer value returned must always be the same
    Assert.notNull(identifier);
    // a dirty alternative would be:
    // if(identifier==null) return 0;
    return identifier.hashCode();
  }

  @Override
  public boolean equals(Object o) {
    return o instanceof ResourceGroup 
      && ((ResourceGroup) o).identifier.equals(identifier);
  }
}

My project is set up with Spring JPA, so I have my CrudRepository<EntityOne,Long> injected in a Service class that has a few @Transactional methods and I scan my domain/service packages for JPA and transactions respectively.

One of the service methods calls the repository's findAll() method and returns a list of EntityOnes. Everything works fine unless I try to access the getter for two, which obviously throws:

org.hibernate.LazyInitializationException: could not initialize proxy - no Session

I thought it might be useful to have this object initialized, so I switched the fetching type from lazy to eager. However, if I do that I get the following:

java.lang.IllegalArgumentException: [Assertion failed] - this argument is required; it must not be null
    at org.springframework.util.Assert.notNull(Assert.java:112)
    at org.springframework.util.Assert.notNull(Assert.java:123)
    at my.pkg.domain.EntityOne.hashCode(ResourceGroup.java:74)
    at java.util.HashMap.hash(HashMap.java:351)
    at java.util.HashMap.put(HashMap.java:471)
    at java.util.HashSet.add(HashSet.java:217)
    at java.util.AbstractCollection.addAll(AbstractCollection.java:334)
    at org.hibernate.collection.internal.PersistentSet.endRead(PersistentSet.java:346)
    at org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollection(CollectionLoadContext.java:243)
    at org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollections(CollectionLoadContext.java:233)
    at org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollections(CollectionLoadContext.java:209)
    at org.hibernate.loader.Loader.endCollectionLoad(Loader.java:1149)
//...

I briefly looked at Hibernate's source code and it looks like it's trying to put my EntityOne objects in a set before their business key is initialized. Is my interpretation correct? Is there a way around this? Am I doing something incredibly dumb?

I appreciate your help

EDIT: I just want to clarify that what I'm trying to understand here is what the best practices are specifically with respect to JPA and Hibernate. If this was a plain POJO I could make the identifier field final (I would actually make the whole class immutable) and be safe. I can't do this because I'm using JPA. So the questions: do you violate the hashCode contract and in which way? How does Hibernate deal with this violation? What's the JPA recommended way of doing this in general? Should I get rid of hash based collections altogether and use lists instead?

Giovanni

Integral answered 18/7, 2013 at 21:58 Comment(1)
I replaced all my relational Set fields' initial value with new LinkedHashMap()' instead of new HashMap()` and it started working. Is it not strange?Jochebed
I
0

I believe I actually found a way to make this work a little better, i.e., forcing Hibernate (or whatever JPA provider) to have the key available before sticking objects in the collection. In this scenario, the object will be properly initialized and we can be sure that the business key won't be null.

For example, here's how the class EntityTwo would have to look:

@Entity
@Table(name = "mytable2")
public class EntityTwo {
  // other code omitted ...
  @OneToMany(mappedBy = "entityTwo", fetch = FetchType.EAGER, 
    cascade = {CascadeType.ALL}, orphanRemoval = true)
  @MapKey(name = "identifier")
  private Map<String, EntityOne> entityOnes = new HashMap<>();
}

I haven't tested this specific code but I have other working examples and it should work fine according to the JPA docs. In this case, the JPA provider is cornered: it must know the value of identifier before it can put the object in the collection. Besides, the object's hashCode and equals are not even called because the mapping is explicitly handled by the JPA provider.

This is a case in which explicitly forcing the tool to understand the way things are modeled and related to each other leads to great benefit.

Integral answered 24/1, 2014 at 14:58 Comment(0)
O
10

No, you're not doing anything dumb. Implementing equals and hashCode on a JPA entity is a matter of much heated debate, and all of the approaches I know about have significant drawbacks. There's no obvious, trivial solution that you're just missing.

You have, however, hit on a case which is not discussed very much for some reason. The hibernate wiki recommends using a business key as you are doing, and "Java Persistence with Hibernate" (Bauer / King, 2007, widely regarded as the standard Hibernate reference work) on page 398 recommends the same thing. But in some situations, as you observe, Hibernate can add an entity into a Set before its fields are initialized, so the business-key-based hashCode doesn't work, just as you point out. See Hibernate issue HHH-3799 for discussion of this case. There is an expected-to-fail test case in the Hibernate source code demonstrating the issue, added in 2010, so at least one Hibernate developer considers it to be a bug and wants to fix it, but there hasn't been any activity since 2010. Please consider voting for that issue.

One solution you might consider is to expand the scope of your session so that all your access to entities happens within the same session. Then you can make your Set<EntityThree> be lazy-fetched instead of eager-fetched, and you'll avoid the eager-fetching problem in HHH-3799. Most applications I've worked on make only sparing use of objects in the detached state. It sounds like you're loading your entity and then using it for a while after the session ends; that's a pattern I'd recommend against. If you're writing a web application, see the "open session in view" pattern and Spring's OpenSessionInViewFilter for ideas on how to do this.

Incidentally, I like how you throw an exception when the business key is not initialized; that way you can catch coding errors quickly. Our application has a nasty bug due to HHH-3799 which we might have caught in development if we had used your not-null assertion.

Osage answered 12/9, 2013 at 23:54 Comment(1)
Unfortunately to make it work I had to remove the assertion. I think it fails even within the same transaction, I have to double check. Thanks for the reply though!Integral
U
0

Your interpretation is correct. As a first first step code your hashCode() and equals() with your id field - the one you are telling Hibernate that is your id.

As a second step implement a correct hashCode() and equals() to save you from future trouble. There are plenty of resources if you google it. Here is one on this site

Uzzial answered 18/7, 2013 at 22:38 Comment(0)
I
0

I believe I actually found a way to make this work a little better, i.e., forcing Hibernate (or whatever JPA provider) to have the key available before sticking objects in the collection. In this scenario, the object will be properly initialized and we can be sure that the business key won't be null.

For example, here's how the class EntityTwo would have to look:

@Entity
@Table(name = "mytable2")
public class EntityTwo {
  // other code omitted ...
  @OneToMany(mappedBy = "entityTwo", fetch = FetchType.EAGER, 
    cascade = {CascadeType.ALL}, orphanRemoval = true)
  @MapKey(name = "identifier")
  private Map<String, EntityOne> entityOnes = new HashMap<>();
}

I haven't tested this specific code but I have other working examples and it should work fine according to the JPA docs. In this case, the JPA provider is cornered: it must know the value of identifier before it can put the object in the collection. Besides, the object's hashCode and equals are not even called because the mapping is explicitly handled by the JPA provider.

This is a case in which explicitly forcing the tool to understand the way things are modeled and related to each other leads to great benefit.

Integral answered 24/1, 2014 at 14:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.