diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/JPAUnitTestCase.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/JPAUnitTestCase.java new file mode 100644 index 000000000000..19b6ccee2cb7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/JPAUnitTestCase.java @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.broken; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.LongStream; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; + +public class JPAUnitTestCase extends BaseEntityManagerFunctionalTestCase { + + + @Override + public Class[] getAnnotatedClasses() { + return new Class[] {ThirdParty.class, Provider.class, VoiceGroup.class, TelephoneNumber.class}; + } + + @Before + public void setup() { + doInJPA( this::entityManagerFactory, em -> { + + ThirdParty thirdParty = new ThirdParty(); + thirdParty.setName( "Globex Corporation" ); + em.persist( thirdParty ); + + Provider provider = new Provider(); + provider.setThirdParty( thirdParty ); + em.persist( provider ); + + VoiceGroup voiceGroup = new VoiceGroup(); + em.persist( voiceGroup ); + + TelephoneNumber primaryNumber = new TelephoneNumber(); + primaryNumber.setNumber( "4065551234" ); + primaryNumber.setProvider( provider ); + primaryNumber.setVoiceGroup( voiceGroup ); + em.persist( primaryNumber ); + + voiceGroup.setPrimaryNumber( primaryNumber ); + + LongStream.rangeClosed( 4065551235L, 4065551255L ).forEach( value -> { + TelephoneNumber telephoneNumber = new TelephoneNumber(); + telephoneNumber.setNumber( String.valueOf( value ) ); + telephoneNumber.setProvider( provider ); + telephoneNumber.setVoiceGroup( voiceGroup ); + em.persist( telephoneNumber ); + } ); + } ); + } + + @Test + public void testThatPasses() { + EntityManager em = getOrCreateEntityManager(); + em.getTransaction().begin(); + VoiceGroup voiceGroup = em.find( VoiceGroup.class, 1, + Map.of( "jakarta.persistence.fetchgraph", em.getEntityGraph( "voiceGroup.graph" ) ) ); + allNumbersWithThirdPartyFetch( em, voiceGroup ).forEach( telephoneNumber -> Objects.requireNonNull( telephoneNumber.getProvider().getName() ) ); + em.getTransaction().commit(); + em.close(); + } + + @Test + public void testThatFails() { + EntityManager em = getOrCreateEntityManager(); + em.getTransaction().begin(); + VoiceGroup voiceGroup = em.find( VoiceGroup.class, 1, + Map.of( "jakarta.persistence.fetchgraph", em.getEntityGraph( "voiceGroup.graph" ) ) ); + allNumbersWithoutThirdPartyFetch( em, voiceGroup ).forEach( telephoneNumber -> Objects.requireNonNull( telephoneNumber.getProvider().getName() ) ); + em.getTransaction().commit(); + em.close(); + } + + + private List allNumbersWithoutThirdPartyFetch(EntityManager em, VoiceGroup voiceGroup) { + CriteriaQuery query = em.getCriteriaBuilder().createQuery( TelephoneNumber.class ); + Root root = query.from( TelephoneNumber.class ); + root.fetch( "provider" ); + query.where( em.getCriteriaBuilder().equal( root.get( "voiceGroup" ), voiceGroup ) ); + return em.createQuery( query ).getResultList(); + } + + private List allNumbersWithThirdPartyFetch(EntityManager em, VoiceGroup voiceGroup) { + CriteriaQuery query = em.getCriteriaBuilder().createQuery( TelephoneNumber.class ); + Root root = query.from( TelephoneNumber.class ); + root.fetch( "provider" ).fetch( "thirdParty" ); + query.where( em.getCriteriaBuilder().equal( root.get( "voiceGroup" ), voiceGroup ) ); + return em.createQuery( query ).getResultList(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/Provider.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/Provider.java new file mode 100644 index 000000000000..735a21291b3a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/Provider.java @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.broken; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Transient; +import jakarta.persistence.Version; + +import java.io.Serializable; +import java.util.Objects; + +@Entity +public class Provider implements Serializable { + + private Integer id; + private int version; + private ThirdParty thirdParty; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Version + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + @OneToOne(mappedBy = "provider", optional = false) + public ThirdParty getThirdParty() { + return thirdParty; + } + + public void setThirdParty(ThirdParty thirdParty) { + this.thirdParty = thirdParty; + } + + @Transient + public String getName() { + return thirdParty.getName(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Provider provider) { + return this == o || getId().equals(provider.getId()); + } + else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/TelephoneNumber.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/TelephoneNumber.java new file mode 100644 index 000000000000..173c6ac8fad0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/TelephoneNumber.java @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.broken; + +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.Version; + +import java.io.Serializable; +import java.util.Objects; + +@Entity +public class TelephoneNumber implements Serializable { + + private Integer id; + private int version; + private String number; + private VoiceGroup voiceGroup; + private Provider provider; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Version + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "voiceGroup", nullable = false) + public VoiceGroup getVoiceGroup() { + return voiceGroup; + } + + public void setVoiceGroup(VoiceGroup voiceGroup) { + this.voiceGroup = voiceGroup; + } + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "provider", nullable = false) + public Provider getProvider() { + return provider; + } + + public void setProvider(Provider provider) { + this.provider = provider; + } + + @Override + public boolean equals(Object o) { + if ( o instanceof TelephoneNumber telephoneNumber ) { + return this == o || getId().equals( telephoneNumber.getId() ); + } + else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode( id ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/ThirdParty.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/ThirdParty.java new file mode 100644 index 000000000000..70d229f47be7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/ThirdParty.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.broken; + +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.OneToOne; +import jakarta.persistence.Version; +import jakarta.validation.constraints.NotNull; + +import java.io.Serializable; + +@Entity +public class ThirdParty implements Serializable { + + private Integer id; + private int version; + private String name; + private Provider provider; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Version + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + @NotNull + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "provider", nullable = true) + public Provider getProvider() { + return provider; + } + + public void setProvider(Provider provider) { + this.provider = provider; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/VoiceGroup.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/VoiceGroup.java new file mode 100644 index 000000000000..b7d93b64b97c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/broken/VoiceGroup.java @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.broken; + +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.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Version; + +import java.io.Serializable; +import java.util.Objects; + +@NamedEntityGraph(name = "voiceGroup.graph", attributeNodes = { @NamedAttributeNode("primaryNumber")}) +@Entity +public class VoiceGroup implements Serializable { + + private Integer id; + private int version; + private TelephoneNumber primaryNumber; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Version + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "primaryNumber", nullable = true) + public TelephoneNumber getPrimaryNumber() { + return primaryNumber; + } + + public void setPrimaryNumber(TelephoneNumber primaryNumber) { + this.primaryNumber = primaryNumber; + } + + @Override + public boolean equals(Object o) { + if (o instanceof VoiceGroup voiceGroup) { + return this == o || getId().equals(voiceGroup.getId()); + } + else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +}