Hibernate’s @NotFound Annotation – How to use it and a better alternative
Take your skills to the next level!
The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.
Some table models don’t enforce their foreign key references by a foreign key constraint. This is a bad practice that often leads to foreign key references that point to non-existing records. When Hibernate tries to resolve such a broken reference, it throws an EntityNotFoundException. You should, therefore, define a foreign key constraint for every foreign key reference.
But after someone decided against using foreign key constraints and deployed the application to production, that decision is often hard to reverse. And that puts you in the position where you need to build a persistence layer that can handle associations that reference a non-existing record.
By default, Hibernate throws an exception if it tries to resolve a broken foreign key reference. The best way to fix this is, of course, to clean up your database and fix your foreign key references. But if that’s not an option, you need to decide if you:
- want to handle an EntityNotFoundException every time you call the getter method of a potentially broken association or
- use Hibernate’s @NotFound annotation to tell Hibernate to fetch a potentially broken association and ignore it or throw a FetchNotFoundException when instantiating your entity object.
Hibernate’s @NotFound annotation
Annotating an association with Hibernate’s proprietary @NotFound annotation has 3 effects:
- Hibernate assumes that the table model doesn’t define a foreign key constraint for that association and doesn’t generate one if it generates the table model.
- You define if Hibernate shall ignore broken foreign key references or throw an exception.
- Hibernate fetches the association eagerly, even if you set its FetchType to LAZY.
I will get into more details about Hibernate’s enforced eager fetching in the next section. First, let’s take a closer look at the @NotFound annotation and the 2 supported NotFoundActions.
NotFoundAction.EXCEPTION
You can define the NotFoundAction.EXCEPTION by annotating the attribute that maps your association with @NotFound and setting the action attribute to EXCEPTION or keeping it empty. This tells Hibernate to throw a FetchNotFoundException if it can’t resolve the foreign key reference.
@Entity
public class ChessGame {
@ManyToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.EXCEPTION)
private ChessPlayer playerBlack;
...
}
This behavior might seem very similar to the one you get without annotating your association with @NotFound. But there are 2 differences:
- Hibernate throws a FetchNotFoundException instead of an EntityNotFoundException.
- Hibernate ignores the configured FetchType and tries to fetch the association eagerly to validate the foreign key reference. Due to that, Hibernate throws the FetchNotFoundException when it instantiates the entity object and not when you use the association for the first time. This makes the FetchNotFoundException a little easier to handle.
You can see all of this in the log output when I use the mapping in a test case that fetches a ChessGame entity with a broken foreign key reference.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
ChessGame game = em.find(ChessGame.class, 10L);
log.info(game.getPlayerWhite() + " - " + game.getPlayerBlack());
em.getTransaction().commit();
em.close();
Hibernate joins and selects the playerBlack association in the query that fetches the ChessGame entity and throws a FetchNotFoundException.
17:04:20,702 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.chessTournament_id,c1_0.date,c1_0.playerBlack_id,p1_0.id,p1_0.birthDate,p1_0.firstName,p1_0.lastName,p1_0.version,c1_0.playerWhite_id,c1_0.round,c1_0.version from ChessGame c1_0 left join ChessPlayer p1_0 on p1_0.id=c1_0.playerBlack_id where c1_0.id=?
17:04:20,712 ERROR [com.thorben.janssen.sample.TestSample] - org.hibernate.FetchNotFoundException: Entity `com.thorben.janssen.sample.model.ChessPlayer` with identifier value `100` does not exist
NotFoundAction.IGNORE
Setting the NotFoundAction to IGNORE enables you to handle the broken foreign key reference in your business code. Instead of throwing an exception if it can’t resolve the foreign key reference, Hibernate sets the association attribute to null. Due to that, you can no longer distinguish if an association wasn’t set or if it’s referencing a record that no longer exists. You need to decide for your application, if you want to handle these 2 cases differently. If that’s the case, you can’t use NotFoundAction.IGNORE.
Like in the previous example, you need to annotate the attribute that maps the association with Hibernate’s @NotFound annotation. But this time, you also need to set the action to NotFoundAction.IGNORE.
@Entity
public class ChessGame {
@ManyToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.IGNORE)
private ChessPlayer playerBlack;
...
}
When you then execute the same test case as in the previous section, Hibernate no longer throws an exception and initializes the playerBlack attribute with null instead.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
ChessGame game = em.find(ChessGame.class, 10L);
log.info(game.getPlayerWhite() + " - " + game.getPlayerBlack());
em.getTransaction().commit();
em.close();
17:23:24,203 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.chessTournament_id,c1_0.date,c1_0.playerBlack_id,p1_0.id,p1_0.birthDate,p1_0.firstName,p1_0.lastName,p1_0.version,c1_0.playerWhite_id,c1_0.round,c1_0.version from ChessGame c1_0 left join ChessPlayer p1_0 on p1_0.id=c1_0.playerBlack_id where c1_0.id=?
17:23:24,223 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.birthDate,c1_0.firstName,c1_0.lastName,c1_0.version from ChessPlayer c1_0 where c1_0.id=?
17:23:24,237 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=4, firstName=Fabiano, lastName=Caruana, birthDate=1992-07-30, version=0] - null
No lazy fetching with @NotFound
I mentioned earlier that annotating an association with @NotFound changes the fetching behavior to FetchType.EAGER. That’s even the case if you explicitly set FetchType.LAZY in your association mapping, like I did in the previous examples.
@Entity
public class ChessGame {
@ManyToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.IGNORE)
private ChessPlayer playerBlack;
...
}
The reason for that is simple. Hibernate needs to use FetchType.EAGER to ensure that it only initializes the association attribute if it references an existing entity object.
If you don’t annotate your association attribute with @NotFound, Hibernate expects that a foreign key constraint validates the foreign key reference. Due to that, it only needs to check if a foreign key reference is set. If that’s the case, it knows that it will be able to resolve the reference and initializes the entity attribute with a proxy object. When you use that proxy for the first time, Hibernate will execute an SQL statement to resolve the foreign key reference.
If you annotate the association attribute with @NotFound, Hibernate can no longer trust the foreign key reference. Without a foreign key constraint, the reference might be broken. Hibernate, therefore, can’t simply use the foreign key value to instantiate a proxy object. It first needs to check if the reference is valid. Otherwise, it would need to set the association attribute to null.
Performing this additional query can create performance problems. But there is only a minimal performance difference between checking the foreign key reference and trying to fetch the associated entity. Due to that, the Hibernate team decided to use eager fetching for all associations annotated with @NotFound.
An often better alternative
The enforced eager fetching of Hibernate’s @NotFound mapping can cause performance problems. Even though the implementation might be more complex, it’s often better not to annotate your association with @NotFound and handle the broken foreign key reference in your business code.
Hibernate then instantiates a proxy object if the foreign key reference is set and tries to resolve it when using the proxy object for the first time.
17:35:52,212 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.chessTournament_id,c1_0.date,c1_0.playerBlack_id,c1_0.playerWhite_id,c1_0.round,c1_0.version from ChessGame c1_0 where c1_0.id=?
17:35:52,241 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.birthDate,c1_0.firstName,c1_0.lastName,c1_0.version from ChessPlayer c1_0 where c1_0.id=?
17:35:52,255 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.birthDate,c1_0.firstName,c1_0.lastName,c1_0.version from ChessPlayer c1_0 where c1_0.id=?
17:35:52,260 ERROR [com.thorben.janssen.sample.TestSample] - jakarta.persistence.EntityNotFoundException: Unable to find com.thorben.janssen.sample.model.ChessPlayer with id 100
If the foreign key reference is broken, Hibernate throws an EntityNotFoundException, which you need to handle in your business code. The obvious downside of this approach is that you need to handle this exception in various places in your business code.
You need to decide if you’re willing to do that to get the performance benefits of FetchType.LAZY or, if you prefer the ease of use provided by Hibernate’s @NotFound mapping.
Deactivate the foreign key constraint
If you decide to handle the broken foreign key references in your business code and use Hibernate to generate your table model, you need to tell Hibernate not to generate the foreign key constraint.
ATTENTION: You should only use this if you’re working on a legacy application that doesn’t use foreign key constraints. If you still have the choice, you should always use a foreign key constraint to enforce your foreign key references!
You can deactivate the generation of foreign key constraints by annotating your association with a @JoinColumn annotation and setting the foreignKey attribute to @ForeignKey(ConstraintMode.NO_CONSTRAINT). This annotation only affects Hibernate’s generation of the table model and has no effect at runtime.
@Entity
public class ChessGame {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private ChessPlayer playerBlack;
...
}
Conclusion
Your table model should validate all foreign key references by a foreign key constraint. This ensures that new foreign key references can only reference existing records and that you can’t remove a record that’s still referenced.
Unfortunately, some architects and development teams decide to avoid foreign key constraints. Sooner or later, these databases contain broken foreign key references, which you need to handle in your entity mappings or business code.
If you want to handle them in your entity mappings, you can annotate an association with @NotFound. That tells Hibernate not to expect or generate any foreign key constraint. Hibernate then fetches the association eagerly to check the validity of the foreign key reference. The handling of a broken reference depends on your NotFoundAction. Hibernate can either ignore it and initialize the attribute with null or throw an EntityFetchException.
If you prefer to handle the broken foreign key references in your business code, you can annotate your association attribute with @JoinColumn and define the ConstraintMode.NO_CONSTRAINT. Hibernate then doesn’t generate a foreign key constraint when generating the table model. At runtime, it doesn’t check the foreign key reference until the generated proxy object tries to resolve it.