Home > Back-end >  Hibernate loads wrong entity with Polymorphism
Hibernate loads wrong entity with Polymorphism

Time:08-24

I'm pretty new to Java ecosystem and, sometimes, I'm struggling with some features.

In my project I have the similar list of entities and repositories:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
class Tournament {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private int id;
}

@Entity
class SpecialTournament extends Tournament {
  // ... extra fields
}

@Entity
class Match {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private int id;

  @ManyToOne(fetch = FetchType.LAZY)
  private Tournament tournament;

  private int value;
}

@Entity
class SpecialEvent {
  @ManyToOne(fetch = FetchType.LAZY)
  private SpecialTournament tournament;

  private int value;
  private int importantField;
}

interface SpecialEventRepository extends JpaRepository<SpecialEntity, Integer> {
  @Query("FROM SpecialEvent WHERE tournament=:tournament AND value=:value ORDER BY importantField")
  Iterable<SpecialEvent> findForMatchField(Tournament tournament, int value)
}

@Service
class SpecialEventController {
  void doSmth(int matchId, int value) {
    var match = matchRepository.findById(matchId);

    var specialEvents = specialEventRepository.findForMatchField(match.tournament(), value)
  }
}

The problem is that whenever I doSmth I'm getting an error like Parameter value [Tournament(id=3164)] did not match expected type [org.my.SpecialTournament].

I figured out that it could happen because Tournament already exists in the session (it was loaded with Match) and Hibernate tries to reuse it while constructing the SpecialEvent entity.

I managed to "fix it" by adjusting repository method to accept SpecialTournament and by using var tournament = entityManager.find(SpecialTournament.class, match.getTournament().getId()), but I feel that there's something wrong with this approach. Also, I'm getting a warning from Hibernate: Narrowing proxy to class org.my.SpecialEntity - this operation breaks == so I believe there should be a better solution. Especially in this case - when in large sessions it could be extremely hard to identify the way where to apply this "hack".

Thank you!

Update: the repository method doesn't need to receive Tournament as an argument for this error to arrive. The stack trace shows that the error comes from the deep inside of Hibernate when it tries to create the SpecialEvent entity. For some reason, it tries to assign instance of Tournament to SpecialEvent.tournament.

Here is the stack trace: https://pastebin.com/SKWfWmK9

Update 2: also, I've tried to override the .equals() but it seems that Hibernate proxy doesn't care whatever I do to my entities.

CodePudding user response:

I would say it is kind of expected behavior:

@Entity
class Match {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private int id;

  @ManyToOne(fetch = FetchType.LAZY)
  private Tournament tournament;

  private int value;
}

FetchType.LAZY over private Tournament tournament causes HBN to create a proxy, which stores information only about base entity type (Tournament) and it's identifier. In order to turn that proxy into full-functional entity you need may call Hibernate#unproxy, however that won't work outside transaction.

here:

interface SpecialEventRepository extends JpaRepository<SpecialEntity, Integer> {
  @Query("FROM SpecialEvent WHERE tournament=:tournament AND value=:value ORDER BY importantField")
  Iterable<SpecialEvent> findForMatchField(Tournament tournament, int value)
}

you are getting error did not match expected type because tournament field of SpecialEvent has type SpecialTournament - no magic here. The quick and dirty solution is to declare findForMatchField method as:

interface SpecialEventRepository extends JpaRepository<SpecialEntity, Integer> {
  @Query("FROM SpecialEvent WHERE tournament.id=:tournamentId AND value=:value ORDER BY importantField")
  Iterable<SpecialEvent> findForMatchField(Integer tournamentId, int value)
}

CodePudding user response:

So, after digging half of the internet, I discovered that sometimes hibernate, surprisingly, is not smart enough and it needs to get additional advice.

I am currently using Hibernate 5, so this post gave me the perfect solution: https://stackoverflow.com/a/217848/1246437

Of course, I'm using not raw Object as the property type, rather the concrete type of my domain, but it still seems to work as expected.

There are a few caveats:

  1. @AnyMetaDef is deprecated in Hibernate 5 and is removed in Hibernate 6.
  2. It should be possible to apply @AnyMetaDef at the package level (by providing package-info.java)
  3. Hibernate 6 should be way smarter and require less ceremonies. At least, according to the doc. Search for "Example 213. @Any mapping usage".
  • Related