21

Hi what I am trying to achieve is to get SQL native query result map into my DTO in java spring jpa repository, how do I do this properly? I try several code, but it does not work, here is what I tried:

First try :

@Repository
public interface StockRepository extends RevisionRepository<Stock, Long, Integer>, JpaRepository<Stock, Long> {
    
   @Query(value = "SELECT stock_akhir.product_id AS productId, stock_akhir.product_code AS productCode, SUM(stock_akhir.qty) as stockAkhir "
        + "FROM book_stock stock_akhir "
        + "where warehouse_code = (:warehouseCode) "
        + "AND product_code IN (:productCodes) "
        + "GROUP BY product_id, product_code, warehouse_id, warehouse_code", nativeQuery = true)
   List<StockAkhirDto> findStockAkhirPerProductIn(@Param("warehouseCode") String warehouseCode, @Param("productCodes") Set<String> productCode);
}

once I execute the function, I got this error:

No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [com.b2bwarehouse.Dto.RequestDto.StockDto.StockAkhirDto]

Second try :

@Repository
public interface StockRepository extends RevisionRepository<Stock, Long, Integer>, JpaRepository<Stock, Long> {
    
   @Query(value = "SELECT new com.b2bwarehouse.Dto.RequestDto.StockDto.StockAkhirDto(stock_akhir.product_id AS productId, stock_akhir.product_code AS productCode, SUM(stock_akhir.qty) as stockAkhir) "
      + "FROM book_stock stock_akhir "
      + "where warehouse_code = (:warehouseCode) "
      + "AND product_code IN (:productCodes) "
      + "GROUP BY product_id, product_code, warehouse_id, warehouse_code", nativeQuery = true)
   List<StockAkhirDto> findStockAkhirPerProductIn(@Param("warehouseCode") String warehouseCode, @Param("productCodes") Set<String> productCode);
}

in second here is the error:

could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet

below is my DTO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class StockAkhirDto {
   private Long productId;
   private String productCode;
   private Integer stockAkhir;
}

How should I correct my code? So, can I get the result into my DTO?

SternK
  • 11,649
  • 22
  • 32
  • 46
Ke Vin
  • 3,478
  • 11
  • 60
  • 91

6 Answers6

32

You can define the following named native query with appropriate sql result set mapping:

import javax.persistence.NamedNativeQuery;
import javax.persistence.SqlResultSetMapping;
import javax.persistence.ConstructorResult;
import javax.persistence.ColumnResult;

@Entity
@NamedNativeQuery(
    name = "find_stock_akhir_dto",
    query =
        "SELECT " + 
        "  stock_akhir.product_id AS productId, " + 
        "  stock_akhir.product_code AS productCode, " + 
        "  SUM(stock_akhir.qty) as stockAkhir " + 
        "FROM book_stock stock_akhir " + 
        "where warehouse_code = :warehouseCode " + 
        "  AND product_code IN :productCodes " + 
        "GROUP BY product_id, product_code, warehouse_id, warehouse_code",
    resultSetMapping = "stock_akhir_dto"
)
@SqlResultSetMapping(
    name = "stock_akhir_dto",
    classes = @ConstructorResult(
        targetClass = StockAkhirDto.class,
        columns = {
            @ColumnResult(name = "productId", type = Long.class),
            @ColumnResult(name = "productCode", type = String.class),
            @ColumnResult(name = "stockAkhir", type = Integer.class)
        }
    )
)
public class SomeEntity
{
}

and then use it:

@Repository
public interface StockRepository extends RevisionRepository<Stock, Long, Integer>, JpaRepository<Stock, Long> {

