hibernate's [not-found="ignore"] is buggy as hell
by Jerome Kehrli
Posted on Wednesday Jan 27, 2010 at 07:52PM in Java
I'm working on a java application which makes an extensive usage of hibernate's relation mapping system. The later offers several ways to define association mapping. We mostly use many-to-one
relation declarations. The problem comes from the database. It's a pre-relational, pre-transactional, legacy database running on a prehistorical IBM zSeries host. The data on this database is very often dumb or corrupted. The lack of a proper referential integrity support and the foolish design make us end up quite often following non-existent relations.
Happily, hibernate provides a semantic which allow the application not to bother when a relation is missing, just as the legacy app does. This semantic is the not-found="ignore"
parameter on the relation definition.
However, the usage of this semantic resumes to open very wide the doors to oblivion.
Hibernate's first level cache is very nifty for what is related to relation management. For instance, it allows great pre-fetching strategies. Should you load a hundred rows from some entity which holds references to another entity, all you have to do is define the relation with lazy="proxy"
(which is the default), and when you follow one of these relations all the other (for the rest of the hundred rows) are loaded in more or less one shot.
This works great and is very efficient.
Problems arise when the same relation to the very same entity (db table) is mapped using different definitions. We will now illustrate this issue.
Let's imagine the following situation :
Entity C
| represents the target of both relations |
Entity A
|
holds a relation to Entity C. The relation is defined with not-found="exception" as in the following hibernate mapping file snippet:<many-to-one name="associatedC" column="ID_OF_C" class="very.nifty.EntityC" lazy="proxy" not-found="exception"/>
|
Entity B
|
holds a relation to Entity C. The relation is defined with not-found="ignore" as in the following hibernate mapping file snippet:<many-to-one name="associatedC" column="ID_OF_C" class="very.nifty.EntityC" not-found="ignore"/>
|
This means that both Entity A
and Entity B
hold a relation to Entity C
. All instances of Entity A
should mandatorily have a counterpart target in Entity C
. If none is found following the relation, hibernate should throw an exception.
On the contrary when the target Entity C
corresponding to Entity B
is not found, hibernate should silently set null on the relation holder and not complain in any way.
We will now see that things can behave quite differently than what we can expect.
Case 1 : only Entity A
instances are loaded.
When several instances of Entity A
are loaded which reference the same instance of Entity C
, everything works fine.
We have asked hibernate to manage the relation through a proxy. Hence, instances of Entity C
associated to Entity A
are not eagerly fetched. When an instance of Entity A
is build, hibernate associates to it a proxy on the corresponding Entity C
instance without actually fetching it from the DB. The proxy is obviously not null.
Only when the program actually tries to call a method on this proxy, hibernate puts the call on hold and tries to load the data from the database. At that stage, if the target Entity C
instance does not exist in the database, hibernate does not have much of a choice but triggers an ObjectNotFoundException
. Due to the proxy already associated to the relation in Entity A
, returning null is not an option any more (obviously). So throwing an exception is pretty much anything hibernate can do.
This works as expected and is fine.
Case 2 : only Entity B
instances are loaded.
The relation from Entity B
to Entity C
is defined as not-null="ignore"
. In order to support this, hibernate cannot use any proxing mechanism. It needs to check immediately the existence of the corresponding row in the target table in order to be able to set null on the relation holder within the source entity should the target row not exist. This prevents it from associating a proxy to the relation holder and fetching the row lazily.
You can perfectly define a relation set as not-null="ignore"
with lazy="proxy"
but this is just going to be ignored by hibernate. The not-null="ignore"
primes and the relation is not managed with a proxy.
So what happens if one or several instance of Entity B
are loaded which reference the same instance of Entity C
? Each and every time an Entity B
is loaded, hibernate will try to find the matching row in Entity C
in the DB. If it doesn't find it, it will set null to the relation holder in Entity B
and return it to the calling program.
You might think "great, so this not-null="ignore"
actually works" and you wouldn't be wrong. But there's a trick :
The problem is that the caching mechanisms within hibernate are quite stupid. They don't cache negative hits. Imagine you load a hundred rows from Entity B
which all reference the very same row of Entity C
which does not exist. When the first row is loaded, hibernate checks whether the target row in Entity C
exist. It doesn't and hence hibernate sets null on the first entity relation holder. The the second row is loaded. At that stage hibernate does not remember it already tried to lookup that very same instance of Entity C
and tries again, And again for the third row, and so on. This is quite stupid as there are very few chance that a row suddenly and magically appear between to SQL calls within the same transaction. This might depend of the transaction isolation level of course, but still, chances are very little, especially on very short time periods.
This is what I mean when stating that hibernate does not cache negative hits. It does not remember that it has already look for some data in a table within a single transaction and that it didn't find it and it tries again and again and again...Stupid.
However this doesn't affect the functional behaviour of the application, only the performance, and this is not the problem on which I want to focus here. So let's be happy with it. And as a sidenote it's no big deal to fix it on your own in hibernate's code. One can tweak the first level caching system or the 2nd level cache (a very little bit harder) to cache negative hits as well by using, for instance, a dummy static instance used to identify negative hits.
We'll know look into a third non-problematic case.
Case 3 : instances of both Entity A
and Entity B
are loaded which reference the same Entity C
instance.Entity B
(with not-found="ignore"
) loaded first
In this case we first load an instance of Entity B
, where the relation is defined with not-found="ignore"
, and then an instance of Entity A
where the relation is defined with not-found="exception"
is loaded. Both these two instances reference the very same instance of Entity C
.
This goes fine. When the instance of Entity B
is loaded, hibernate searches the database for the target instance of Entity C
. It doesn't exist so hibernate sets null on the relation holder. When the Entity A
is loaded, a proxy for Entity C
is created and associated to it. If one follows the relation on the java object, hibernate doesn't find the corresponding row and throws an exception, just as expected.
When further instances of Entity A
or Entity B
are loaded, hibernate still finds a way to behave as expected and associates null values or throws exception when the proxy is accessed, depending on the way the relation has been defined.
We will now look at the buggy case:
Case 4 : instances of both Entity A
and Entity B
are loaded which reference the same Entity C
instance.Entity A
(with not-found="exception"
) loaded first AND NOT USED.
Let's imagine now the following scenario :
We first load an instance of Entity A
, where the relation is defined with not-found="exception"
, and then an instance of Entity B
where the relation is defined with not-found="ignore"
. Again, both these two instances reference the very same instance of Entity C
.
At the time the instance of Entity A
is loaded, the relation is not followed and thus the proxy is not resolved. Hibernate doesn't know at that stage that the row does not exist.
The problem arises when the instance of Entity B
is loaded. Just as usual, as we have seen before, hibernate should go to the database looking for the target row in order to set null on the relation holder if it doesn't exist. But for whatever reason, in this very specific case it does not. It gets screwed by the first level cache which keeps a reference on the proxy created for Entity A
.
Instead of looking for the row in the DB and setting null on the relation holder, it finds the proxy in the first level cache and sets it on the relation holder, despite the relation being defined with not-found="ignore"
.
The corresponding instance of Entity C
doesn't exist, but instead of having null on the relation holder, the Entity B
instance has the proxy, which is not null at all !!!
When one tries to follow the relation from Entity B
, hibernate doesn't have much of a choice but throws an exception, contradicting the not-found="ignore"
definition.
This happens just always in this very situation. When two objects reference the same third object with different not-found
behaviour, and the one which uses not-found="exception"
is loaded first, the second gets the same proxy associated to its relation holder and gets screwed when the program tries to follow the relation. The program tests for nullity and gets the proxy, hence not null, and when it tries to call anything on the proxy, it gets an exception. Screwed.
The problem comes from the fact that the entity loading mechanism in hibernate doesn't keep track of the details of the relation definition when it looks in the first level cache for a relation target. If a proxy exist before any relation defined as not-found="ignore"
is loaded, the relations will get the proxy from the first level cache, no matter whether the target row exists or not.
The funny thing here is that if any relation defined as not-found="ignore"
has been loaded first, the problem is not triggered. No matter how the subsequent relations are defined, hibernate finds a way to behave correctly and doesn't reuse any proxy for relations defined with not-found="ignore"
. Everything works correctly.
But in the other case, again, is the proxy is created first before any other lookup for the row in the DB, every relation defined with not-found="ignore"
will be screwed.
So how does one workaround this ?
Well there's no silver bullet here.
The very only solution for this is to make sure every relation which might target a table with missing rows or corrupted references must declare the relation with not-found="ignore"
and check programmatically within the Business Object whether this is a problem or not. If it is, the business object throws an exception on his own.
I'm pretty sure this problem has not been experienced by many hibernate users so far as I haven't been able to find any discussion about it on the various hibernate forums or expert exchange or so.
This makes sense to me as I assume most hibernate users uses it on a modern Database with well defined referential integrity constraints and the whole valuable shebang modern databases can bring to a project.
But I'm working on a very big project where we have mapped hundred of db tables almost directly in our business object model using hibernate, each of these objects having a lot of relations together and most of them suffering from corrupter references or simply missing rows in the target table.
For us, this problem has been difficult to spot and we are still looking for a better workaround that the one describe above to handle it. If anyone has one, I'm a buyer.
As a sidenote, I cannot provide you here with any test case or program related to this issue as I am experiencing it at work and I have non-divulgation clause linking myself to my employer. And I don't really have the required time to reproduce it and a specific test case for the moment.
However I might sooner or later open a case for this in the hibernate bug tracking system, so if this problem interests you, you might want to keep looking there sometimes.