From 36c73ab342a84d20180b20c421c6002c28304a8d Mon Sep 17 00:00:00 2001 From: Leandro Taveras Date: Thu, 5 Oct 2023 19:58:09 -0700 Subject: [PATCH] [HHH-17294] Test verifying issue where Entity is not marked as dirty if one of its JSON children is modified --- .../test/mapping/embeddable/Aggregate.java | 469 ++++++++++++++++++ .../mapping/embeddable/JsonAggregateTest.java | 110 ++++ 2 files changed, 579 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/Aggregate.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonAggregateTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/Aggregate.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/Aggregate.java new file mode 100644 index 0000000000..184e0cfefd --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/Aggregate.java @@ -0,0 +1,469 @@ +/* + * 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 http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.mapping.embeddable; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Temporal; +import jakarta.persistence.TemporalType; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; +import org.hibernate.testing.orm.domain.gambit.MutableValue; +import org.junit.jupiter.api.Assertions; + +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.Clob; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Date; +import java.util.Objects; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@Access( AccessType.PROPERTY ) +public class Aggregate { + + private Boolean theBoolean = false; + private Boolean theNumericBoolean = false; + private Boolean theStringBoolean = false; + private String theString; + private Integer theInteger; + private int theInt; + private double theDouble; + private URL theUrl; + private Clob theClob; + private byte[] theBinary; + private Date theDate; + private Date theTime; + private Date theTimestamp; + private Instant theInstant; + private UUID theUuid; + private EntityOfBasics.Gender gender; + private EntityOfBasics.Gender convertedGender; + private EntityOfBasics.Gender ordinalGender; + private Duration theDuration; + + private LocalDateTime theLocalDateTime; + private LocalDate theLocalDate; + private LocalTime theLocalTime; + private ZonedDateTime theZonedDateTime; + private OffsetDateTime theOffsetDateTime; + + private MutableValue mutableValue; + + public Aggregate() { + } + + public String getTheString() { + return theString; + } + + public void setTheString(String theString) { + this.theString = theString; + } + + public Integer getTheInteger() { + return theInteger; + } + + public void setTheInteger(Integer theInteger) { + this.theInteger = theInteger; + } + + public int getTheInt() { + return theInt; + } + + public void setTheInt(int theInt) { + this.theInt = theInt; + } + + public double getTheDouble() { + return theDouble; + } + + public void setTheDouble(double theDouble) { + this.theDouble = theDouble; + } + + public URL getTheUrl() { + return theUrl; + } + + public void setTheUrl(URL theUrl) { + this.theUrl = theUrl; + } + + public Clob getTheClob() { + return theClob; + } + + public void setTheClob(Clob theClob) { + this.theClob = theClob; + } + + public byte[] getTheBinary() { + return theBinary; + } + + public void setTheBinary(byte[] theBinary) { + this.theBinary = theBinary; + } + + @Enumerated( EnumType.STRING ) + public EntityOfBasics.Gender getGender() { + return gender; + } + + public void setGender(EntityOfBasics.Gender gender) { + this.gender = gender; + } + + @Convert( converter = EntityOfBasics.GenderConverter.class ) + @Column(name = "converted_gender", length = 1) + @JdbcTypeCode( Types.CHAR ) + public EntityOfBasics.Gender getConvertedGender() { + return convertedGender; + } + + public void setConvertedGender(EntityOfBasics.Gender convertedGender) { + this.convertedGender = convertedGender; + } + + @Column(name = "ordinal_gender") + public EntityOfBasics.Gender getOrdinalGender() { + return ordinalGender; + } + + public void setOrdinalGender(EntityOfBasics.Gender ordinalGender) { + this.ordinalGender = ordinalGender; + } + + @Temporal( TemporalType.DATE ) + public Date getTheDate() { + return theDate; + } + + public void setTheDate(Date theDate) { + this.theDate = theDate; + } + + @Temporal( TemporalType.TIME ) + public Date getTheTime() { + return theTime; + } + + public void setTheTime(Date theTime) { + this.theTime = theTime; + } + + @Temporal( TemporalType.TIMESTAMP ) + public Date getTheTimestamp() { + return theTimestamp; + } + + public void setTheTimestamp(Date theTimestamp) { + this.theTimestamp = theTimestamp; + } + + @Temporal( TemporalType.TIMESTAMP ) + public Instant getTheInstant() { + return theInstant; + } + + public void setTheInstant(Instant theInstant) { + this.theInstant = theInstant; + } + + public UUID getTheUuid() { + return theUuid; + } + + public void setTheUuid(UUID theUuid) { + this.theUuid = theUuid; + } + + public LocalDateTime getTheLocalDateTime() { + return theLocalDateTime; + } + + public void setTheLocalDateTime(LocalDateTime theLocalDateTime) { + this.theLocalDateTime = theLocalDateTime; + } + + public LocalDate getTheLocalDate() { + return theLocalDate; + } + + public void setTheLocalDate(LocalDate theLocalDate) { + this.theLocalDate = theLocalDate; + } + + public LocalTime getTheLocalTime() { + return theLocalTime; + } + + public void setTheLocalTime(LocalTime theLocalTime) { + this.theLocalTime = theLocalTime; + } + + public OffsetDateTime getTheOffsetDateTime() { + return theOffsetDateTime; + } + + public void setTheOffsetDateTime(OffsetDateTime theOffsetDateTime) { + this.theOffsetDateTime = theOffsetDateTime; + } + + public ZonedDateTime getTheZonedDateTime() { + return theZonedDateTime; + } + + public void setTheZonedDateTime(ZonedDateTime theZonedDateTime) { + this.theZonedDateTime = theZonedDateTime; + } + + public Duration getTheDuration() { + return theDuration; + } + + public void setTheDuration(Duration theDuration) { + this.theDuration = theDuration; + } + + public Boolean isTheBoolean() { + return theBoolean; + } + + public void setTheBoolean(Boolean theBoolean) { + this.theBoolean = theBoolean; + } + + @JdbcTypeCode( Types.INTEGER ) + public Boolean isTheNumericBoolean() { + return theNumericBoolean; + } + + public void setTheNumericBoolean(Boolean theNumericBoolean) { + this.theNumericBoolean = theNumericBoolean; + } + + @JdbcTypeCode( Types.CHAR ) + public Boolean isTheStringBoolean() { + return theStringBoolean; + } + + public void setTheStringBoolean(Boolean theStringBoolean) { + this.theStringBoolean = theStringBoolean; + } + + @Convert( converter = EntityOfBasics.MutableValueConverter.class ) + public MutableValue getMutableValue() { + return mutableValue; + } + + public void setMutableValue(MutableValue mutableValue) { + this.mutableValue = mutableValue; + } + + static void assertEquals(Aggregate a1, Aggregate a2) { + Assertions.assertEquals( a1.theInt, a2.theInt ); + Assertions.assertEquals( a1.theDouble, a2.theDouble ); + Assertions.assertEquals( a1.theBoolean, a2.theBoolean ); + Assertions.assertEquals( a1.theNumericBoolean, a2.theNumericBoolean ); + Assertions.assertEquals( a1.theStringBoolean, a2.theStringBoolean ); + Assertions.assertEquals( a1.theString, a2.theString ); + Assertions.assertEquals( a1.theInteger, a2.theInteger ); + Assertions.assertEquals( a1.theClob, a2.theClob ); + assertArrayEquals( a1.theBinary, a2.theBinary ); + Assertions.assertEquals( a1.theDate, a2.theDate ); + Assertions.assertEquals( a1.theTime, a2.theTime ); + Assertions.assertEquals( a1.theTimestamp, a2.theTimestamp ); + Assertions.assertEquals( a1.theInstant, a2.theInstant ); + Assertions.assertEquals( a1.theUuid, a2.theUuid ); + Assertions.assertEquals( a1.gender, a2.gender ); + Assertions.assertEquals( a1.convertedGender, a2.convertedGender ); + Assertions.assertEquals( a1.ordinalGender, a2.ordinalGender ); + Assertions.assertEquals( a1.theDuration, a2.theDuration ); + Assertions.assertEquals( a1.theLocalDateTime, a2.theLocalDateTime ); + Assertions.assertEquals( a1.theLocalDate, a2.theLocalDate ); + Assertions.assertEquals( a1.theLocalTime, a2.theLocalTime ); + if ( a1.theZonedDateTime == null ) { + assertNull( a2.theZonedDateTime ); + } + else { + assertNotNull( a2.theZonedDateTime ); + Assertions.assertEquals( a1.theZonedDateTime.toInstant(), a2.theZonedDateTime.toInstant() ); + } + if ( a1.theOffsetDateTime == null ) { + assertNull( a2.theOffsetDateTime ); + } + else { + assertNotNull( a2.theOffsetDateTime ); + Assertions.assertEquals( a1.theOffsetDateTime.toInstant(), a2.theOffsetDateTime.toInstant() ); + } + if ( a1.mutableValue == null ) { + assertNull( a2.mutableValue ); + } + else { + assertNotNull( a2.mutableValue ); + Assertions.assertEquals( a1.mutableValue.getState(), a2.mutableValue.getState() ); + } + } + + public static Aggregate createAggregate1() { + final Aggregate aggregate = new Aggregate(); + aggregate.theBoolean = true; + aggregate.theNumericBoolean = true; + aggregate.theStringBoolean = true; + aggregate.theString = "String \"A&B\""; + aggregate.theInteger = -1; + aggregate.theInt = Integer.MAX_VALUE; + aggregate.theDouble = 1.3e20; + try { + aggregate.theUrl = new URL( "https://hibernate.org" ); + } + catch (MalformedURLException e) { + throw new RuntimeException( e ); + } + aggregate.theBinary = new byte[] { 1 }; + aggregate.theDate = new java.sql.Date( 2000 - 1900, 0, 1 ); + aggregate.theTime = new Time( 1, 0, 0 ); + aggregate.theTimestamp = new Timestamp( 2000 - 1900, 0, 1, 1, 0, 0, 1000 ); + aggregate.theInstant = LocalDateTime.of( 2000, 1, 1, 0, 0, 0 ).toInstant( ZoneOffset.UTC ); + aggregate.theUuid = UUID.fromString( "53886a8a-7082-4879-b430-25cb94415be8" ); + aggregate.gender = EntityOfBasics.Gender.FEMALE; + aggregate.convertedGender = EntityOfBasics.Gender.MALE; + aggregate.ordinalGender = EntityOfBasics.Gender.OTHER; + aggregate.theDuration = Duration.ofHours( 1 ); + aggregate.theLocalDateTime = LocalDateTime.of( 2000, 1, 1, 0, 0, 0 ); + aggregate.theLocalDate = LocalDate.of( 2000, 1, 1 ); + aggregate.theLocalTime = LocalTime.of( 1, 0, 0 ); + aggregate.theZonedDateTime = LocalDateTime.of( 2000, 1, 1, 0, 0, 0 ).atZone( ZoneOffset.UTC ); + aggregate.theOffsetDateTime = LocalDateTime.of( 2000, 1, 1, 0, 0, 0 ).atOffset( ZoneOffset.UTC ); + aggregate.mutableValue = new MutableValue( "some state" ); + return aggregate; + } + + public static Aggregate createAggregate2() { + final Aggregate aggregate = new Aggregate(); + aggregate.theString = "String 'abc'"; + return aggregate; + } + + public static Aggregate createAggregate3() { + final Aggregate aggregate = new Aggregate(); + aggregate.theString = "ABC"; + aggregate.theBinary = new byte[] { 1 }; + aggregate.theUuid = UUID.fromString( "53886a8a-7082-4879-b430-25cb94415be8" ); + aggregate.theLocalDateTime = LocalDateTime.of( 2022, 12, 1, 1, 0, 0 ); + return aggregate; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + Aggregate that = (Aggregate) o; + + if ( theInt != that.theInt ) { + return false; + } + if ( Double.compare( that.theDouble, theDouble ) != 0 ) { + return false; + } + if ( !Objects.equals( theBoolean, that.theBoolean ) ) { + return false; + } + if ( !Objects.equals( theNumericBoolean, that.theNumericBoolean ) ) { + return false; + } + if ( !Objects.equals( theStringBoolean, that.theStringBoolean ) ) { + return false; + } + if ( !Objects.equals( theString, that.theString ) ) { + return false; + } + if ( !Objects.equals( theInteger, that.theInteger ) ) { + return false; + } + if ( !Objects.equals( theUrl, that.theUrl ) ) { + return false; + } + if ( !Objects.equals( theClob, that.theClob ) ) { + return false; + } + if ( !Arrays.equals( theBinary, that.theBinary ) ) { + return false; + } + if ( !Objects.equals( theDate, that.theDate ) ) { + return false; + } + if ( !Objects.equals( theTime, that.theTime ) ) { + return false; + } + if ( !Objects.equals( theTimestamp, that.theTimestamp ) ) { + return false; + } + if ( !Objects.equals( theInstant, that.theInstant ) ) { + return false; + } + if ( !Objects.equals( theUuid, that.theUuid ) ) { + return false; + } + if ( gender != that.gender ) { + return false; + } + if ( convertedGender != that.convertedGender ) { + return false; + } + if ( ordinalGender != that.ordinalGender ) { + return false; + } + if ( !Objects.equals( theDuration, that.theDuration ) ) { + return false; + } + if ( !Objects.equals( theLocalDateTime, that.theLocalDateTime ) ) { + return false; + } + if ( !Objects.equals( theLocalDate, that.theLocalDate ) ) { + return false; + } + if ( !Objects.equals( theLocalTime, that.theLocalTime ) ) { + return false; + } + if ( !Objects.equals( theZonedDateTime, that.theZonedDateTime ) ) { + return false; + } + if ( !Objects.equals( theOffsetDateTime, that.theOffsetDateTime ) ) { + return false; + } + return Objects.equals( mutableValue, that.mutableValue ); + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonAggregateTest.java new file mode 100644 index 0000000000..b6f55c555f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonAggregateTest.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 http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.mapping.embeddable; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.testing.orm.junit.BaseSessionFactoryFunctionalTest; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; + +import org.hibernate.type.SqlTypes; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +//@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonAggregate.class) +public class JsonAggregateTest extends BaseSessionFactoryFunctionalTest { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + JsonHolder.class + }; + } + + @BeforeEach + public void setUp() { + inTransaction( + session -> { + session.persist( new JsonHolder( 1L, Aggregate.createAggregate2() ) ); + } + ); + } + + @AfterEach + protected void cleanupTest() { + inTransaction( + session -> { + session.createMutationQuery( "delete from JsonHolder h" ).executeUpdate(); + } + ); + } + + @Test + @JiraKey("HHH-17294") + public void testDirtyChecking() { + sessionFactoryScope().inTransaction( + entityManager -> { + JsonHolder jsonHolder = entityManager.find( JsonHolder.class, 1L ); + assertEquals("String 'abc'", jsonHolder.getAggregate().getTheString()); + jsonHolder.getAggregate().setTheString( "MyString" ); + entityManager.flush(); + entityManager.clear(); + // Fails, when it should pass + assertEquals( "String 'MyString'", entityManager.find( JsonHolder.class, 1L ).getAggregate().getTheString() ); + } + ); + } + + //tag::json-type-mapping-example[] + @Entity(name = "JsonHolder") + public static class JsonHolder { + + @Id + private Long id; + @JdbcTypeCode(SqlTypes.JSON) + private Aggregate aggregate; + + //end::json-type-mapping-example[] + //Getters and setters are omitted for brevity + + public JsonHolder() { + } + + public JsonHolder(Long id, Aggregate aggregate) { + this.id = id; + this.aggregate = aggregate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Aggregate getAggregate() { + return aggregate; + } + + public void setAggregate(Aggregate aggregate) { + this.aggregate = aggregate; + } + + //tag::json-type-mapping-example[] + } + + //end::json-type-mapping-example[] +} +