Skip to content

Native query EntityManager.createNativeQuery(…, EntityType.class) returns different result when run via EntityManager.createNativeQuery(…) #3805

Closed as not planned
@filpano

Description

@filpano

I have the following PostgreSQL table DDL:

                                   Partitioned table "service.my_values"
       Column       |            Type             | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
--------------------+-----------------------------+-----------+----------+---------+---------+-------------+--------------+-------------
 hour               | timestamp(6) with time zone |           | not null |         | plain   |             |              | 
 client_id          | uuid                        |           | not null |         | plain   |             |              | 
 number_of_invoices | bigint                      |           | not null |         | plain   |             |              | 
 gross_revenue      | numeric(18,3)               |           | not null |         | main    |             |              | 
Partition key: RANGE (hour)
Indexes:
    "my_values_pkey" PRIMARY KEY, btree (client_id, hour)
Partitions: ... (monthly partitions for a given time frame)

And the following data (note: uuid is random):

INSERT INTO service.my_values (hour, client_id, number_of_invoices, gross_revenue)
VALUES
('2024-03-19T00:00:00Z', 'b5aeae94-5e69-4775-a8ea-b5265619cda7', 1, 1),
('2024-03-19T11:00:00Z', 'b5aeae94-5e69-4775-a8ea-b5265619cda7', 1, 1),
('2024-03-20T01:00:00Z', 'b5aeae94-5e69-4775-a8ea-b5265619cda7', 1, 1),
('2024-03-20T04:00:00Z', 'b5aeae94-5e69-4775-a8ea-b5265619cda7', 1, 1);

When I run the following native query repository method:

@Repository
public interface MyRepository extends JpaRepository<MyValue, UUID> {

    /**
     * @implNote JPA behaviour seems broken when grouping by the {@code date_trunc} function,
     * hence we are grouping on the second select "column".
     */
    @Query(value = """
            SELECT
                client_id,
                date_trunc(:#{#unitOfTime.name()}, hour AT TIME ZONE 'UTC' - (INTERVAL '1 hour') * :offset) AS hour,
                SUM(gross_revenue) AS gross_revenue,
                SUM(number_of_invoices) AS number_of_invoices
            FROM
                service.my_values
            WHERE
                client_id = :clientId
                AND hour >= :from
                AND hour < :until
            GROUP BY
                client_id,
                2
            ORDER BY
                2 ASC;
            """, nativeQuery = true)
    Stream<MyValue> findMyValues(
            @Param("clientId") UUID clientId,
            @Param("from") Instant from,
            @Param("until") Instant until,
            @Param("offset") int offset,
            @Param("unitOfTime") UnitOfTime unitOfTime);
}

I expect to get the value: MyValue(clientId=b5aeae94-5e69-4775-a8ea-b5265619cda7, hour=2024-03-19T00:00:00Z, grossRevenue=2, numberOfInvoices=2).

The actual value I get is: MyValue(clientId=b5aeae94-5e69-4775-a8ea-b5265619cda7, hour=2024-03-19T00:00:00Z, grossRevenue=1, numberOfInvoices=1).

If I run the same query using entityManager.createNativeQuery() (note: SPeL part switched for a string as createNativeQuery does not seem to support it):

        var query = entityManager.createNativeQuery("""
            SELECT
                client_id,
                date_trunc(:unitOfTimeString, hour AT TIME ZONE 'UTC' - (INTERVAL '1 hour') * :offset) AS hour,
                SUM(gross_revenue) AS gross_revenue,
                SUM(number_of_invoices) AS number_of_invoices
            FROM
                service.my_values
            WHERE
                client_id = :clientId
                AND hour >= :from
                AND hour < :until
            GROUP BY
                client_id,
                2
            ORDER BY
                2 ASC;
            """)
                .setParameter("clientId", clientId)
                .setParameter("from", timeSpan.from())
                .setParameter("until", timeSpan.until())
                .setParameter("offset", timeUtils.getCurrentTZOffset(dt_2024_03_19T10_00))
                .setParameter("unitOfTimeString", DAY.name());

        var emResult = query.getResultList();

        assertThat(emResult).hasSize(1);

        Object[] row = (Object[]) emResult.get(0);
        assertThat(row[0]).isEqualTo(clientId);
        assertThat(row[1]).isEqualTo(Timestamp.from(expectedDayBoundary));
        assertThat(row[2]).isEqualTo(BigDecimal.valueOf(2));
        assertThat(row[3]).isEqualTo(BigDecimal.valueOf(2));

Then I get the expected result. If I run the same query in the console (using e.g. psql), I also get the expected result.

Using spring.jpa.show-sql=true and spring.jpa.properties.hibernate.format_sql=true, I see the following SQL call logged:

