diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java index f7e6bd6d5f6205..9a3784006626ca 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java @@ -42,6 +42,7 @@ private static DotName createConstant(String fqcn) { public static final DotName ID_CLASS = createConstant("jakarta.persistence.IdClass"); public static final DotName CONVERTER = createConstant("jakarta.persistence.Converter"); public static final DotName EMBEDDED = createConstant("jakarta.persistence.Embedded"); + public static final DotName ID = createConstant("jakarta.persistence.Id"); public static final DotName EMBEDDED_ID = createConstant("jakarta.persistence.EmbeddedId"); public static final DotName ELEMENT_COLLECTION = createConstant("jakarta.persistence.ElementCollection"); public static final DotName PROXY = createConstant("org.hibernate.annotations.Proxy"); diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/GraalVMFeatures.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/GraalVMFeatures.java index d964cdae8694c8..2a2be846590a2c 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/GraalVMFeatures.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/GraalVMFeatures.java @@ -29,16 +29,6 @@ ReflectiveClassBuildItem registerGeneratorClassesForReflections() { .build(); } - // Workaround for https://hibernate.atlassian.net/browse/HHH-16809 - // See https://github.com/hibernate/hibernate-orm/pull/6815#issuecomment-1662197545 - @BuildStep - ReflectiveClassBuildItem registerJdbcArrayTypesForReflection() { - return ReflectiveClassBuildItem - .builder(ClassNames.JDBC_JAVA_TYPES.stream().map(d -> d.toString() + "[]").toArray(String[]::new)) - .reason(ClassNames.GRAAL_VM_FEATURES.toString()) - .build(); - } - // Workaround for https://hibernate.atlassian.net/browse/HHH-18875 // See https://hibernate.zulipchat.com/#narrow/channel/132094-hibernate-orm-dev/topic/StandardStack.20and.20reflection @BuildStep diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/JpaJandexScavenger.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/JpaJandexScavenger.java index ea981a57ff95c8..8222e2855cf358 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/JpaJandexScavenger.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/JpaJandexScavenger.java @@ -10,8 +10,10 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; +import io.quarkus.deployment.bean.JavaBeanUtil; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmCompositeAttributeType; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmDiscriminatorSubclassEntityType; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmEntityBaseDefinition; @@ -19,6 +21,8 @@ import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmJoinedSubclassEntityType; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmRootEntityType; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmUnionSubclassEntityType; +import org.hibernate.boot.jaxb.mapping.AttributesContainer; +import org.hibernate.boot.jaxb.mapping.JaxbAttributes; import org.hibernate.boot.jaxb.mapping.EntityOrMappedSuperclass; import org.hibernate.boot.jaxb.mapping.JaxbConverter; import org.hibernate.boot.jaxb.mapping.JaxbEmbeddable; @@ -26,6 +30,7 @@ import org.hibernate.boot.jaxb.mapping.JaxbEntityListener; import org.hibernate.boot.jaxb.mapping.JaxbEntityListeners; import org.hibernate.boot.jaxb.mapping.JaxbEntityMappings; +import org.hibernate.boot.jaxb.mapping.JaxbId; import org.hibernate.boot.jaxb.mapping.JaxbMappedSuperclass; import org.hibernate.boot.jaxb.mapping.JaxbPersistenceUnitDefaults; import org.hibernate.boot.jaxb.mapping.JaxbPersistenceUnitMetadata; @@ -60,6 +65,7 @@ */ public final class JpaJandexScavenger { + public static final List ID_ATTRIBUTE_ANNOTATIONS = Arrays.asList(ClassNames.ID, ClassNames.EMBEDDED_ID); public static final List EMBEDDED_ANNOTATIONS = Arrays.asList(ClassNames.EMBEDDED_ID, ClassNames.EMBEDDED); private static final String XML_MAPPING_DEFAULT_ORM_XML = "META-INF/orm.xml"; @@ -110,6 +116,27 @@ public JpaModelBuildItem discoverModelAndRegisterForReflection() throws BuildExc reflectiveClass.produce(ReflectiveClassBuildItem.builder(className).methods().fields().build()); } + // Creating an array of IDs doesn't seem far-fetched; Hibernate ORM seems to be doing it in a few places. + // Ideally it'd use Object[] in these cases, but we're not quite there yet. + // See https://hibernate.atlassian.net/browse/HHH-16809 + // and https://hibernate.atlassian.net/browse/HHH-18976, + // which upon fixing may or may not make this unnecessary. + for (String className : collector.idTypes) { + reflectiveClass.produce(ReflectiveClassBuildItem.builder(className + "[]").constructors().build()); + } + + // Workaround for https://hibernate.atlassian.net/browse/HHH-16809 + // Workaround for https://hibernate.atlassian.net/browse/HHH-18976 + // Interestingly, when using @IdClass, + // the follow code seems to result in creating an array whose element type is the entity type... ? + // https://github.com/hibernate/hibernate-orm/blob/1aac8c356ecbd07adad035cb402bd97e8025e43c/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/AbstractMultiIdEntityLoader.java#L49 + // So, we'll assume in the case of IdClass, the entity type is sometimes used as the ID type. + // Note a relevant test in Quarkus is the one involving io.quarkus.it.jpa.generics.SnapshotEventEntry: + // it fails without this piece of code + for (String className : collector.entityTypes) { + reflectiveClass.produce(ReflectiveClassBuildItem.builder(className + "[]").constructors().build()); + } + if (!collector.enumTypes.isEmpty()) { reflectiveClass.produce(ReflectiveClassBuildItem.builder("java.lang.Enum").methods().build()); for (String className : collector.enumTypes) { @@ -225,9 +252,31 @@ private void enlistOrmXmlMappingManagedClass(Collector collector, String package enlistIdClass(collector, DotName.createSimple(qualifyIfNecessary(packagePrefix, idClass.getClazz()))); } + enlistOrmXmlMappingAttributes(collector, packagePrefix, entityClassName, managed.getAttributes()); + enlistOrmXmlMappingListeners(collector, packagePrefix, managed.getEntityListeners()); } + private void enlistOrmXmlMappingAttributes(Collector collector, String packagePrefix, String entityClassName, + AttributesContainer attributes) { + if (attributes instanceof JaxbAttributes) { + for (JaxbId idAttribute : ((JaxbAttributes) attributes).getId()) { + ClassInfo classInfo = index.getClassByName(entityClassName); + collectAttributeTypes(collector.idTypes::add, classInfo, idAttribute.getName()); + } + } + } + + private void collectAttributeTypes(Consumer typeCollector, ClassInfo classInfo, String name) { + // We'll look for all possible sources of information; + // most likely only one will match, and worst case adding too many types won't change much to image size. + var method = classInfo.method(JavaBeanUtil.getGetterName(name, "Z")); + if (method != null) { + typeCollector.accept(getRawTypeName(method.returnType())); + } + + } + private void enlistOrmXmlMappingListeners(Collector collector, String packagePrefix, JaxbEntityListeners entityListeners) { if (entityListeners == null) { return; @@ -372,6 +421,36 @@ private void enlistEmbeddedsAndElementCollections(Collector collector) throws Bu } } + private void enlistIdTypes(Collector collector) throws BuildException { + Set embeddedTypes = new HashSet<>(); + + for (DotName idAnnotation : ID_ATTRIBUTE_ANNOTATIONS) { + for (AnnotationInstance annotation : index.getAnnotations(idAnnotation)) { + AnnotationTarget target = annotation.target(); + + switch (target.kind()) { + case FIELD: + var field = target.asField(); + collectIdAttributeType(embeddedTypes, field.type()); + break; + case METHOD: + var method = target.asMethod(); + if (method.isBridge()) { + // Generated by javac for covariant return type override. + // There's another method with a more specific return type, ignore this one. + continue; + } + collectIdAttributeType(embeddedTypes, method.returnType()); + break; + default: + throw new IllegalStateException( + "[internal error] " + idAnnotation + " placed on a unknown element: " + target); + } + + } + } + } + private void enlistJPAModelAnnotatedPackages(Collector collector, DotName dotName) { Collection jpaAnnotations = index.getAnnotations(dotName); @@ -422,6 +501,7 @@ private void enlistJPAModelIdClasses(Collector collector, DotName dotName) { private void enlistIdClass(Collector collector, DotName idClass) { addClassHierarchyToReflectiveList(collector, idClass); collector.modelTypes.add(idClass.toString()); + collector.idTypes.add(idClass.toString()); } private void enlistPotentialCdiBeanClasses(Collector collector, DotName dotName) { @@ -519,19 +599,23 @@ private static void collectModelType(Collector collector, ClassInfo modelClass) } } - private void collectEmbeddedType(Set embeddedTypes, ClassInfo declaringClass, - Declaration attribute, Type attributeType, boolean validate) - throws BuildException { - DotName className; + private void collectIdAttributeType(Set idTypes, Type attributeType) { switch (attributeType.kind()) { case CLASS: - className = attributeType.asClassType().name(); - break; case PARAMETERIZED_TYPE: - className = attributeType.name(); + idTypes.add(attributeType.name()); break; default: // do nothing + } + } + + private void collectEmbeddedType(Set embeddedTypes, ClassInfo declaringClass, + Declaration attribute, Type attributeType, boolean validate) + throws BuildException { + DotName className = getRawTypeName(attributeType); + if (className == null) { + // do nothing return; } if (validate && !index.getClassByName(className).hasAnnotation(ClassNames.EMBEDDABLE)) { @@ -568,6 +652,19 @@ private void collectElementCollectionTypes(Set embeddedTypes, ClassInfo } } + private String getRawTypeName(Type type) { + switch (attributeType.kind()) { + case CLASS: + className = attributeType.asClassType().name(); + break; + case PARAMETERIZED_TYPE: + className = attributeType.name(); + break; + default: + return null; + } + } + private static boolean isIgnored(DotName classDotName) { String className = classDotName.toString(); if (className.startsWith("java.util.") || className.startsWith("java.lang.") @@ -590,6 +687,7 @@ private static boolean isInJavaPackage(DotName classDotName) { private static class Collector { final Set packages = new HashSet<>(); final Set entityTypes = new HashSet<>(); + final Set idTypes = new HashSet<>(); final Set potentialCdiBeanTypes = new HashSet<>(); final Set modelTypes = new HashSet<>(); final Set enumTypes = new HashSet<>();