org.hibernate.WrongClassException on saving an entity via Hibernate
Asked Answered
C

2

3

In this question I am working with Hibernate 4.3.4.Final and Spring ORM 4.1.2.RELEASE.

I have an User class, that holds a Set of CardInstances like this:

@Entity
@Table
public class User implements UserDetails {

    protected List<CardInstance> cards;

    @ManyToMany
    public List<CardInstance> getCards() {
        return cards;
    }

    // setter and other members/methods omitted 
}

@Table
@Entity
@Inheritance
@DiscriminatorColumn(name = "card_type", discriminatorType = DiscriminatorType.STRING)
public abstract class CardInstance<T extends Card> {

    private T card;

    @ManyToOne
    public T getCard() {
        return card;
    }
}

@Table
@Entity
@Inheritance
@DiscriminatorOptions(force = true)
@DiscriminatorColumn(name = "card_type", discriminatorType = DiscriminatorType.STRING)
public abstract class Card {
    // nothing interesting here
}

I have several types of cards, each extending the Card base class and the CardInstance base class respectivly like this:

@Entity
@DiscriminatorValue("unit")
public class UnitCardInstance extends CardInstance<UnitCard> {
    // all types of CardInstances extend only the CardInstance<T> class
}

@Entity
@DiscriminatorValue("leader")
public class LeaderCardInstance extends CardInstance<LeaderCard> {

}

@Entity
@DiscriminatorValue("unit")
public class UnitCard extends Card {
}

@Entity
@DiscriminatorValue("leader")
public class LeaderCard extends AbilityCard {
}

@Entity
@DiscriminatorValue("hero")
public class HeroCard extends UnitCard {
    // card classes (you could call them the definitions of cards) can
    // extend other types of cards, not only the base class
}

@Entity
@DiscriminatorValue("ability")
public class AbilityCard extends Card {
}

If I add a UnitCardInstance or a HeroCardInstance to the cards collection and save the entity everything works fine. But if I add a AbilityCardInstance to the collection and save the entity it fails with a org.hibernate.WrongClassException. I added the exact exception + message at the bottom of the post.

I read through some questions, and lazy loading seems to be a problem while working with collections of a base class, so here is how I load the User entity before adding the card and saving it:

User user = this.entityManager.createQuery("FROM User u " +
                "WHERE u.id = ?1", User.class)
                .setParameter(1, id)
                .getSingleResult();

        Hibernate.initialize(user.getCards());

        return user;

The database entries for "cards"
The database entries for "cards"
The database entries for "cardinstances"
The database entries for "cardinstances"


org.hibernate.WrongClassException: Object [id=1] was not of the specified subclass [org.gwentonline.model.cards.UnitCard] : Discriminator: leader

Thanks in advance for any clues how to fix this problem. If you need additional information I will gladly update my question!

Castilian answered 6/7, 2015 at 22:51 Comment(7)
It seems that your User with id=1 has a UnitCardInstance with id=1 which holds a reference to a Card with discriminator=leader instead of discriminator=unit (because UnitCardInstances can only reference UnitCards). Check your card_type table for id=1 to see what card_id it refers to and then check the discriminator value for that row.Plantagenet
That does not seem to be the problem, on database side everything seems to be in order. Both discriminator fields have the value "leader", the problem has to be Hibernate sided...Castilian
In your post, you have this: @DiscriminatorValue("unit") public class UnitCardInstance and @DiscriminatorValue("unit") public class UnitCard. There is no reference to a discriminator value of leader in your post. This is why I mentioned that there is a mismatch between the JPA configuration and what Hibernate found in the database.Plantagenet
I don't know your database content and/or your actual code so I am only going by your post and the error message you have provided. The error says that the JPA configuration requires a row to be converted into a UnitCard instance, which requires a discriminator value of unit according to your post; but the actual database row has the value leader so Hibernate cannot make the conversion. This is why I suggested that you check the data as well.Plantagenet
If you have already checked the data and found that the discriminator value is indeed leader and is expected, there may be some JPA misconfiguration somewhere. Do you have LeaderCard and LeaderCardInstance classes? Are they correctly annotated with the discriminator value leader?Plantagenet
Yes, I checked the data in the database itself. I just updated my question to make things clearer... Should I provide some more code, to make things clearer? If so, what would you like to see?Castilian
The data and the updated code seem to match. Can't think of anything else, other than a potential bug. You will have to create a small sample app with Gradle as the build tool that reproduces the error and post it on the Hibernate JIRA if you wish the Hibernate team to investigate.Plantagenet
P
7

According to the first paragraph of the JavaDocs for @ManyToOne:

It is not normally necessary to specify the target entity explicitly since it can usually be inferred from the type of the object being referenced.

However, in this case, @ManyToOne is on a field whose type is generic and generic type information gets erased at the type of compilation. Therefore, when deserializing, Hibernate does not know the exact type of the field.

The fix is to add targetEntity=Card.class to @ManyToOne. Since Card is abstract and has @Inheritance and @DiscriminatorColumn annotations, this forces Hibernate to resolve the actual field type by all possible means. It uses the discriminator value of the Card table to do this and generates the correct class instance. Plus, type safety is retained in the Java code.


So, in general, whenever there is the chance of a field's type not being known fully at runtime, use targetEntity with @ManyToOne and @OneToMany.

Plantagenet answered 10/7, 2015 at 14:52 Comment(0)
C
0

I solved the problem.

The root cause lies in this design:

@Table
@Entity
@Inheritance
@DiscriminatorColumn(name = "card_type", discriminatorType = DiscriminatorType.STRING)
public class CardInstance<T extends Card> {  
    protected T card;
}

@Entity
@DiscriminatorValue("leader")
public class LeaderCardInstance extends CardInstance<LeaderCard> {
}

At runtime information about generic types of an class are not present in java. Refer to this question for further information: Java generics - type erasure - when and what happens

This means hibernate has no way of determining the actual type of the CardInstance class.


The solution to this is simply getting rid of the generic type and all extending (implementing) classes and just use one class like this:

@Table
@Entity
@Inheritance
@DiscriminatorColumn(name = "card_type", discriminatorType = DiscriminatorType.STRING)
public class CardInstance {
    Card card;
}

This is possible (and by the way the better design) because the member card carries all the information about the card type.


I hope this helps folk if they run into the same problem.

Castilian answered 7/7, 2015 at 10:33 Comment(3)
This reminds me of what the JavaDocs for @ManyToOne say. If the field annotated with @ManyToOne is of a generic type, the targetEntity parameter must be set. You could have annotated your getCard method with @ManyToOne(targetEntity=Card.class) and kept your version with the generic type parameter.Plantagenet
I have also logged a ticket with the Hibernate team to understand the root cause of the behaviour as I have also ran into this in the past and different Hibernate versions behave differently.Plantagenet
Very nice solution! If you create a short answer, I will accept it, since it seems to be the best way to goCastilian

© 2022 - 2024 — McMap. All rights reserved.