Native SQL reports#
Annotation-driven queries compile great SELECTs for CRUD, but reporting is usually where hand-written SQL earns its keep — aggregates, window functions, complex joins. Publish those as first-class endpoints with NativeQueryHandler.
A reporting endpoint#
@Component
@CrudMapping("/v1/reports/user-sales")
public class UserSalesReportHandler implements NativeQueryHandler {
@Autowired private AuthProvider auth;
@Override
public int aclCheck(FilterCriteria criteria, HandlerContext ctx) {
return userProvider.hasRole("REPORT_VIEW") ? 0 : -1;
}
@Override
public void preProcess(FilterCriteria criteria, HandlerContext ctx) {
if (criteria.getSimpleCriteria().get("since") == null) {
criteria.addCriteria("since", Instant.now().minus(30, ChronoUnit.DAYS).toString());
}
}
@Override
public NativeQuery getQuery(FilterCriteria criteria, HandlerContext ctx) {
String sql = """
SELECT u.id, u.name, SUM(o.amount) AS total
FROM users u JOIN orders o ON o.user_id = u.id
WHERE o.created_at >= ?
GROUP BY u.id, u.name
""";
NativeQuery q = new NativeQuery(sql, criteria.getSimpleCriteria().get("since"));
q.setCountQuery("""
SELECT COUNT(DISTINCT u.id)
FROM users u JOIN orders o ON o.user_id = u.id
WHERE o.created_at >= ?
""");
q.setFetchSize(500);
return q;
}
@Override
public Tuple onQueryResult(Tuple tuple, Action action) {
BigDecimal total = (BigDecimal) tuple.get("total");
if (total != null) tuple.set("total", total.setScale(2, RoundingMode.HALF_UP));
return tuple;
}
}Guidelines#
- Use positional
?placeholders. Bind in order with the vararg constructor oraddParams; Palmyra does not substitute named parameters. - Set
countQuerywhen pagination matters. A naiveCOUNT(*)over a GROUP BY / DISTINCT query is usually wrong — supply the right count query explicitly. - Tune
fetchSizefor wide reports. The default is 100; bump it for reports that return hundreds of thousands of rows to reduce round trips. - Keep
onQueryResultcheap. It runs per row inside the reactive stream — no database calls from here.
See also#
NativeQueryHandler— full method table.NativeQuery— every constructor / setter, includinglimitFirst()andsetOffset/setLimitfor pagination.