   @Query(name = "find_stock_akhir_dto", nativeQuery = true)
   List<StockAkhirDto> findStockAkhirPerProductIn(
      @Param("warehouseCode") String warehouseCode,
      @Param("productCodes") Set<String> productCode
   );
}
SternK
  • 11,649
  • 22
  • 32
  • 46
  • hi, it seems i can't import "SqlResultSetMapping","ConstructorResult" annotation what pom should i add to import that annotation? – Ke Vin Nov 11 '20 at 02:33
  • @KeVin This are standard jpa annotations (`@SqlResultSetMapping` was added in jpa 1.0 , `@ConstructorResult` was added in jpa 2.1) I have added imports to the code snippet. – SternK Nov 11 '20 at 08:03
  • You have annotated your SomeEntity with @Entity - doesn't this make it a Entity (versus a DTO?) because it now wants a field for Id? – Radika Moonesinghe Apr 20 '22 at 10:05
  • @SternK your putting the results into a Entity not DTO? – Radika Moonesinghe Apr 20 '22 at 10:21
  • @RadikaMoonesinghe The `SomeEntity` is not important here. I want to illustrate here only that `@NamedNativeQuery` and `@SqlResultSetMapping` annotations should be placed on some entity. – SternK Apr 20 '22 at 13:05
28

i find a way which is not usual, but i find data type called "Tuple" when i try to use QueryDsl to solved this problem, but i won't recommend QueryDsl if you are just getting started just like me. Lets focus on how i do it with "Tuple"

i changed my return type to Tuple, here is how my repository looked like :

@Repository
public interface StockRepository extends RevisionRepository<Stock, Long, Integer>, JpaRepository<Stock, Long> {
    
   @Query(value = "SELECT stock_akhir.product_id AS productId, stock_akhir.product_code AS productCode, SUM(stock_akhir.qty) as stockAkhir "
        + "FROM book_stock stock_akhir "
        + "where warehouse_code = (:warehouseCode) "
        + "AND product_code IN (:productCodes) "
        + "GROUP BY product_id, product_code, warehouse_id, warehouse_code", nativeQuery = true)
   List<Tuple> findStockAkhirPerProductIn(@Param("warehouseCode") String warehouseCode, @Param("productCodes") Set<String> productCode);
}

and then here in my service class, since it's returned as Tuple, i have to map the column one by one manually, here is my service function looked like :

public List<StockTotalResponseDto> findStocktotal() {
    List<Tuple> stockTotalTuples = stockRepository.findStocktotal();
    
    List<StockTotalResponseDto> stockTotalDto = stockTotalTuples.stream()
            .map(t -> new StockTotalResponseDto(
                    t.get(0, String.class), 
                    t.get(1, String.class), 
                    t.get(2, BigInteger.class)
                    ))
            .collect(Collectors.toList());
    
    return stockTotalDto;
}

the column field start with 0, in this way i can keep my query neat at Repository level. But i will accept SternK answer as the accepted answer because that way worked too, i will keep my answer here if someone need something like this

Ke Vin
  • 3,478
  • 11
  • 60
  • 91
  • This a simple version when dealing with some of the more complicated Spring annotations. – Michael Rountree May 24 '21 at 14:16
  • 2
    Quite neat! Thanks for this advice! – kolyaiks Sep 26 '21 at 19:46
  • 3
    Thanks! All day looking for such a simple solution. JPA Repository can be quite a pain in the ass – Kypps Jan 10 '22 at 16:35
  • Did the accepted answer ever work for you? – NRJ Jun 15 '22 at 12:35
  • @NRJ it worked at my question but i can't maintain too many 'NamedNativeQuery' at my entity, plus i don't think i can build dynamic query with it so i never try to improve it anymore, so i still go with my answer till now (still the best option i have got) – Ke Vin Oct 05 '22 at 15:33
17

Create standard native @Query

@Query(value = "select id, age, name FROM Person WHERE age=?1", nativeQuery=true)
List<PersonView> getPersonsByAge(int age);

and an interface

public interface PersonView {
    Long getId();
    Integer getAge();
    String getName();
}

columns are matched by order (not by names). In this way, you have a native query, no entities and not too much boiler plate code (aka many annotations).

