I'm working with Spring Boot and need to run a complex native SQL query with several JOINS, but I have problem to map the result in the next record (DTO):
public record FindAvailableEventResponseDto(
UUID id,
String name,
String description,
ZonedDateTime date,
short capacity,
short occupiedSeats,
String organizedBy) {
}
My SQL query is:
SELECT e.id, e.name, e.description, e.date, e.capacity,
COUNT(s.id) AS occupiedSeats, u.name as organizedBy
FROM events e
LEFT JOIN seats s ON s.eventId = e.id
LEFT JOIN users u on e.organizedBy = u.id
WHERE e.date > :currentDate
AND s.state = 'OCCUPIED'
GROUP BY e.id, e.name, e.description, e.date, e.capacity, u.name
ORDER BY e.date ASC
Inside my interface EventRepository
, I put the @Query
annotation like this:
public interface EventRepository extends JpaRepository<Event, UUID> {
@Query(value = """
SELECT e.id, e.name, e.description, e.date, e.capacity,
CAST(COUNT(s.id) AS smallint) AS occupiedSeats, u.name as organizedBy
FROM events e
LEFT JOIN seats s ON s.eventId = e.id AND s.state = 'OCCUPIED'
LEFT JOIN users u on e.organizedBy = u.id
WHERE e.date > :currentDate
GROUP BY e.id, e.name, e.description, e.date, e.capacity, u.name
ORDER BY e.date ASC;
""", nativeQuery = true)
List<FindAvailableEventResponseDto> findUpcomingEventsWithAvailableSeats(
@Param("currentDate") ZonedDateTime currentDate);
}
This code throw the error:
org.springframework.orm.jpa.JpaSystemException: Cannot instantiate query result type 'com.hector.crud.events.dtos.response.FindAvailableEventResponseDto' due to: argument type mismatch\r\n\tat org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:341)\r\n\tat org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:241)\r\n\tat org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:560)\r\n\tat org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)\r\n\tat org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:343)\r\n\tat org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:160)\r\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\r\n\tat org.springframework.data.jpa.repository.support...
What is the cleanest way to achieve this while keeping the mapping logic inside the repository?
I read about using JPL, but I prefer to use SQL in case I have to change DB engines.
In case you need search queries with similar complexity as above, what is more advisable? I have MapStruct configured in my project, but Java is not my main stack language.
Please try any of the below solution works:
Option 1: You can modify your query to return an object array or a Tuple, which Spring Data JPA can then map manually.
public interface EventRepository extends JpaRepository<Event, UUID> {
@Query(value = """
SELECT e.id, e.name, e.description, e.date, e.capacity,
COUNT(s.id) AS occupiedSeats, u.name as organizedBy
FROM events e
LEFT JOIN seats s ON s.eventId = e.id AND s.state = 'OCCUPIED'
LEFT JOIN users u on e.organizedBy = u.id
WHERE e.date > :currentDate
GROUP BY e.id, e.name, e.description, e.date, e.capacity, u.name
ORDER BY e.date ASC
""", nativeQuery = true)
List<Object[]> findUpcomingEventsWithAvailableSeats(@Param("currentDate") ZonedDateTime currentDate);
}
And wherever you are using it, you can map it to FindAvailableEventResponseDto
something like below:
List<Object[]> results = eventRepository.findUpcomingEventsWithAvailableSeats(currentDate);
List<FindAvailableEventResponseDto> eventResponseDtoList =
results.stream()
.map(result -> {
UUID id = (UUID) result[0];
String name = (String) result[1];
String description = (String) result[2];
ZonedDateTime date = (ZonedDateTime) result[3];
short capacity = ((Number) result[4]).shortValue();
short occupiedSeats = ((Number) result[5]).shortValue();
String organizedBy = (String) result[6];
return new FindAvailableEventResponseDto(id, name, description, date, capacity, occupiedSeats, organizedBy);
}).collect(Collectors.toList());
Option 2: You can even make use of Mapper.
Create a Mapper:
@Mapper(componentModel = "spring")
public interface EventMapper {
@Mapping(source = "result[0]", target = "id")
@Mapping(source = "result[1]", target = "name")
@Mapping(source = "result[2]", target = "description")
@Mapping(source = "result[3]", target = "date")
@Mapping(source = "result[4]", target = "capacity")
@Mapping(source = "result[5]", target = "occupiedSeats")
@Mapping(source = "result[6]", target = "organizedBy")
FindAvailableEventResponseDto toDto(Object[] result);
}
To use it:
@Autowired
private EventMapper eventMapper;
List<Object[]> results = eventRepository.findUpcomingEventsWithAvailableSeats(currentDate);
List<FindAvailableEventResponseDto> eventResponseDtoList = results.stream()
.map(eventMapper::toDto)
.collect(Collectors.toList());