Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Group by date intervals using JPA's Criteria API

I'm trying to group entities by date intervals using JPA's Criteria API. I use this way of querying for entities as this is a part of the service that serves API requests which may ask for any field of any entity, including sorting, filtering, grouping and aggregations. Everything works fine except for grouping by date fields. My underlying DBMS i PostgreSQL.

To give a minimal example, here's my entity class:

@Entity
@Table(name = "receipts")
public class DbReceipt {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private Date sellDate;
    // Many other fields
}

This example discusses grouping my "month" interval (therefore grouping by year+month), but in the end I'm looking for a solution that would let me group by any interval, such as "year", "day" or "minutes".

What I'm trying to achieve is the following query, but using Criteria API:

SELECT TO_CHAR(sell_date, 'YYYY-MM') AS alias1 FROM receipts GROUP BY alias1;

My attempt to do so is this:

@Service
public class ReceiptServiceImpl extends ReceiptService {
    @Autowired
    private EntityManager em;

    @Override
    public void test() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Object[]> query = cb.createQuery(Object[].class);
        Root<?> root = query.from(DbReceipt.class);
        Expression<?> expr = cb.function("to_char", String.class, root.get("sellDate"), cb.literal("YYYY-MM"));

        query.groupBy(expr);
        query.multiselect(expr);

        TypedQuery<Object[]> typedQuery = em.createQuery(query);
        List<Object[]> resultList = typedQuery.getResultList();
    }
}

The reason I use to_char function and not MONTH and similar is that I need entities like 2019-05 and 2020-05 to not be grouped together. I also narrow this example down to only year and month to keep things short, but the goal is to group by any date interval.

The code above creates the following query (SQL logging enabled) which results in an error:

Hibernate: select to_char(dbreceipt0_.sell_date, ?) as col_0_0_ from receipts dbreceipt0_ group by to_char(dbreceipt0_.sell_date, ?)
24-05-2020 12:16:30.071 [http-nio-1234-exec-5] WARN  o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions - SQL Error: 0, SQLState: 42803
24-05-2020 12:16:30.071 [http-nio-1234-exec-5] ERROR o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions - ERROR: column "dbreceipt0_.sell_date" must appear in the GROUP BY clause or be used in an aggregate function
  Position: 16

which to me is caused by the fact that the whole expression is put into the 'group by' part of the query, rather than just an alias. Now, I've tried to assign an alias to the expression (which returns Selection<T> and groupBy accepts expressions, therefore I can only really use that in the multiselect), but that didn't affect how the query is performed - nothing changed.

How do I achieve grouping by year and month as described above using Criteria API? Maybe there's a different way other than using to_char? Maybe there's a way to give an alias to the groupBy method that would cause it to group by an alias instead of the whole expression?

like image 291
Jacek Ślimok Avatar asked Nov 07 '22 07:11

Jacek Ślimok


1 Answers

I think it's a bug in PostgreSQL (the error comes from there, not from Hibernate). I have tried a slightly modified version of your code with EclipseLink + Derby and works perfectly. Note that I had to use numbers instead of strings because Derby DB doesn't have an equivalent of TO_CHAR function.

Expression<Integer> year = cb.function("YEAR", Integer.class, root.get("sellDate"));
Expression<Integer> month = cb.function("MONTH", Integer.class, root.get("sellDate"));
Expression<Integer> expr = cb.sum(month, cb.prod(12, year));
query.groupBy(expr);
query.multiselect(expr);

This returns the following SQL:

SELECT (MONTH(MY_DATE) + (12 * YEAR(MY_DATE))) 
FROM MY_DATE_TABLE 
GROUP BY (MONTH(MY_DATE) + (12 * YEAR(MY_DATE)))

Note that there are no portable solutions for manipulating dates in JPA criteria queries. If the number of groups to be queried simultaneously is not too high I'd go with a more practical approach where you find the dates in Java and pass them as literals to the query builder.

Another workaround is to query with a groupBy(root.get("sellDate")) and then aggregate the results in Java according to the desired time period.

Post Scriptum: I don't think it's relevant, however I modified the query's return type from Object[] to Object.

like image 72
perissf Avatar answered Nov 11 '22 03:11

perissf