Sunday, April 03, 2011

Hibernate WrongClassException : discriminator force = 'true'

As the java docs of the org.hibernate.WrongClassException says :

"Thrown when 'Session.load()' selects a row with the given primary key (identifier value) but the row's discriminator value specifies a subclass that is not assignable to the class requested by the user."

This could happen under several different scenarios. Here I would discuss a particular scenario in which this occurred and it was a bit tricky to identify the solution of this problem.

Following Table-Per-Class hierarchy inheritance mapping an application had a scenario which is described below.

Consider the following data model.

Listing 1
class Shop {
Collection deskTops;
Collection lapTops;
..
}

class DeskTop extends AbstractComputer {
...
}

class Laptop extends AbstractComputer {
...
}

Hibernate Mapping :

<class abstract="true" discriminator-value="null" name="AbstractComputer" table="COMPUTER">
<discriminator column="TYPE" type="big_integer"/>
...
...
</class>

<subclass discriminator-value="1" extends="AbstractComputer" name="DeskTop">
...
...
</subclass>

<subclass discriminator-value="2" extends="AbstractComputer" name="Laptop">
...
...
</subclass>

Consider a shop selling X laptops and Y desktops.
Assuming the shop object has a lazy access to the collection of desktops and laptops, the following code with burst with an exception.

Listing 2
1. shop = (Shop) shopDAO.readById(123);
2. Collection deskTops = shop.getDeskTops();
3. deskTops.iterator().hasNext();

The exception would occur at line number 3 and would be similar to :

Listing 3
org.hibernate.WrongClassException: Object with id: 1243 was not of the specified subclass: DeskTop (loaded object was of wrong class class Laptop) 
 at org.hibernate.loader.Loader.instanceAlreadyLoaded(Loader.java:1307)
 at org.hibernate.loader.Loader.getRow(Loader.java:1260)
 at org.hibernate.loader.Loader.getRowFromResultSet(Loader.java:619)
 at org.hibernate.loader.Loader.doQuery(Loader.java:745)
 at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:270)
 at org.hibernate.loader.Loader.loadCollection(Loader.java:2082)
 at org.hibernate.loader.collection.CollectionLoader.initialize(CollectionLoader.java:62)
 at org.hibernate.persister.collection.AbstractCollectionPersister.initialize(AbstractCollectionPersister.java:628)
 at org.hibernate.event.def.DefaultInitializeCollectionEventListener.onInitializeCollection(DefaultInitializeCollectionEventListener.java:83)
 at org.hibernate.impl.SessionImpl.initializeCollection(SessionImpl.java:1853)
 at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:369)
 at org.hibernate.collection.AbstractPersistentCollection.read(AbstractPersistentCollection.java:111)
 at org.hibernate.collection.PersistentBag.iterator(PersistentBag.java:272)


This happens because hibernate does not tries to distinguish between the Desktops and Laptops, while fetching the collection deskTops of a given shop. It gets all the AbstractComputers in the database (as illustrated at listing 2 in line 3) when you try to access the deskTops collection and hence inflate this collection. While doing so, it tries to build a DeskTop objects from the returned rows, these rows could contain a Laptop as well, and hence the WrongClassException.

Examining the fired (at listing 2 in line 3) SQL query below :

SELECT deskTops0_.SHOP AS SHOP242_1_,
  deskTops0_.ID             AS ID1_,
  ....
  ....
FROM PUBLIC.COMPUTER deskTops0_
WHERE deskTops0_.SHOP=?

It shows in this case when the deskTops collection is accessed, the SQL fired would fetch the complete list of all computers for a given shop. It would not differentiate if the given computer is a desktop or not. Hence the wrongClassException would be thrown while inflating the deskTops collection.

In order to solve this problem, I changed the force property of discriminator attribute, from the default value of 'false' to 'true'. hence the modified hbm file for the Computer object looked like :

<class name="AbstractComputer" table="COMPUTER" abstract="true" discriminator-value="null">
<discriminator column="TYPE" type="big_integer" force="true"/>

...
...
</class>

The property force=true will ensure that every query fired for the Computer objects would have discriminator taken into account. Hence while accessing the same deskTop collection as above, now the fired query would be somthing like :

SELECT deskTops0_.SHOP AS SHOP242_1_,
  deskTops0_.ID             AS ID1_,
  ....
  ....
FROM PUBLIC.COMPUTER deskTops0_
WHERE deskTops0_.TYPE=1
AND deskTops0_.SHOP=?

As you can see it included the disciminator, which is 'type' in this case. The value 1 is for the subclass Desktop of Abstract Computers. Hence with this query only the data of type Desktop ois returned and WrongClassException is avoided.

6 comments:

Javin Paul said...

Nice article , you have indeed cover the topic with great details. I have also blogged my experience as How to use Comparator and Comparable in Java . let me know how do you find it.

Rukshan Christy said...

thanks for the article.

well explained and did't waste any of my time to resolve the problem.

keep up the good work

Tedi Zanfolim said...

I am really glad you shared this: I got this error and it seemed impossible to make it work.
For the records, the annotation style is @DiscriminatorOptions(force=true)

Thanks,

Tedi

krk said...

Thanks for the article.

Wanted to add that this exception comes in case of data corruption also.

Using the same example given in the article: if there is an ID=1002 in computers table, and for the same id if a row exists in both desktops table as well as laptops table, then also this exception is thrown, even after using the @DiscriminatorColumn annotation.

Thanks
Ramakanth

dilipmathi said...

Thanks for this great article , it's saved my time

Azizi said...

Thank you, you saved my day :)