diff --git a/hibernate-core/src/main/java/org/hibernate/type/BasicType.java b/hibernate-core/src/main/java/org/hibernate/type/BasicType.java index 68710d96ab..ad4063f30d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/BasicType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/BasicType.java @@ -11,6 +11,7 @@ import java.util.List; import org.hibernate.Incubating; import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.IndexedConsumer; import org.hibernate.metamodel.mapping.BasicValuedMapping; @@ -169,4 +170,9 @@ public interface BasicType extends Type, BasicDomainType, MappingType, Bas final BasicValueConverter valueConverter = getValueConverter(); return valueConverter == null ? null : valueConverter.getSpecializedTypeDeclaration( getJdbcType(), dialect ); } + + @Override + default int compare(Object x, Object y, SessionFactoryImplementor sessionFactory) { + return compare( x, y ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java b/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java index 5117a638db..86da681a51 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java @@ -125,6 +125,11 @@ public abstract class CollectionType extends AbstractType implements Association return 0; // collections cannot be compared } + @Override + public int compare(Object x, Object y, SessionFactoryImplementor sessionFactory) { + return compare( x, y ); + } + @Override public int getHashCode(Object x) { throw new UnsupportedOperationException( "cannot doAfterTransactionCompletion lookups on collections" ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java index 02c49573d9..b9b7e2241a 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java @@ -220,6 +220,20 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen return 0; } + @Override + public int compare(Object x, Object y, SessionFactoryImplementor sessionFactory) { + if ( x == y ) { + return 0; + } + for ( int i = 0; i < propertySpan; i++ ) { + int propertyCompare = propertyTypes[i].compare( getPropertyValue( x, i ), getPropertyValue( y, i ), sessionFactory ); + if ( propertyCompare != 0 ) { + return propertyCompare; + } + } + return 0; + } + public boolean isMethodOf(Method method) { return false; } diff --git a/hibernate-core/src/main/java/org/hibernate/type/CustomType.java b/hibernate-core/src/main/java/org/hibernate/type/CustomType.java index 1d7ad211b9..511fd6abe4 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/CustomType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/CustomType.java @@ -12,7 +12,6 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Arrays; import java.util.Map; -import java.util.Objects; import org.hibernate.HibernateException; import org.hibernate.MappingException; diff --git a/hibernate-core/src/main/java/org/hibernate/type/MetaType.java b/hibernate-core/src/main/java/org/hibernate/type/MetaType.java index cf9487295d..4d863e5cce 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/MetaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/MetaType.java @@ -75,6 +75,11 @@ public class MetaType extends AbstractType { return String.class; } + @Override + public int compare(Object x, Object y, SessionFactoryImplementor sessionFactory) { + return compare( x, y ); + } + @Override public void nullSafeSet( PreparedStatement st, diff --git a/hibernate-core/src/main/java/org/hibernate/type/Type.java b/hibernate-core/src/main/java/org/hibernate/type/Type.java index 1d70b6bc82..d1486c9e57 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/Type.java +++ b/hibernate-core/src/main/java/org/hibernate/type/Type.java @@ -218,9 +218,8 @@ public interface Type extends Serializable { */ int compare(Object x, Object y); - default int compare(Object x, Object y, SessionFactoryImplementor sessionFactory) { - return compare( x, y ); - } + int compare(Object x, Object y, SessionFactoryImplementor sessionFactory); + /** * Should the parent be considered dirty, given both the old and current value? * diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/orderupdates/OrderUpdatesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/orderupdates/OrderUpdatesTest.java new file mode 100644 index 0000000000..a0db6f8ea3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/orderupdates/OrderUpdatesTest.java @@ -0,0 +1,227 @@ +package org.hibernate.orm.test.orderupdates; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedList; + +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static jakarta.persistence.CascadeType.REMOVE; + +@DomainModel( + annotatedClasses = { + OrderUpdatesTest.Parent.class, + OrderUpdatesTest.Child.class + } +) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.ORDER_UPDATES, value = "true") + } +) +@TestForIssue(jiraKey = "HHH-16368") +public class OrderUpdatesTest { + + @Test + public void testIt(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent1 = new Parent(); + parent1 = session.merge( parent1 ); + + Parent parent2 = new Parent(); + parent2 = session.merge( parent2 ); + + session.flush(); + + // create two children with the same name, so that they differ only in their parent + // otherwise the key doesn't trigger the exception + Child child1 = new Child(); + child1.setName( "name1" ); + child1.setValue( "value" ); + parent1.addChild( child1 ); + child1 = session.merge( child1 ); + + Child child2 = new Child(); + child2.setName( "name1" ); + child2.setValue( "value" ); + parent2.addChild( child2 ); + child2 = session.merge( child2 ); + + session.flush(); + + child1.setValue( "new-value" ); + child2.setValue( "new-value" ); + + session.flush(); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "parent", cascade = { REMOVE }, fetch = FetchType.LAZY) + private Collection children = new LinkedList<>(); + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public void addChild(Child child) { + children.add( child ); + child.setParent( this ); + } + + public void removeChild(Child child) { + children.remove( child ); + child.setParent( null ); + } + } + + @Entity(name = "Chil;d") + @IdClass(Key.class) + public static class Child { + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ID") + private Parent parent; + + @Id + protected String name; + + @Column(name = "VALUE_COLUMN") + protected String value; + + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( !( o instanceof Child ) ) { + return false; + } + return parent != null && parent.equals( ( (Child) o ).getParent() ); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + } + + public static class Key implements Serializable, Comparable { + + protected Parent parent; + + protected String name; + + public Key() { + } + + public Key(Parent parent, String name) { + this.parent = parent; + this.name = name; + } + + public Parent getParent() { + return parent; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + Key key = (Key) o; + + if ( parent != null ? + !parent.getId().equals( key.parent != null ? key.parent.getId() : null ) : + key.parent != null ) { + return false; + } + if ( name != null ? !name.equals( key.name != null ? key.name : null ) : key.name != null ) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = parent != null ? parent.getId().hashCode() : 0; + result = 31 * result + ( name != null ? name.hashCode() : 0 ); + return result; + } + + private final static Comparator COMPARATOR = Comparator.comparing( Key::getName ) + .thenComparing( c -> c.getParent() == null ? null : c.getParent().getId() ); + + @Override + public int compareTo(Key other) { + return COMPARATOR.compare( this, other ); + } + } +}