Why down-casting throws an exception when using lazy fetch with quarkus panache?

Viewed 463

I have a hierarchy of entities: ATeamEntity and BTeamEntity that extends BaseTeamEntity and UserEntity that contain the BaseTeamEntity. BaseTeamEntity and UserEntity extends BaseEntity and BaseEntity extends PanacheEntityBase.

I've defined in UserEntity to fetch the BaseTeamEntity in a lazy way and I'm trying to perform a downcast like this:

UserEntity user = UserEntity.findById(1L);
BaseTeamEntity team = user.team;
ATeamEntity ateam = (ATeamEntity)team; <--
Long a = ateam.aData;

when I do that I'm getting an exception:

java.lang.ClassCastException: class ***.***.entities.team.BaseTeamEntity$HibernateProxy$YcDcMejE cannot be cast to class ***.***.entities.team.ATeamEntity (***.***.entities.team.BaseTeamEntity$HibernateProxy$YcDcMejE and ***.***.entities.team.ATeamEntity are in unnamed module of loader io.quarkus.bootstrap.classloading.QuarkusClassLoader @4f636cba)

and if I'm trying to downcast directly and not through the UserEntity like this:

BaseTeamEntity team = BaseTeamEntity.findById(1L);
ATeamEntity ateam = (ATeamEntity)team;  <--
Long a = ateam.aData;

it seems to work.

when I'm fetching BaseTeamEntity in UserEntity in an Eager way both ways seems to work.

I don't understand why... Can someone please help me understand why?

Following is the high-level structure of the classes

@MappedSuperclass
public class BaseEntity extends PanacheEntityBase {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long id;

}
@Entity
@Table(name = "users")
public class UserEntity extends BaseEntity {

    @ManyToOne(fetch = FetchType.LAZY)  <------------ throws exception 
    @ManyToOne(fetch = FetchType.EAGER) <--------- working
    @JsonbTransient
    public BaseTeamEntity team;
    
}
@Inheritance(strategy = InheritanceType.JOINED)
@Entity
@Table(name = "teams")
@DiscriminatorColumn(name = "team_type", discriminatorType = DiscriminatorType.STRING)
public abstract class BaseTeamEntity extends BaseEntity {
    
    public String sharedData;

}
@Entity
@Table(name = "teams_A")
@DiscriminatorValue("team_A")
@PrimaryKeyJoinColumn
public class ATeamEntity extends BaseTeamEntity {

    public Long aData;

}
@Entity
@Table(name = "teams_B")
@DiscriminatorValue("team_B")
@PrimaryKeyJoinColumn
public class BTeamEntity extends BaseTeamEntity {

    public Integer bData;

}
2 Answers

When the user's team is fetched lazily, Hibernate will populate the field with a proxy for the team instead of the real team object. That proxy will essentially be an empty instance of a class generated dynamically by Hibernate.

As soon as you will call a method on the team object (e.g. getAData()) Hibernate will initialize the proxy by executing the corresponding select query.

Until that query has been executed, Hibernate does not know the type (ATeamEntity or BTeamEntity), it only knows it's a BaseTeamEntity, so the proxy class extends BaseTeamEntity and the cast fails.

When changing the fetch type to eager, you ask Hibernate to immediately fetch the team and it does not need to create a proxy because it already knows the type.

One of the advantage of the proxy is that you might never need to access the team object, in that case you saving a query.

In addition to the @Guillaume answer I would say to perform a proxy-safe typecast you should do something like this:

UserEntity user = UserEntity.findById(1L);
BaseTeamEntity team = user.team;
ATeamEntity ateam = entityManager.getReference(ATeamEntity.class, team.id);

assertTrue(team != ateam); // !!!

Long a = ateam.aData;

After the getReference() call, team and ateam refer to two different proxy instances, both of which delegate to the same underlying ATeamEntity instance. The second proxy has a different interface, though, and you can check fields like ateam.aData that apply only to this interface. (Note that team.getId() will trigger a SELECT if you map the id property with field access)

Related