diff --git a/orm/hibernate-orm-6/pom.xml b/orm/hibernate-orm-6/pom.xml index 9712e85b..8e84e3f2 100644 --- a/orm/hibernate-orm-6/pom.xml +++ b/orm/hibernate-orm-6/pom.xml @@ -79,7 +79,7 @@ maven-compiler-plugin 3.14.0 - 11 + 21 diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java index eefb7df4..2a9b7bbe 100644 --- a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java @@ -15,6 +15,11 @@ */ package org.hibernate.bugs; +import jakarta.persistence.criteria.CriteriaBuilder; +import org.assertj.core.api.Assertions; +import org.hibernate.bugs.application.InstrumentData; +import org.hibernate.bugs.application.InstrumentQueryService; +import org.hibernate.bugs.domain.model.*; import org.hibernate.cfg.AvailableSettings; import org.hibernate.testing.orm.junit.DomainModel; @@ -24,6 +29,11 @@ import org.hibernate.testing.orm.junit.Setting; import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + /** * This template demonstrates how to develop a test case for Hibernate ORM, using its built-in unit test framework. * Although ORMStandaloneTestCase is perfectly acceptable as a reproducer, usage of this class is much preferred. @@ -36,8 +46,8 @@ @DomainModel( annotatedClasses = { // Add your entities here. - // Foo.class, - // Bar.class + Instrument.class, + OrdinaryShare.class }, // If you use *.hbm.xml mappings, instead of annotations, add the mappings here. xmlMappings = { @@ -62,9 +72,45 @@ class ORMUnitTestCase { // Add your tests, using standard JUnit 5. @Test - void hhh123Test(SessionFactoryScope scope) throws Exception { + void hhh19542Test(SessionFactoryScope scope) throws Exception { scope.inTransaction( session -> { - // Do stuff... + List instruments = session.createQuery("from Instrument ").list(); + System.out.println(instruments); } ); } + + @Test + void hhh19543Test(SessionFactoryScope scope) throws Exception { + final InstrumentQueryService instrumentQueryService = new InstrumentQueryService(); + + scope.inTransaction( session -> { + Instrument instrument = new OrdinaryShare( + new InstrumentCode("AVGO"), + "Equity", + "Broadcom", + Arrays.asList( + new InstrumentLine( + new InstrumentLineKey("AVGO:XPAR"), + new CurrencyCode("EUR"), + "Broadcom Equity Paris"), + new InstrumentLine( + new InstrumentLineKey("AVGO:XMIL"), + new CurrencyCode("EUR"), + "Broadcom Equity Milan"), + new InstrumentLine( + new InstrumentLineKey("AVGO:NYSE"), + new CurrencyCode("USD"), + "Broadcom Equity New York Stock Exchange"))); + + // Add any instruments you want here, purpose is to show aggregation by "code" (JoinOn) + + session.persist(instrument); + + List instruments = instrumentQueryService.allInstruments(session); + + assertThat( instruments ).isNotEmpty(); + assertThat( instruments.getFirst().lineKeys() ).isNotEmpty(); + assertThat( instruments.getFirst().lineKeys().size() == 3 ).isTrue(); + }); + } } diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/application/InstrumentData.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/application/InstrumentData.java new file mode 100644 index 00000000..9275f2a2 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/application/InstrumentData.java @@ -0,0 +1,17 @@ +package org.hibernate.bugs.application; + +import java.util.List; + +public record InstrumentData( + String code, + String category, + String description, + List lineKeys) { + + public record LineKeyData( + String id, + String currencyCode, + String description) { + + } +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/application/InstrumentQueryService.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/application/InstrumentQueryService.java new file mode 100644 index 00000000..f1d9ed3b --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/application/InstrumentQueryService.java @@ -0,0 +1,31 @@ +package org.hibernate.bugs.application; + +import jakarta.transaction.Transactional; +import org.hibernate.Session; +import org.hibernate.bugs.domain.model.Instrument; +import org.hibernate.bugs.domain.model.InstrumentLine; +import org.hibernate.bugs.port.adapter.persistence.hibernate.HibernateResultListTransformer; +import org.hibernate.bugs.port.adapter.persistence.hibernate.JoinOn; +import org.hibernate.jpa.spi.NativeQueryTupleTransformer; + +import java.util.List; + +public class InstrumentQueryService { + + @Transactional + public List allInstruments(final Session session) { + return session.createQuery(""" + select instrument.instrumentCode.id as code, + instrument.category as category, + instrument.description as description, + instrumentLines.key.id as line_keys_id, + instrumentLines.currencyCode.code as line_keys_currency_code, + instrumentLines.description as line_keys_description + from Instrument instrument + join instrument.instrumentLines instrumentLines + order by code""") + .setTupleTransformer(new NativeQueryTupleTransformer()) + .setResultListTransformer(new HibernateResultListTransformer(new JoinOn("code"), InstrumentData.class)) + .list(); + } +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/CreditDerivative.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/CreditDerivative.java new file mode 100644 index 00000000..a0b6de38 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/CreditDerivative.java @@ -0,0 +1,12 @@ +package org.hibernate.bugs.domain.model; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +@Embeddable +public record CreditDerivative( + @Column(name = "INDEX_SUB_FAMILY", table = "INSTRUMENT_CREDIT_DERIVATIVE") + String indexSubFamily, + CurrencyCode currencyCode) { +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/CurrencyCode.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/CurrencyCode.java new file mode 100644 index 00000000..a0606bf7 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/CurrencyCode.java @@ -0,0 +1,17 @@ +package org.hibernate.bugs.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +@Embeddable +public record CurrencyCode( + @Column(name = "CURRENCY_CODE") + String code) { + + public CurrencyCode { + if (code == null || code.trim().isEmpty()) throw new IllegalArgumentException("Currency code must be provided."); + if (code.trim().length() != 3) throw new IllegalArgumentException("Currency code must have 3 characters."); + + code = code.trim().toUpperCase().intern(); + } +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/Identity.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/Identity.java new file mode 100644 index 00000000..8b7ae214 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/Identity.java @@ -0,0 +1,6 @@ +package org.hibernate.bugs.domain.model; + +public interface Identity { + + String id(); +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/Instrument.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/Instrument.java new file mode 100644 index 00000000..cc243c43 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/Instrument.java @@ -0,0 +1,44 @@ +package org.hibernate.bugs.domain.model; + +import jakarta.persistence.*; +import org.hibernate.annotations.NaturalId; + +import java.util.List; + +@Entity +@Table(name = "INSTRUMENT") +@DiscriminatorColumn(name = "INSTRUMENT_TYPE") +@SecondaryTable(name = "INSTRUMENT_CREDIT_DERIVATIVE", pkJoinColumns = @PrimaryKeyJoinColumn(name = "INSTRUMENT_ID", referencedColumnName = "ID")) +public abstract class Instrument { + + @Id + @Column(name = "ID") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NaturalId + private InstrumentCode instrumentCode; + + @Column(name = "CATEGORY") + private String category; + + @Column(name = "DESCRIPTION") + private String description; + + private CreditDerivative creditDerivative; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "INSTRUMENT_LINE", joinColumns = @JoinColumn(name = "INSTRUMENT_ID", referencedColumnName = "ID")) + private List instrumentLines; + + protected Instrument(final InstrumentCode instrumentCode, final String category, final String description, final List instrumentLines) { + this.instrumentCode = instrumentCode; + this.category = category; + this.description = description; + this.instrumentLines = instrumentLines; + } + + protected Instrument() { + + } +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/InstrumentCode.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/InstrumentCode.java new file mode 100644 index 00000000..b8c60d37 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/InstrumentCode.java @@ -0,0 +1,12 @@ +package org.hibernate.bugs.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +import java.io.Serializable; + +@Embeddable +public record InstrumentCode( + @Column(name = "INSTRUMENT_CODE") + String id) implements Identity, Serializable { +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/InstrumentLine.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/InstrumentLine.java new file mode 100644 index 00000000..075f1141 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/InstrumentLine.java @@ -0,0 +1,12 @@ +package org.hibernate.bugs.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +@Embeddable +public record InstrumentLine( + InstrumentLineKey key, + CurrencyCode currencyCode, + @Column(name = "DESCRIPTION") + String description) { +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/InstrumentLineKey.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/InstrumentLineKey.java new file mode 100644 index 00000000..00cedd70 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/InstrumentLineKey.java @@ -0,0 +1,12 @@ +package org.hibernate.bugs.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +import java.io.Serializable; + +@Embeddable +public record InstrumentLineKey( + @Column(name = "LINE_KEY") + String id) implements Identity, Serializable { +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/OrdinaryShare.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/OrdinaryShare.java new file mode 100644 index 00000000..afaec154 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/domain/model/OrdinaryShare.java @@ -0,0 +1,23 @@ +package org.hibernate.bugs.domain.model; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +import java.util.List; + +@Entity +@DiscriminatorValue("ORD") +public class OrdinaryShare extends Instrument { + + public OrdinaryShare( + final InstrumentCode instrumentCode, + final String category, + final String description, + final List instrumentLines) { + super(instrumentCode, category, description, instrumentLines); + } + + protected OrdinaryShare() { + super(); + } +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/port/adapter/persistence/hibernate/HibernateObjectMapper.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/port/adapter/persistence/hibernate/HibernateObjectMapper.java new file mode 100644 index 00000000..100a31a6 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/port/adapter/persistence/hibernate/HibernateObjectMapper.java @@ -0,0 +1,257 @@ +package org.hibernate.bugs.port.adapter.persistence.hibernate; + +import jakarta.persistence.TupleElement; +import org.hibernate.bugs.port.adapter.persistence.hibernate.HibernateResultListTransformer.ResultCursor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.util.*; + +final class HibernateObjectMapper { + + private final JoinOn joinOn; + private final ResultCursor resultCursor; + private final String aliasPrefix; + private final Class resultType; + + HibernateObjectMapper( + final JoinOn joinOn, + final ResultCursor resultCursor, + final Class resultType) { + + this.joinOn = joinOn; + this.resultCursor = resultCursor; + this.resultType = resultType; + this.aliasPrefix = null; + } + + HibernateObjectMapper( + final JoinOn joinOn, + final ResultCursor resultCursor, + final String aliasPrefix, + final Class resultType) { + + this.joinOn = joinOn; + this.resultCursor = resultCursor; + this.resultType = resultType; + this.aliasPrefix = aliasPrefix; + } + + T mapResultToType() { + + final Map associationsToMap = new HashMap<>(); + + final Parameter[] parameters = this.resultTypeParameters(); + + final List parameterValues = new ArrayList<>(parameters.length); + + for (Parameter parameter : parameters) { + String aliasName = this.parameterToAliasName(parameter.getName()); + + if (this.hasAlias(this.cursor(), aliasName)) { + Object parameterValue = this.parameterValueFor(aliasName, parameter.getType()); + + parameterValues.add(parameterValue); + } else { + String objectPrefix = this.toObjectPrefix(aliasName); + + if (!associationsToMap.containsKey(objectPrefix) && + this.hasAssociation(this.cursor(), objectPrefix)) { + + associationsToMap.put(parameter.getName(), parameter); + } + } + } + + if (!associationsToMap.isEmpty()) { + this.mapAssociations(parameterValues, this.cursor(), associationsToMap.values()); + } + + return this.createFrom(parameterValues); + } + + private String aliasPrefix() { + return this.aliasPrefix; + } + + private boolean hasAliasPrefix() { + return this.aliasPrefix != null; + } + + private Object parameterValueFor(final String aliasName, final Class type) { + Object value; + + try { + value = this.cursor().get(aliasName); + + if (value == null && type.isPrimitive()) { + throw new IllegalArgumentException("Cannot assign null value to primitive type."); + } + + if (value != null && !type.isAssignableFrom(value.getClass())) { + throw new IllegalArgumentException("Cannot assign " + value.getClass() + " to " + type); + } + + } catch (Exception e) { + throw new IllegalArgumentException("Unable to map " + aliasName, e); + } + + return value; + } + + private Parameter[] resultTypeParameters() { + return this.resultType().getDeclaredConstructors()[0].getParameters(); + } + + private Collection createCollectionFrom(final Class type) { + Collection newCollection = null; + + if (List.class.isAssignableFrom(type)) { + newCollection = new ArrayList<>(); + } else if (Set.class.isAssignableFrom(type)) { + newCollection = new HashSet<>(); + } + + return newCollection; + } + + @SuppressWarnings("unchecked") + private T createFrom(final List parameterValues) { + try { + return (T) this.resultType().getDeclaredConstructors()[0].newInstance(parameterValues.toArray()); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException("Unable to create instance of: " + this.resultType().getName(), e); + } + } + + private String parameterToAliasName(final String parameterName) { + final StringBuilder builder = new StringBuilder(); + + if (this.hasAliasPrefix()) { + builder.append(this.aliasPrefix()); + } + + for (char ch : parameterName.toCharArray()) { + if (Character.isAlphabetic(ch) && Character.isUpperCase(ch)) { + builder.append('_').append(Character.toLowerCase(ch)); + } else { + builder.append(ch); + } + } + + return builder.toString(); + } + + private boolean hasAssociation(final ResultCursor resultCursor, final String objectPrefix) { + try { + + for (TupleElement metaData : resultCursor.getElements()) { + String aliasName = metaData.getAlias(); + + if (aliasName.startsWith(objectPrefix) && + this.joinOn().isJoinedOn(resultCursor)) { + + return true; + } + } + } catch (Exception e) { + throw new IllegalStateException("Unable to read metadata.", e); + } + + return false; + } + + private boolean hasAlias(final ResultCursor resultCursor, final String aliasName) { + try { + resultCursor.get(aliasName); + + return true; + } catch (Exception e) { + return false; + } + } + + private JoinOn joinOn() { + return this.joinOn; + } + + private void mapAssociations( + final List parameterValues, + final ResultCursor resultCursor, + final Collection associationsToMap) { + + final Map> mappedCollections = new HashMap<>(); + + String currentAssociationName = null; + + try { + this.joinOn().saveCurrentLeftQualifier(resultCursor); + + for (boolean hasResult = true; hasResult; hasResult = resultCursor.next()) { + + if (!this.joinOn().hasCurrentLeftQualifier(resultCursor)) { + resultCursor.previous(); + + return; + } + + for (Parameter associationParameter : associationsToMap) { + + currentAssociationName = associationParameter.getName(); + + Class associationParameterType; + + Collection collection = null; + + if (Collection.class.isAssignableFrom(associationParameter.getType())) { + collection = mappedCollections.get(associationParameter.getName()); + + if (collection == null) { + collection = this.createCollectionFrom(associationParameter.getType()); + mappedCollections.put(associationParameter.getName(), collection); + parameterValues.add(collection); + } + + ParameterizedType parameterizedType = (ParameterizedType) associationParameter.getParameterizedType(); + associationParameterType = (Class) parameterizedType.getActualTypeArguments()[0]; + + } else { + associationParameterType = associationParameter.getType(); + } + + String aliasName = this.parameterToAliasName(associationParameter.getName()); + + HibernateObjectMapper mapper = + new HibernateObjectMapper<>( + this.joinOn(), + this.cursor(), + this.toObjectPrefix(aliasName), + associationParameterType); + + Object associationObject = mapper.mapResultToType(); + + if (collection != null) { + collection.add(associationObject); + } else { + parameterValues.add(associationObject); + } + } + } + } catch (Exception e) { + throw new IllegalArgumentException("Unable to map object association for " + currentAssociationName, e); + } + } + + private ResultCursor cursor() { + return this.resultCursor; + } + + private Class resultType() { + return this.resultType; + } + + private String toObjectPrefix(final String aliasName) { + return aliasName + "_"; + } +} diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/port/adapter/persistence/hibernate/HibernateResultListTransformer.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/port/adapter/persistence/hibernate/HibernateResultListTransformer.java new file mode 100644 index 00000000..cab8a9e7 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/port/adapter/persistence/hibernate/HibernateResultListTransformer.java @@ -0,0 +1,158 @@ +package org.hibernate.bugs.port.adapter.persistence.hibernate; + +import jakarta.persistence.Tuple; +import jakarta.persistence.TupleElement; +import org.hibernate.jpa.spi.NativeQueryTupleTransformer; +import org.hibernate.query.ResultListTransformer; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +/** + *

+ * {@link HibernateResultListTransformer} is a utility for mapping Hibernate query results + * (as {@link Tuple}s) to Java records or classes using constructor-based mapping. It is + * designed to work with queries that use SQL-style aliases, automatically transforming + * those aliases into Java naming conventions and grouping related fields into nested structures. + *

+ *
+ * Alias Mapping and Java Records + *

+ * This transformer maps each query alias to the corresponding constructor parameter in the + * target Java record or class. It supports alias transformation from SQL-style (snake_case) + * to Java-style (camelCase). For example, a query alias like {@code line_keys_id} will be + * mapped to the id field of a nested record named {@code lineKeys} in the target record, + * as shown below: + *

+ *
+ * public record InstrumentData(
+ *     String code,
+ *     String categoryCode,
+ *     String description,
+ *     List<LineKeyData> lineKeys) {
+ *
+ *     public record LineKeyData(
+ *         String id,
+ *         String countryCode,
+ *         String currencyCode) {}
+ * }
+ * 
+ *

Given the following query:

+ *
+ * select instrument.instrumentCode.id as code,
+ *        instrument.instrument.category.code as category_code,
+ *        instrument.instrument.description as description,
+ *        instrumentLines.lineKey.id as line_keys_id,
+ *        instrumentLines.country.code as line_keys_country_code,
+ *        instrumentLines.currency.code as line_keys_currency_code
+ *   from Instrument instrument
+ *   join instrument.lines instrumentLines
+ *  order by code
+ * 
+ *

+ * The transformer will group all fields prefixed with {@code line_keys_} into the + * {@code lineKeys} list, mapping each subfield to the corresponding property in {@code LineKeyData}. + *

+ *
+ * Importance of Ordering and JoinOn + *

+ * The {@link JoinOn} parameter (e.g., {@code new JoinOn("code")}) specifies the field + * used to group related rows (such as multiple lineKeys per instrument). The query must + * be ordered by this field ({@code order by code}) to ensure correct grouping, as the + * transformer iterates through the result set in order and groups rows based on the {@link JoinOn} alias. + *

+ *

+ * {@link JoinOn} also supports multiple fields, allowing for more complex grouping scenarios, + * when the natural id is composite for instance. In this case, the query should be ordered by + * all fields specified in the {@link JoinOn}. + *

+ *
+ * Tuple Transformation Requirement + *

+ * Due to Hibernate 6's separation of tuple and result list transformers, you must use + * {@link NativeQueryTupleTransformer} before {@link HibernateResultListTransformer}. This ensures + * the query returns results as {@link Tuple} objects, which the transformer expects. + *

+ *
+ * Example Usage + *
+ * .setTupleTransformer(new NativeQueryTupleTransformer())
+ * .setResultListTransformer(new HibernateResultListTransformer<>(new JoinOn("code"), InstrumentData.class))
+ * 
+ *

This pattern is required for correct mapping and grouping of query results into Java records.

+ *
+ * Summary: + *
    + *
  • Maps Hibernate query results to Java records or classes using constructor-based mapping.
  • + *
  • Groups related fields into nested structures based on prefixes (e.g., {@code line_keys_*} into {@code lineKeys}).
  • + *
  • Requires ordering by the {@link JoinOn} field(s) to ensure correct grouping.
  • + *
  • Must be used after {@link NativeQueryTupleTransformer} to work with {@link Tuple} results.
  • + *
+ *

This class is non thread-safe

+ */ + +public final class HibernateResultListTransformer implements ResultListTransformer { + + private final JoinOn joinOn; + private final Class resultType; + + public HibernateResultListTransformer( + final JoinOn joinOn, + final Class resultType) { + this.resultType = resultType; + this.joinOn = joinOn; + } + + @Override + public List transformList(final List collection) { + try { + final List resultList = new ArrayList<>(collection.size()); + final ResultCursor resultCursor = new ResultCursor(collection); + + while (resultCursor.next()) { + HibernateObjectMapper mapper = new HibernateObjectMapper<>( + joinOn, + resultCursor, + resultType); + + resultList.add(mapper.mapResultToType()); + } + + return resultList; + } catch (Exception e) { + throw new IllegalStateException("Unable to query object.", e); + } + } + static class ResultCursor { + + private final ListIterator iterator; + private Tuple current; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public ResultCursor(final List list) { + this.iterator = list.listIterator(); + } + + public Object get(final String alias) { + return this.current.get(alias); + } + + public boolean next() { + if (this.iterator.hasNext()) { + this.current = this.iterator.next(); + return true; + } + return false; + } + + public void previous() { + this.current = this.iterator.previous(); + } + + public List> getElements() { + return this.current.getElements(); + } + } + +} \ No newline at end of file diff --git a/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/port/adapter/persistence/hibernate/JoinOn.java b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/port/adapter/persistence/hibernate/JoinOn.java new file mode 100644 index 00000000..ef1ef741 --- /dev/null +++ b/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/port/adapter/persistence/hibernate/JoinOn.java @@ -0,0 +1,75 @@ +package org.hibernate.bugs.port.adapter.persistence.hibernate; + +import org.hibernate.bugs.port.adapter.persistence.hibernate.HibernateResultListTransformer.ResultCursor; + +import java.util.StringJoiner; + +public final class JoinOn { + + private final String[] leftAliases; + private Object currentLeftQualifier; + + public JoinOn(final String... leftAliases) { + this.leftAliases = leftAliases; + } + + boolean hasCurrentLeftQualifier(final ResultCursor resultCursor) { + try { + Object columnValue = this.getLeftQualifier(resultCursor); + + if (columnValue == null) { + return false; + } + + return columnValue.equals(this.currentLeftQualifier); + + } catch (Exception e) { + return false; + } + } + + boolean isJoinedOn(final ResultCursor resultCursor) { + String leftColumn = null; + + try { + if (this.isSpecified()) { + leftColumn = this.getLeftQualifier(resultCursor); + } + } catch (Exception e) { + // ignore + } + + return leftColumn != null && !leftColumn.isEmpty(); + } + + boolean isSpecified() { + return this.leftAliases() != null && this.leftAliases().length > 0; + } + + String[] leftAliases() { + return this.leftAliases; + } + + void saveCurrentLeftQualifier(final ResultCursor resultCursor) { + try { + this.currentLeftQualifier = this.getLeftQualifier(resultCursor); + } catch (Exception e) { + // ignore + } + } + + @Override + public String toString() { + return String.valueOf(this.currentLeftQualifier); + } + + private String getLeftQualifier(final ResultCursor resultCursor) { + final StringJoiner leftQualifier = new StringJoiner("_"); + + for (String leftAlias : this.leftAliases()) { + leftQualifier.add(String.valueOf(resultCursor.get(leftAlias))); + } + + return leftQualifier.toString(); + } +}