diff --git a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java index 89744194adc3..23e30e5fdba7 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java @@ -385,7 +385,7 @@ public boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) { /** * Resolve an identifier or unique key value */ - private Object resolve(Object value, SharedSessionContractImplementor session, Object owner) { + protected Object resolve(Object value, SharedSessionContractImplementor session, Object owner) { if ( value != null && !isNull( owner, session ) ) { if ( isReferenceToPrimaryKey() ) { return resolveIdentifier( value, session, null ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/OneToOneType.java b/hibernate-core/src/main/java/org/hibernate/type/OneToOneType.java index 3e09d267e0bf..d67390a72b09 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/OneToOneType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/OneToOneType.java @@ -106,17 +106,12 @@ public boolean isOneToOne() { @Override public boolean isDirty(Object old, Object current, SharedSessionContractImplementor session) { - if ( isSame( old, current ) ) { - return false; - } - - return getIdentifierType( session ) - .isDirty( getIdentifier( old, session ), getIdentifier( current, session ), session ); + return false; } @Override public boolean isDirty(Object old, Object current, boolean[] checkable, SharedSessionContractImplementor session) { - return isDirty(old, current, session); + return false; } @Override @@ -141,37 +136,25 @@ public boolean useLHSPrimaryKey() { @Override public Serializable disassemble(Object value, SharedSessionContractImplementor session, Object owner) throws HibernateException { - if (value == null) { - return null; - } - - Object id = ForeignKeys.getEntityIdentifierIfNotUnsaved( getAssociatedEntityName(), value, session ); - - if ( id == null ) { - throw new AssertionFailure( - "cannot cache a reference to an object with a null id: " + - getAssociatedEntityName() - ); - } - - return getIdentifierType( session ).disassemble( id, session, owner ); + return null; } @Override public Object assemble(Serializable oid, SharedSessionContractImplementor session, Object owner) throws HibernateException { - - //the owner of the association is not the owner of the id - Object id = getIdentifierType( session ).assemble( oid, session, null ); - - if ( id == null ) { - return null; - } - - return resolveIdentifier( id, session ); + //this should be a call to resolve(), not resolveIdentifier(), + //'cos it might be a property-ref, and we did not cache the + //referenced value + return resolve( session.getContextEntityIdentifier(owner), session, owner ); } + /** + * We don't need to dirty check one-to-one because of how + * assemble/disassemble is implemented and because a one-to-one + * association is never dirty + */ @Override public boolean isAlwaysDirtyChecked() { - return true; + //TODO: this is kinda inconsistent with CollectionType + return false; } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/onetoone/EmbeddedIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/onetoone/EmbeddedIdTest.java new file mode 100644 index 000000000000..247d9a88cab1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/onetoone/EmbeddedIdTest.java @@ -0,0 +1,90 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.annotations.onetoone; + +import java.io.Serializable; + +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.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +@TestForIssue(jiraKey = "HHH-15235") +@DomainModel( + annotatedClasses = { + EmbeddedIdTest.Bar.class, + EmbeddedIdTest.Foo.class + } +) +@SessionFactory +public class EmbeddedIdTest { + + @Test + public void testMerge(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + FooId fooId = new FooId(); + fooId.id = "foo"; + Foo foo = new Foo(); + foo.id = fooId; + Bar bar = new Bar(); + BarId barId = new BarId(); + barId.id = 1l; + bar.id = barId; + foo.bar = bar; + bar.foo = foo; + session.merge( foo ); + session.flush(); + } + ); + } + + @Embeddable + public static class BarId implements Serializable { + private Long id; + } + + @Embeddable + public static class FooId implements Serializable { + private String id; + } + + @Entity(name = "Bar") + @Table(name = "BAR_TABLE") + public static class Bar { + @EmbeddedId + private BarId id; + + private String name; + + @OneToOne(mappedBy = "bar") + private Foo foo; + } + + @Entity(name = "Foo") + @Table(name = "FOO_TABLE") + public static class Foo { + @EmbeddedId + private FooId id; + + private String name; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "bar_id") + private Bar bar; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/MainObject.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/MainObject.hbm.xml new file mode 100644 index 000000000000..7f2e1003be58 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/MainObject.hbm.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + seq_mainobj + + + + + + + + + + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/MainObject.java b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/MainObject.java new file mode 100644 index 000000000000..3a8918efba65 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/MainObject.java @@ -0,0 +1,45 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.onetoone.cache; + + +/** + * @author Wolfgang Voelkl + */ +public class MainObject { + private Long id; + private String description; + private Object2 obj2; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Object2 getObj2() { + return obj2; + } + + public String getDescription() { + return description; + } + + public void setDescription(String string) { + description = string; + } + + public void setObj2(Object2 object2) { + this.obj2 = object2; + if (object2 != null) { + object2.setBelongsToMainObj(this); + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/Object2.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/Object2.hbm.xml new file mode 100644 index 000000000000..cbff05599ddf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/Object2.hbm.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + belongsToMainObj + + + + + + + + + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/Object2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/Object2.java new file mode 100644 index 000000000000..245f5ec93690 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/Object2.java @@ -0,0 +1,44 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.onetoone.cache; + + +/** + * + * @author Wolfgang Voelkl + * + */ +public class Object2 { + private Long id; + private String dummy; + private MainObject belongsToMainObj; + + public Long getId() { + return id; + } + + public void setId(Long l) { + this.id = l; + } + + public String getDummy() { + return dummy; + } + + public void setDummy(String string) { + dummy = string; + } + + public MainObject getBelongsToMainObj() { + return belongsToMainObj; + } + + public void setBelongsToMainObj(MainObject object) { + belongsToMainObj = object; + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/OneToOneCacheTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/OneToOneCacheTest.java index b4d0a65d8e28..edd25fb48b37 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/OneToOneCacheTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/OneToOneCacheTest.java @@ -10,6 +10,7 @@ import org.hibernate.stat.spi.StatisticsImplementor; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.FailureExpected; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; @@ -120,6 +121,7 @@ private List getPersons( } @Test + @FailureExpected( jiraKey = "HHH-14216", reason = "The changes introduces by HHH-14216 have been reverted see https://github.com/hibernate/hibernate-orm/pull/5061 discussion") public void OneToOneCacheByForeignKey(SessionFactoryScope scope) throws Exception { OneToOneTest( PersonByFK.class, DetailsByFK.class, scope ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/OneToOneConstrainedCacheTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/OneToOneConstrainedCacheTest.java new file mode 100644 index 000000000000..af4bb14360d0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/cache/OneToOneConstrainedCacheTest.java @@ -0,0 +1,102 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.onetoone.cache; + +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.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/** + * Simple testcase to illustrate HB-992 + * + * @author Wolfgang Voelkl, michael + */ +@DomainModel( + xmlMappings = { "org/hibernate/orm/test/onetoone/cache/Object2.hbm.xml", "org/hibernate/orm/test/onetoone/cache/MainObject.hbm.xml" } +) +@SessionFactory +public class OneToOneConstrainedCacheTest { + + @Test + public void testOneToOneCache(SessionFactoryScope scope) { + + //create a new MainObject + Object mainObjectId = createMainObject( scope ); + // load the MainObject + readMainObject( mainObjectId, scope ); + + //create and add Ojbect2 + addObject2( mainObjectId, scope ); + + //here the newly created Object2 is written to the database + //but the MainObject does not know it yet + MainObject mainObject = readMainObject( mainObjectId, scope ); + + assertNotNull( mainObject.getObj2() ); + + // after evicting, it works. + scope.getSessionFactory().getCache().evictEntityData( MainObject.class ); + + mainObject = readMainObject( mainObjectId, scope ); + + assertNotNull( mainObject.getObj2() ); + } + + /** + * creates a new MainObject + *

+ * one hibernate transaction ! + */ + private Object createMainObject(SessionFactoryScope scope) { + MainObject mainObject = scope.fromTransaction( + session -> { + MainObject mo = new MainObject(); + mo.setDescription( "Main Test" ); + + session.persist( mo ); + return mo; + } + ); + return mainObject.getId(); + } + + /** + * loads the newly created MainObject + * and adds a new Object2 to it + *

+ * one hibernate transaction + */ + private void addObject2(Object mainObjectId, SessionFactoryScope scope) { + scope.inTransaction( + session -> { + MainObject mo = session.getReference( MainObject.class, mainObjectId ); + + Object2 toAdd = new Object2(); + toAdd.setDummy( "test" ); + + mo.setObj2( toAdd ); + } + ); + } + + /** + * reads the newly created MainObject + * and its Object2 if it exists + *

+ * one hibernate transaction + */ + private MainObject readMainObject(Object id, SessionFactoryScope scope) { + return scope.fromTransaction( + session -> + session.get( MainObject.class, id ) + ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/flush/DirtyFlushTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/flush/DirtyFlushTest.java new file mode 100644 index 000000000000..9ab519397885 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/flush/DirtyFlushTest.java @@ -0,0 +1,110 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.onetoone.flush; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@TestForIssue(jiraKey = "HHH-15045") +@Jpa( + annotatedClasses = { + DirtyFlushTest.User.class, + DirtyFlushTest.Profile.class + } +) +public class DirtyFlushTest { + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + final User user = new User(); + user.id = 1; + user.version = 1; + + final Profile profile = new Profile(); + profile.id = 1; + profile.version = 1; + + em.persist( user ); + em.persist( profile ); + } ); + } + + @Test + public void testDirtyFlushNotHappened(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + final User user = em.find( User.class, 1 ); + assertEquals( 1, user.version ); + + final Profile profile = em.find( Profile.class, 1 ); + assertEquals( 1, profile.version ); + + profile.user = user; + user.profile = profile; + + em.persist( profile ); + em.flush(); + } ); + + scope.inTransaction( em -> { + final Profile profile = em.find( Profile.class, 1 ); + assertEquals( 2, profile.version ); + + final User user = em.find( User.class, 1 ); + assertEquals( + 1, + user.version, + "without fixing, the version will be bumped due to erroneous dirty flushing" + ); + } ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + em.createQuery( "delete from Profile" ).executeUpdate(); + em.createQuery( "delete from User" ).executeUpdate(); + } ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + public static class User { + @Id + int id; + + @Version + int version; + + @OneToOne(mappedBy = "user") + Profile profile; + } + + @Entity(name = "Profile") + @Table(name = "PROFILE_TABLE") + public static class Profile { + @Id + int id; + + @Version + int version; + @OneToOne // internally Hibernate will use `@ManyToOne` for this field + User user; + } +}