Hibernate: 
    /* dynamic native SQL query */ SELECT
        client_id,
        date_trunc(?, hour AT TIME ZONE 'UTC' - (INTERVAL '1 hour') * ?) AS hour,
        SUM(gross_revenue) AS gross_revenue,
        SUM(number_of_invoices) AS number_of_invoices 
    FROM
        service.my_values 
    WHERE
        client_id = ?     
        AND hour >= ?     
        AND hour < ? 
    GROUP BY
        client_id,
        2 
    ORDER BY
        2 ASC; 
2025-03-07 17:05:26.311 TRACE 1007964 --- [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [DAY]
2025-03-07 17:05:26.311 TRACE 1007964 --- [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (2:INTEGER) <- [3]
2025-03-07 17:05:26.311 TRACE 1007964 --- [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (3:UUID) <- [b5aeae94-5e69-4775-a8ea-b5265619cda7]
2025-03-07 17:05:26.311 TRACE 1007964 --- [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (4:TIMESTAMP_UTC) <- [2024-03-19T03:00:00Z]
2025-03-07 17:05:26.312 TRACE 1007964 --- [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (5:TIMESTAMP_UTC) <- [2024-03-20T02:59:59.999999999Z]

which is exactly the query I was expecting.

Here are my entity definitions:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@IdClass(MyKey.class)
@Table(name = "my_values")
public class MyValue {

    @Id
    @JsonIgnore
    private UUID clientId;
    @Id
    private Instant hour;
    @Column(precision = BigDecimalColumnDef.PRECISION, scale = BigDecimalColumnDef.SCALE)
    private BigDecimal grossRevenue;
    private long numberOfInvoices;
}

Id class:

@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class MyKey {
    private UUID clientId;
    private Instant hour;
}

And this is how the failing test which contains this issue creates the database entries:

List<MyValue> entries = List.of(
        /* not in day */ new MyValue(clientId, dt_2024_03_19T01_00, BigDecimal.ONE, 1L),
        new MyValue(clientId, dt_2024_03_19T12_00, BigDecimal.ONE, 1L),
        new MyValue(clientId, dt_2024_03_20T02_00, BigDecimal.ONE, 1L),
        /* not in day */ new MyValue(clientId, dt_2024_03_20T05_00, BigDecimal.ONE, 1L)
);
repository.saveAll(entries);
repository.flush();

(note to avoid confusion: the datetime variables represent times in a +01:00 time zone, but are Instants that match the above UTC times in the INSERT statements)


Seeing how:

  1. JPA generates the query I was expecting, and
  2. The native query via entityManager.createNativeQuery() returns the correct results

this seems to be Spring Data JPA issue.

I have almost exactly the same query (using different tables with very similar structure) in different repositories which works, which is why I'm a bit baffled at this behaviour. Another query that looks practically identical, is as follows:

    @Query(value = """
            SELECT
                client_id,
                date_trunc(:#{#unitOfTime.name()}, hour AT TIME ZONE 'UTC' - (INTERVAL '1 hour') * :offset) AS hour,
                SUM(gross_revenue) AS gross_revenue,
                SUM(net_revenue) AS net_revenue
            FROM
                service.my_other_values
            WHERE
                client_id = :clientId
                AND hour >= :from
                AND hour < :until
            GROUP BY
                client_id,
                2
            ORDER BY
                2 ASC;
            """, nativeQuery = true)
    Stream<MyOtherValue> findOtherValues(
        @Param("clientId") UUID clientId,
        @Param("from") Instant from,
        @Param("until") Instant until,
        @Param("offset") int offset,
        @Param("unitOfTime") UnitOfTime unitOfTime);

with the following entity:

@Data
@NoArgsConstructor // Required by JPA
@AllArgsConstructor
@Entity
@IdClass(MyOtherKey.class)
@Table(name = "my_other_values")
@NamedNativeQueries({ /* named native queries used for other methods */ })
public class MyOtherValue {

    @Id
    @JsonIgnore
    private UUID clientId;
    @Id
    private Instant hour;
    @Column(precision = BigDecimalColumnDef.PRECISION, scale = BigDecimalColumnDef.SCALE)
    private BigDecimal grossRevenue;
    @Column(precision = BigDecimalColumnDef.PRECISION, scale = BigDecimalColumnDef.SCALE)
    private BigDecimal netRevenue;

}

and Id class:

@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class MyOtherKey {
    private UUID clientId;
    private Instant hour;
}

Unfortunately, I'm not sure how to debug this as JPA/Hibernate stacks are very deep and difficult (IMO) to debug unless you have a lot of experience with it.

I unfortunately cannot provide a reproducible sample at this time, but if that's the only way to move forward, I can spend some time next week preparing an MRP. In the meantime, if there's anything I could potentially try, I would appreciate it.

Metadata

Metadata

Assignees

Labels

for: external-projectFor an external project and not something we can fix

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions