Skip to content

Interface projection fails with AliasCollisionException using the same leaf property name #4135

@oualidbouh

Description

@oualidbouh

Description

After upgrading to Spring Boot 4.0 (Spring Data JPA 4.0, Hibernate 7), interface-based projections using the documented flattened property naming convention fail with AliasCollisionException when the projection traverses multiple relationships that share property names (e.g., id, name).

This is a regression from Spring Boot 3.x where this feature worked correctly.

The root cause: Spring Data JPA generates SQL aliases using only the last segment of the property path (e.g., id) instead of the full flattened property name derived from the getter (e.g., customerId). Hibernate 7 now correctly enforces SQL standards and rejects duplicate aliases, exposing this issue.

Reproduction Repository

spring-data-jpa-alias-collision-bug

git clone https://github.com/oualidbouh/spring-data-jpa-alias-collision-bug.git
cd spring-data-jpa-alias-collision-bug
./mvnw test

Versions

Dependency Version
Spring Boot 4.0.0
Spring Data JPA 4.0.x
Hibernate 7.0.x
Java 21

Minimal Example

Entities

@Entity
@Table(name = "orders")
public class Order {
    @Id
    private Long id;
    
    @ManyToOne
    private Customer customer;
    
    @ManyToOne
    private Product product;
    
    @ManyToOne
    private Shipment shipment;
}

@Entity
public class Customer {
    @Id
    private Long id;
    private String name;
}

@Entity
public class Product {
    @Id
    private Long id;
    private String name;
}

@Entity
public class Shipment {
    @Id
    private Long id;
}

Interface Projection (following documented naming convention)

public interface OrderSummaryProjection {
    Long getId();
    Long getCustomerId();       // → should map to customer.id
    Long getProductId();        // → should map to product.id
    Long getShipmentId();       // → should map to shipment.id
    String getCustomerName();   // → should map to customer.name
    String getProductName();    // → should map to product.name
}

Repository

public interface OrderRepository extends JpaRepository {
    OrderSummaryProjection findProjectionById(Long id);
}

Expected Behavior

Spring Data JPA should generate a query with unique aliases matching the flattened property names:

SELECT 
    o.id AS id,
    c.id AS customerId,
    p.id AS productId,
    s.id AS shipmentId,
    c.name AS customerName,
    p.name AS productName
FROM Order o
LEFT JOIN o.customer c
LEFT JOIN o.product p
LEFT JOIN o.shipment s
WHERE o.id = ?

Actual Behavior

Spring Data JPA generates duplicate aliases using only the last path segment:

SELECT 
    o.id AS id,
    c.id AS id,        -- Duplicate alias
    p.id AS id,        -- Duplicate alias
    s.id AS id,        -- Duplicate alias
    c.name AS name,    -- Duplicate alias
    p.name AS name     -- Duplicate alias
FROM Order o
LEFT JOIN o.customer c
LEFT JOIN o.product p
LEFT JOIN o.shipment s
WHERE o.id = ?

Exception

org.springframework.data.jpa.repository.query.BadJpqlGrammarException: 
org.hibernate.query.sqm.AliasCollisionException: Duplicate alias 'id' at position 1 in 'select' clause;
Bad JPQL grammar [SELECT o.id id, c.id id, p.id id, s.id id, c.name name, p.name name 
FROM Order o LEFT JOIN o.customer c LEFT JOIN o.product p LEFT JOIN o.shipment s WHERE o.id = :id]

Root Cause Analysis

Getter Property Path Current Alias Expected Alias
getId() id id id
getCustomerId() customer.id id customerId
getProductId() product.id id productId
getShipmentId() shipment.id id shipmentId
getCustomerName() customer.name name customerName
getProductName() product.name name productName

This worked silently in Hibernate 5/6 which tolerated duplicate aliases. Hibernate 7 correctly validates aliases per SQL standards and rejects duplicates via AliasCollisionException.

Debugging Analysis

Step 1: Path Expressions Are Built Correctly

