diff --git a/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java b/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java index 664a2044a970..c85f165381ee 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java @@ -175,8 +175,8 @@ public Serializable disassemble( ); if ( id == null ) { throw new AssertionFailure( - "cannot cache a reference to an object with a null id: " + - getAssociatedEntityName() + "cannot cache a reference to an object with a null id: " + + getAssociatedEntityName() ); } return getIdentifierType( session ).disassemble( id, session, owner ); @@ -207,7 +207,7 @@ public Object assemble( Serializable oid, SharedSessionContractImplementor session, Object owner) throws HibernateException { - + //TODO: currently broken for unique-key references (does not detect // change to unique key property of the associated object) @@ -245,12 +245,7 @@ public boolean isDirty( Object old, Object current, SharedSessionContractImplementor session) throws HibernateException { - if ( isSame( old, current ) ) { - return false; - } - Object oldid = getIdentifier( old, session ); - Object newid = getIdentifier( current, session ); - return getIdentifierType( session ).isDirty( oldid, newid, session ); + return isDirtyManyToOne(old, current, null, session); } @Override @@ -259,18 +254,29 @@ public boolean isDirty( Object current, boolean[] checkable, SharedSessionContractImplementor session) throws HibernateException { - if ( isAlwaysDirtyChecked() ) { - return isDirty( old, current, session ); + return isDirtyManyToOne(old, current, isAlwaysDirtyChecked()? null:checkable, session); + } + + private boolean isDirtyManyToOne(Object old, Object current, boolean[] checkable, SharedSessionContractImplementor session) { + if (isSame(old, current)) { + return false; } - else { - if ( isSame( old, current ) ) { - return false; - } - Object oldid = getIdentifier( old, session ); - Object newid = getIdentifier( current, session ); - return getIdentifierType( session ).isDirty( oldid, newid, checkable, session ); + + if (old == null || current == null) { + return true; + } + + if (ForeignKeys.isTransient(getAssociatedEntityName(), current, Boolean.FALSE, session)) { + return true; } - + + Object oldid = getIdentifier(old, session); + Object newid = getIdentifier(current, session); + Type identifierType = getIdentifierType(session); + + return checkable == null + ? identifierType.isDirty(oldid, newid, session) + : identifierType.isDirty(oldid, newid, checkable, session); } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/SessionIsDirtyForNewManyToOneObjectTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/SessionIsDirtyForNewManyToOneObjectTest.java new file mode 100644 index 000000000000..53b15ae17e51 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/SessionIsDirtyForNewManyToOneObjectTest.java @@ -0,0 +1,154 @@ +package org.hibernate.orm.test.type; + +import jakarta.enterprise.event.Reception; +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.annotations.Cascade; +import org.hibernate.annotations.Generated; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.PreUpdateEventListenerVetoTest; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.orm.test.legacy.E; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.BaseSessionFactoryFunctionalTest; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.Before; +import org.junit.jupiter.api.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestForIssue(jiraKey = "HHH-15848") +public class SessionIsDirtyForNewManyToOneObjectTest extends BaseSessionFactoryFunctionalTest { + + private Long parentId; + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { EntityChild.class, EntityChildAssigned.class, EntityParent.class }; + } + + @BeforeEach + public void setUp() { + inTransaction( session -> { + EntityChild child = new EntityChild("InitialChild"); + EntityChildAssigned assigned = new EntityChildAssigned(1L, "InitialChild"); + EntityParent parent = new EntityParent("InitialParent", child, assigned); + + session.persist(child); + session.persist(parent); + session.persist(assigned); + + + session.flush(); + parentId = parent.id; + } ); + } + + @Test + public void SessionIsDirtyShouldNotFailForNewManyToOneObject() + { + inTransaction( session -> { + var parent = getParent(session); + + EntityChild nextChild = new EntityChild("NewManyToOneChild"); + + //parent.Child entity is not cascaded, I want to save it explicitly later + parent.child = nextChild; + + // will throw TransientObjectException + assertDoesNotThrow(()->session.isDirty(), "session.isDirty() call should not fail for transient many-to-one object referenced in session.\""); + assertTrue(session.isDirty(),"session.isDirty() call should return true."); + + session.save(nextChild); + }); + } + + @Test + public void SessionIsDirtyShouldNotFailForNewManyToOneObjectWithAssignedId() + { + inTransaction( session -> { + var parent = getParent(session); + + EntityChildAssigned nextChildAssigned = new EntityChildAssigned(2L, "NewManyToOneChildAssignedId"); + + //parent.ChildAssigned entity is not cascaded, I want to save it explicitly later + parent.childAssigned = nextChildAssigned; + + assertDoesNotThrow(()->session.isDirty(), "session.isDirty() call should not fail for transient many-to-one object referenced in session.\""); + assertTrue(session.isDirty(),"session.isDirty() call should return true."); + session.save(nextChildAssigned); + }); + } + + private EntityParent getParent(Session session){ + return session.get(EntityParent.class, parentId); + } + + @AfterEach + public void cleanUp() { + inTransaction( + session -> { + session.createQuery("delete from EntityParent").executeUpdate(); + session.createQuery("delete from EntityChild").executeUpdate(); + session.createQuery("delete from EntityChildAssigned").executeUpdate(); + } + ); + } + + @Entity(name = "EntityChild") + public static class EntityChild + { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + Long id; + + String name; + + EntityChild(){} + + EntityChild(String name){this.name = name;} + } + + @Entity(name = "EntityParent") + public static class EntityParent + { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + Long id; + + String name; + + @ManyToOne + EntityChild child; + + @ManyToOne + EntityChildAssigned childAssigned; + + EntityParent(){} + + EntityParent(String name, EntityChild child, EntityChildAssigned childAssigned){ + this.name = name; + this.child = child; + this.childAssigned = childAssigned; + } + } + + @Entity(name = "EntityChildAssigned") + public static class EntityChildAssigned + { + @Id + Long id; + + String name; + + EntityChildAssigned(){} + + EntityChildAssigned(Long id, String name){ + this.id = id; + this.name = name; + } + } +}