However, resulting views (Jdk proxy etc.) are very slow in access, i had some code doing some grouping over a stream, and it's 10x !! slower than with standard DTO/Pojos ! so at the end, I don't use nativeQuery anymore, but:

SELECT new com.my_project.myDTO(p.id, p.age, p.name) .....
razor
  • 2,727
  • 6
  • 33
  • 45
  • 5
    Spring can figure out how to read an interface to populate a projection, but not a POJO. Things can't just be simple can they? :) – mvd Nov 20 '21 at 00:30
  • cool +1. can you elaborate on the `SELECT new com.my_project.myDTO(p.id, p.age, p.name) .....` approach. Atleast commenting with a reference should help. – samshers Oct 15 '22 at 05:22
0

The second variant is pretty close. You just have to remove the aliases for the constructor expression:

new com.b2bwarehouse.Dto.RequestDto.StockDto.StockAkhirDto(
    stock_akhir.product_id, 
    stock_akhir.product_code, 
    SUM(stock_akhir.qty)
)

should work.

Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
0

Another valid option based on Sternk answer woul be as follows

You can define the following named native query with appropriate sql result set mapping:

resources/META-INF/orm.xml

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
                 version="2.0">
    <named-native-query name="find_stock_akhir_dto" result-class="com.fullyqualified.name.SomeEntity"
                        result-set-mapping="stock_akhir_dto">
        <query><![CDATA[
            SELECT 
              stock_akhir.product_id AS productId, 
              stock_akhir.product_code AS productCode, 
              SUM(stock_akhir.qty) as stockAkhir 
            FROM book_stock stock_akhir 
            where warehouse_code = :warehouseCode 
              AND product_code IN :productCodes  
            GROUP BY product_id, product_code, warehouse_id, warehouse_code]]></query>
    </named-native-query>
</entity-mappings>
import javax.persistence.NamedNativeQuery;
import javax.persistence.SqlResultSetMapping;
import javax.persistence.ConstructorResult;
import javax.persistence.ColumnResult;

@Entity
@SqlResultSetMapping(
    name = "stock_akhir_dto",
    classes = @ConstructorResult(
        targetClass = StockAkhirDto.class,
        columns = {
            @ColumnResult(name = "productId", type = Long.class),
            @ColumnResult(name = "productCode", type = String.class),
            @ColumnResult(name = "stockAkhir", type = Integer.class)
        }
    )
)
public class SomeEntity
{
}

and then use it:

@Repository
public interface StockRepository extends RevisionRepository<Stock, Long, Integer>, JpaRepository<Stock, Long> {

   @Query(name = "find_stock_akhir_dto", nativeQuery = true)
   List<StockAkhirDto> findStockAkhirPerProductIn(
      @Param("warehouseCode") String warehouseCode,
      @Param("productCodes") Set<String> productCode
   );
}
Ruslan López
  • 4,433
  • 2
  • 26
  • 37
0

Map your result of a native SQL query into an interface-based DTO in a Spring JPA repository is very simplified by spring boot framework as below steps you can follow

1. Define the DTO Interface:

Create an interface that defines the getter methods for the properties you want to map from the native SQL query result. The interface methods should match the column aliases used in your SQL query.

public interface MyDTO {
    Long getId();
    String getName();
    // Other getter methods for other properties
}

2. Define the Native SQL Query:

In your Spring JPA repository interface, use the @Query annotation with a native SQL query that returns the columns needed for the DTO interface.

@Repository
public interface MyEntityRepository extends JpaRepository<MyEntity, Long> {

    @Query(value = "SELECT id as id, name as name FROM my_table WHERE ...", nativeQuery = true)
    List<MyDTO> findDataUsingNativeQuery();
}

You can now use the findDataUsingNativeQuery() method from your service layer to retrieve the data mapped to the DTO interface.

anandchaugule
  • 901
  • 12
  • 20