diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index 20bdec761f7f..20f638618777 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -25,6 +25,7 @@ import jakarta.persistence.metamodel.Metamodel; import org.hibernate.CacheMode; import org.hibernate.ConnectionAcquisitionMode; +import org.hibernate.FetchNotFoundException; import org.hibernate.Filter; import org.hibernate.FlushMode; import org.hibernate.HibernateException; @@ -2332,7 +2333,14 @@ public T find(Class entityClass, Object primaryKey, LockModeType lockMode return loadAccess.load( primaryKey ); } - catch ( EntityNotFoundException ignored ) { + catch (EntityNotFoundException entityNotFoundException) { + /* + This may happen if the entity has an associations mapped with @NotFound(action = NotFoundAction.EXCEPTION) + and this associated entity is not found. + */ + if ( entityNotFoundException instanceof FetchNotFoundException ) { + throw entityNotFoundException; + } // DefaultLoadEventListener#returnNarrowedProxy() may throw ENFE (see HHH-7861 for details), // which find() should not throw. Find() should return null if the entity was not found. if ( log.isDebugEnabled() ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionTest.java new file mode 100644 index 000000000000..a25d78665b5f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/exception/NotFoundExceptionTest.java @@ -0,0 +1,240 @@ +package org.hibernate.orm.test.notfound.exception; + +import java.util.Set; + +import org.hibernate.FetchNotFoundException; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DomainModel( + annotatedClasses = { + NotFoundExceptionTest.ChessGame.class, + NotFoundExceptionTest.ChessPlayer.class + } +) +@SessionFactory +@TestForIssue(jiraKey = "HHH-15229") +public class NotFoundExceptionTest { + + @AfterEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> + session.createNativeMutationQuery( "delete from CHESS_GAME" ).executeUpdate() + ); + } + + @Test + public void testNotFoundIgnore(SessionFactoryScope scope) { + scope.inTransaction( + session -> + session.createNativeMutationQuery( + "INSERT INTO CHESS_GAME(id, player_black_id) VALUES (1, 1)" ) + .executeUpdate() + + ); + + scope.inTransaction( + session -> { + ChessGame game = session.find( ChessGame.class, 1L ); + assertNotNull( game, "Returned entity shouldn't be null" ); + assertNull( + game.getPlayerBlack(), + "Broken foreign key reference with NotFoundAction.IGNORE should return null" + ); + assertNull( game.getPlayerWhite() ); + } + ); + + scope.inTransaction( + session -> { + ChessGame chessGame = session.getReference( ChessGame.class, 1L ); + assertNotNull( chessGame ); + assertNull( + chessGame.getPlayerBlack(), + "Broken foreign key reference with NotFoundAction.IGNORE should return null" + ); + assertNull( chessGame.getPlayerWhite() ); + } + ); + } + + @Test + public void testNotFoundIgnoreAndNotFoundException(SessionFactoryScope scope) { + scope.inTransaction( + session -> + session.createNativeMutationQuery( + "INSERT INTO CHESS_GAME(id, player_white_id, player_black_id) VALUES (1, 1, 2)" ) + .executeUpdate() + ); + + Assertions.assertThrows( + FetchNotFoundException.class, () -> scope.inTransaction( + session -> + session.find( ChessGame.class, 1L ) + ) + ); + + Assertions.assertThrows( + FetchNotFoundException.class, () -> scope.inTransaction( + session -> { + ChessGame chessGame = session.getReference( ChessGame.class, 1L ); + assertNotNull( chessGame ); + // this triggers ChessGame initialization, and because playerBlack cannot be found the FetchNotFoundException is thown + assertNotNull( chessGame.getPlayerBlack() ); + } + ) + ); + } + + @Test + public void testNotFoundException(SessionFactoryScope scope) { + scope.inTransaction( + session -> + session.createNativeMutationQuery( + "INSERT INTO CHESS_GAME(id, player_white_id) VALUES (1, 1)" ) + .executeUpdate() + ); + + Assertions.assertThrows( + FetchNotFoundException.class, () -> scope.inTransaction( + session -> + session.find( ChessGame.class, 1L ) + ) + ); + + Assertions.assertThrows( + FetchNotFoundException.class, () -> scope.inTransaction( + session -> { + ChessGame chessGame = session.getReference( ChessGame.class, 1L ); + assertNotNull( chessGame ); + // this triggers ChessGame initialization, and because playerBlack cannot be found the FetchNotFoundException is thown + assertNotNull( chessGame.getPlayerBlack() ); + } + ) + ); + } + + @Entity(name = "ChessPlayer") + @Table(name = "CHESS_PLAYER") + public static class ChessPlayer { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String name; + + @OneToMany(mappedBy = "playerWhite") + private Set gamesWhite; + + @OneToMany(mappedBy = "playerBlack") + private Set gamesBlack; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getGamesWhite() { + return gamesWhite; + } + + public void setGamesWhite(Set gamesWhite) { + this.gamesWhite = gamesWhite; + } + + public Set getGamesBlack() { + return gamesBlack; + } + + public void setGamesBlack(Set gamesBlack) { + this.gamesBlack = gamesBlack; + } + } + + @Entity(name = "ChessGame") + @Table(name = "CHESS_GAME") + public static class ChessGame { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "player_white_id") + @NotFound(action = NotFoundAction.EXCEPTION) + private ChessPlayer playerWhite; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "player_black_id") + @NotFound(action = NotFoundAction.IGNORE) + private ChessPlayer playerBlack; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ChessPlayer getPlayerWhite() { + return playerWhite; + } + + public void setPlayerWhite(ChessPlayer playerWhite) { + this.playerWhite = playerWhite; + } + + public ChessPlayer getPlayerBlack() { + return playerBlack; + } + + public void setPlayerBlack(ChessPlayer playerBlack) { + this.playerBlack = playerBlack; + } + } +}