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!

  • 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 `UnitCardInstance`s can only reference `UnitCard`s). 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. – manish Jul 07 '15 at 02:42
  • 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... – Julian Neuberger Jul 07 '15 at 08:05
  • 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. – manish Jul 07 '15 at 08:34
  • 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. – manish Jul 07 '15 at 08:44
  • 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`? – manish Jul 07 '15 at 08:46
  • 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? – Julian Neuberger Jul 07 '15 at 09:05
  • 1
    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. – manish Jul 07 '15 at 09:17
  • Thanks for your time and help! – Julian Neuberger Jul 07 '15 at 09:18

2 Answers2

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.

manish
  • 17,766
  • 4
  • 59
  • 85
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.

Community
  • 1
  • 1
  • 1
    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. – manish Jul 09 '15 at 13:21
  • I have also [logged a ticket](https://hibernate.atlassian.net/browse/HHH-9909) 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. – manish Jul 09 '15 at 13:56
  • Very nice solution! If you create a short answer, I will accept it, since it seems to be the best way to go – Julian Neuberger Jul 10 '15 at 14:26