In JpaQueryCreator.doSelect(), the paths collection contains correctly resolved path expressions:

path origin
Order.id Entity[entity=Order, alias=o]
Customer.id Join[source=Entity[Order], joinType=LEFT JOIN, path=customer]
Product.id Join[source=Entity[Order], joinType=LEFT JOIN, path=product]
Shipment.id Join[source=Entity[Order], joinType=LEFT JOIN, path=shipment]
Customer.name Join[source=Entity[Order], joinType=LEFT JOIN, path=customer]
Product.name Join[source=Entity[Order], joinType=LEFT JOIN, path=product]

Step 2: The Problem Occurs During Rendering

In JpqlQueryBuilder.Multiselect.render():

record Multiselect(Origin source, Collection paths) implements Selection {

    @Override
    public String render(RenderContext context) {
        StringBuilder builder = new StringBuilder();
        for (Expression path : paths) {
            if (!builder.isEmpty()) {
                builder.append(", ");
            }
            builder.append(path.render(context));
            if (!context.isConstructorContext() && path instanceof AliasedExpression ae) {
                builder.append(" ").append(ae.getAlias());  // Returns "id" not "customerId"
            }
        }
        return builder.toString();
    }
}

The ae.getAlias() returns only the column name (id, name) instead of the full flattened property name (customerId, customerName).

Step 3: Generated Query Shows Duplicate Aliases

SELECT o.id id, c.id id, p.id id, s.id id, c.name name, p.name name FROM Order o LEFT JOIN ...

Selection breakdown: ".id id, .id id, .id id, .id id, .name name, .name name"

Unit Test for JpaQueryCreatorTests

private static final TestMetaModel ORDER_WITH_RELATIONS = TestMetaModel.hibernateModel(
        OrderWithRelations.class, Customer.class, Supplier.class);

@Test // GH-XXXX
void interfaceProjectionWithMultipleJoinsShouldGenerateUniqueAliases() {

    queryCreator(ORDER_WITH_RELATIONS) //
            .forTree(OrderWithRelations.class, "findProjectionById") //
            .returning(OrderSummaryProjection.class) //
            .withParameters(1L) //
            .as(QueryCreatorTester::create) //
            .expectJpql(
                    "SELECT o.id id, c.id customerId, s.id supplierId, c.name customerName, s.name supplierName FROM %s o LEFT JOIN o.customer c LEFT JOIN o.supplier s WHERE o.id = ?1",
                    DefaultJpaEntityMetadata.unqualify(OrderWithRelations.class)) //
            .validateQuery();
}

@jakarta.persistence.Entity
static class OrderWithRelations {
    @Id Long id;
    @ManyToOne Customer customer;
    @ManyToOne Supplier supplier;
}

@jakarta.persistence.Entity
static class Customer {
    @Id Long id;
    String name;
}

@jakarta.persistence.Entity
static class Supplier {
    @Id Long id;
    String name;
}

interface OrderSummaryProjection {
    Long getId();
    Long getCustomerId();      // → customer.id
    Long getSupplierId();      // → supplier.id
    String getCustomerName();  // → customer.name
    String getSupplierName();  // → supplier.name
}

Proposed Fix

In JpaQueryCreator.doSelect(), explicitly set the alias using the selection variable which contains the correct flattened property name:

Current code (broken):

for (String selection : requiredSelection) {
    paths.add(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
          PropertyPath.from(selection, returnedType.getDomainType()), true));
}

Fixed code:

for (String selection : requiredSelection) {
    JpqlQueryBuilder.Expression expr = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
          PropertyPath.from(selection, returnedType.getDomainType()), true);
    paths.add(expr.as(selection));  // ← Explicitly set alias to "customerId", "productId", etc.
}

The selection variable already contains the correct alias (customerId, productId, customerName, etc.) — it just needs to be explicitly set on the expression via .as(selection).

References


I have submitted a pull request with the proposed fix and unit test.

Metadata

Metadata

Assignees

Labels

type: regressionA regression from a previous release

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions