diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index fc6259bc7f8a..514068088899 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -481,10 +481,22 @@ private boolean mapAsIdClass( final ClassDetails classWithIdClass = inheritanceState.getClassWithIdClass( false ); if ( classWithIdClass != null ) { final IdClass idClassAnn = classWithIdClass.getDirectAnnotationUsage( IdClass.class ); - final Class idClassValue = idClassAnn.value(); - final ClassDetails compositeClass = - getMetadataCollector().getSourceModelBuildingContext().getClassDetailsRegistry() - .resolveClassDetails( idClassValue.getName() ); + final ClassDetails compositeClass; + if ( idClassAnn == null ) { + try { + compositeClass = getMetadataCollector().getSourceModelBuildingContext() + .getClassDetailsRegistry() + .resolveClassDetails( inheritanceState.getClassDetails().getClassName() + "_$Id" ); + } + catch (RuntimeException e) { + return false; + } + } + else { + final Class idClassValue = idClassAnn.value(); + compositeClass = getMetadataCollector().getSourceModelBuildingContext() + .getClassDetailsRegistry().resolveClassDetails( idClassValue.getName() ); + } final TypeDetails compositeType = new ClassTypeDetailsImpl( compositeClass, TypeDetails.Kind.CLASS ); final TypeDetails classWithIdType = new ClassTypeDetailsImpl( classWithIdClass, TypeDetails.Kind.CLASS ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java index c149b6806297..cb52313d3626 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.hibernate.AnnotationException; import org.hibernate.boot.spi.AccessType; @@ -183,6 +184,13 @@ else if ( classDetails.hasDirectAnnotationUsage( IdClass.class ) ) { return classDetails; } else { + final long count = Stream.concat( + classDetails.getFields().stream(), + classDetails.getMethods().stream() + ).filter( t -> t.hasDirectAnnotationUsage( Id.class ) ).count(); + if ( count > 1 ) { + return classDetails; + } final InheritanceState state = getSuperclassInheritanceState( classDetails, inheritanceStatePerClass ); if ( state != null ) { return state.getClassWithIdClass( true ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/Order.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/Order.java index cc624bb9e5ef..470f5983b5a4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/Order.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/Order.java @@ -13,9 +13,11 @@ import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import org.hibernate.annotations.processing.Exclude; @SuppressWarnings("serial") @Entity +@Exclude @Table(name = "orders") public class Order implements Serializable { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/OrderLine.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/OrderLine.java index 2eed3be99ce6..97bba26f34fa 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/OrderLine.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/OrderLine.java @@ -10,9 +10,11 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import org.hibernate.annotations.processing.Exclude; @SuppressWarnings("serial") @Entity +@Exclude @Table(name = "order_line") // @IdClass(OrderLinePK.class) public class OrderLine implements Serializable diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/Product.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/Product.java index dce2a99c93bd..69fc69c138c9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/Product.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/bidirectional/Product.java @@ -9,9 +9,11 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.hibernate.annotations.processing.Exclude; @SuppressWarnings("serial") @Entity +@Exclude @Table(name = "products") public class Product implements Serializable { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/hhh18829/AutoGeneratedIdClassTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/hhh18829/AutoGeneratedIdClassTest.java new file mode 100644 index 000000000000..ef51add96b9e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/hhh18829/AutoGeneratedIdClassTest.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.hhh18829; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.InvocationTargetException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel(annotatedClasses = EmployeeWithoutIdClass.class) +@JiraKey(" HHH-18829") +@SessionFactory +public class AutoGeneratedIdClassTest { + + @BeforeAll + void setUp(SessionFactoryScope sessionFactoryScope) { + sessionFactoryScope.inTransaction( sess -> { + final var one = new EmployeeWithoutIdClass(); + one.empName = "John Doe"; + one.empId = 1; + one.address = "10 Downing Street, SW1A 2AA"; + sess.persist( one ); + + final var two = new EmployeeWithoutIdClass(); + two.empName = "Dave Default"; + two.empId = 1; + two.address = "1600 Pennsylvania Avenue"; + sess.persist( two ); + } ); + } + + @Test + public void test(SessionFactoryScope sessionFactoryScope) + throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException { + final var idClass = Class.forName( EmployeeWithoutIdClass.class.getName() + "_$Id" ); + final var id = idClass.getConstructors()[0].newInstance( "John Doe", 1 ); + final var employees = sessionFactoryScope.fromSession( + sess -> sess.createQuery( "from EmployeeWithoutIdClass where id=:id", EmployeeWithoutIdClass.class ).setParameter( "id", id ) + .getResultList() + ); + assertEquals( 1, employees.size() ); + assertEquals( "10 Downing Street, SW1A 2AA", employees.get( 0 ).address ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/hhh18829/EmployeeWithoutIdClass.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/hhh18829/EmployeeWithoutIdClass.java new file mode 100644 index 000000000000..9ea1b8d14b39 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/hhh18829/EmployeeWithoutIdClass.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.hhh18829; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class EmployeeWithoutIdClass { + @Id + String empName; + @Id + Integer empId; + String address; +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java index a3f975e836b7..cc52bb0c429c 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java @@ -108,6 +108,8 @@ */ public class AnnotationMetaEntity extends AnnotationMeta { + private static final String ID_CLASS_MEMBER_NAME = ""; + private final ImportContext importContext; private final TypeElement element; private final Map members; @@ -443,6 +445,8 @@ && containsAnnotation( method, HQL, SQL, FIND ) ) { addPersistentMembers( fieldsOfClass, AccessType.FIELD ); addPersistentMembers( gettersAndSettersOfClass, AccessType.PROPERTY ); + + addIdClassIfNeeded( fieldsOfClass, gettersAndSettersOfClass ); } addAuxiliaryMembers(); @@ -456,6 +460,33 @@ && containsAnnotation( method, HQL, SQL, FIND ) ) { initialized = true; } + private void addIdClassIfNeeded(List fields, List methods) { + if ( hasAnnotation( element, ID_CLASS ) ) { + return; + } + final List components = new ArrayList<>(); + for ( Element field : fields ) { + if ( hasAnnotation( field, ID ) && isPersistent( field, AccessType.FIELD ) ) { + final String propertyName = propertyName( this, field ); + if ( members.containsKey( propertyName ) ) { + components.add( members.get( propertyName ) ); + } + } + } + for ( Element method : methods ) { + if ( hasAnnotation( method, ID ) && isPersistent( method, AccessType.PROPERTY ) ) { + final String propertyName = propertyName( this, method ); + if ( members.containsKey( propertyName ) ) { + components.add( members.get( propertyName ) ); + } + } + } + if ( components.size() < 2 ) { + return; + } + putMember( ID_CLASS_MEMBER_NAME, new IdClassMetaAttribute( this, components ) ); + } + private boolean checkEntities(List lifecycleMethods) { boolean foundPersistenceEntity = false; VariableElement nonPersistenceParameter = null; diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaSingleAttribute.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaSingleAttribute.java index c51ff79f5b1a..e7c40cf38d64 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaSingleAttribute.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaSingleAttribute.java @@ -4,11 +4,15 @@ */ package org.hibernate.processor.annotation; +import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import org.hibernate.processor.model.MetaSingleAttribute; import org.hibernate.processor.util.Constants; +import java.util.ArrayList; +import java.util.List; + /** * @author Max Andersen * @author Hardy Ferentschik @@ -24,4 +28,9 @@ public AnnotationMetaSingleAttribute(AnnotationMetaEntity parent, Element elemen public final String getMetaType() { return Constants.SINGULAR_ATTRIBUTE; } + + @Override + public List inheritedAnnotations() { + return new ArrayList<>(element.getAnnotationMirrors()); + } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/IdClassMetaAttribute.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/IdClassMetaAttribute.java new file mode 100644 index 000000000000..9843789cae48 --- /dev/null +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/IdClassMetaAttribute.java @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.annotation; + +import org.hibernate.processor.model.MetaAttribute; +import org.hibernate.processor.model.Metamodel; + +import java.util.List; + +public class IdClassMetaAttribute implements MetaAttribute { + + private final Metamodel parent; + + private final List components; + + public IdClassMetaAttribute(Metamodel parent, List components) { + this.parent = parent; + this.components = components; + } + + @Override + public boolean hasTypedAttribute() { + return true; + } + + @Override + public boolean hasStringAttribute() { + return false; + } + + @Override + public String getAttributeDeclarationString() { + final StringBuilder decl = new StringBuilder() + .append("\n/**\n * Static ID class for {@link ") + .append( parent.getQualifiedName() ) + .append( "}\n **/\n" ) + .append( "public record Id" ); + String delimiter = "("; + for ( MetaAttribute component : components ) { + decl.append( delimiter ).append( parent.importType( component.getTypeDeclaration() ) ) + .append( ' ' ).append( component.getPropertyName() ); + delimiter = ", "; + } + return decl.append( ") {}" ).toString(); + } + + @Override + public String getAttributeNameDeclarationString() { + return ""; + } + + @Override + public String getMetaType() { + return ""; + } + + @Override + public String getPropertyName() { + return ""; + } + + @Override + public String getTypeDeclaration() { + return ""; + } + + @Override + public Metamodel getHostingEntity() { + return parent; + } +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/Address.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/Address.java new file mode 100644 index 000000000000..e07d21e87d75 --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/Address.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.hhh18829; + +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +public class Address { + String address; +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/AnotherEmployee.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/AnotherEmployee.java new file mode 100644 index 000000000000..5cc782856f9c --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/AnotherEmployee.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.hhh18829; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class AnotherEmployee extends Address { + Integer empId; + + String empName; + + @Id + public Integer getEmpId() { + return empId; + } + + public void setEmpId(Integer empId) { + this.empId = empId; + } + + @Id + public String getEmpName() { + return empName; + } + + public void setEmpName(String empName) { + this.empName = empName; + } +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/AutoGeneratedIdClassTest.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/AutoGeneratedIdClassTest.java new file mode 100644 index 000000000000..58e73bf333b7 --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/AutoGeneratedIdClassTest.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.hhh18829; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.TestForIssue; +import org.hibernate.processor.test.util.TestUtil; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import java.lang.reflect.RecordComponent; +import java.util.Arrays; + +import static org.hibernate.processor.test.util.TestUtil.getMetamodelClassFor; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestForIssue(jiraKey = " HHH-18829") +public class AutoGeneratedIdClassTest extends CompilationTest { + + @Test + @WithClasses({Employee.class, AnotherEmployee.class, Address.class, EmployeeWithIdClass.class}) + @TestForIssue(jiraKey = "HHH-18829") + public void test() { + System.out.println( TestUtil.getMetaModelSourceAsString( Employee.class ) ); + System.out.println( TestUtil.getMetaModelSourceAsString( AnotherEmployee.class ) ); + System.out.println( TestUtil.getMetaModelSourceAsString( Address.class ) ); + System.out.println( TestUtil.getMetaModelSourceAsString( EmployeeWithIdClass.class ) ); + + checkIfIdClassIsGenerated( Employee.class, new String[] {"empName", "empId"} ); + checkIfIdClassIsGenerated( AnotherEmployee.class, new String[] {"empId", "empName"} ); + + final var clazz = getMetamodelClassFor( EmployeeWithIdClass.class ); + assertTrue( Arrays.stream( clazz.getClasses() ).map( Class::getSimpleName ) + .noneMatch( "Id"::equals ), + "EmployeeWithIdClass_ should not have inner class Id" ); + } + + private static void checkIfIdClassIsGenerated(Class entityClass, String[] idComponentNames) { + final var clazz = getMetamodelClassFor( entityClass ); + final var maybeIdClass = Arrays.stream( clazz.getClasses() ) + .filter( c -> c.getSimpleName().equals( "Id" ) ).findAny(); + assertTrue( maybeIdClass.isPresent(), () -> clazz.getSimpleName() + "_ should have inner class Id" ); + final Class idClass = maybeIdClass.get(); + assertTrue( idClass.isRecord(), "Generated ID class should be a record" ); + assertArrayEquals( + idComponentNames, + Arrays.stream( idClass.getRecordComponents() ).map( RecordComponent::getName ).toArray( String[]::new ) + ); + } +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/Employee.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/Employee.java new file mode 100644 index 000000000000..4ef9535d39cf --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/Employee.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.hhh18829; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class Employee extends Address { + @Id + String empName; + @Id + Integer empId; + +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/EmployeeWithIdClass.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/EmployeeWithIdClass.java new file mode 100644 index 000000000000..fa5e80d7ce9f --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18829/EmployeeWithIdClass.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.hhh18829; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; + +@Entity +@IdClass(EmployeeWithIdClass.EmployeeId.class) +public class EmployeeWithIdClass extends Address { + @Id + String empName; + @Id + Integer empId; + + public record EmployeeId(String empName, Integer empId) { + } + +}