简体   繁体   中英

Returning List<InvoiceDto> from SqlResultSetMapping in Spring Boot using native query?

So, I want to extract list of DTO s from entity table, without having to first get all entities (which are satisfying some condition) and then to make DTO s out of entities. Here is what I have now.

Invoice entity with following sql mappings :

@Entity
@SqlResultSetMappings({ 
    @SqlResultSetMapping(name = "findInvoicesDtoMapping",
            classes = {@ConstructorResult(targetClass=com.path.to.dto.invoice.InvoiceDto.class,
            columns = {@ColumnResult(name="invoiceId", type=Integer.class),
                       @ColumnResult(name="projectNumber", type=String.class),
            })} ),
    })
@NamedNativeQueries(value = {
    
        @NamedNativeQuery(name = "findInvoicesDto", query = ""
        +"SELECT invoice.invoice_id AS invoiceId, invoice.project_number AS projectNumber "  
        +"FROM Invoiceinvoice "
        +"WHERE invoice_tour.paid = :paid OR invoice_tour.invoice_number LIKE % :invoiceNumber", 
        resultSetMapping = "findInvoicesTourMapping"),

    })
@Table(name = "invoice_tour")
public class InvoiceTour {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "invoice_id")
    private int invoiceId;
    
     @Column(name = "date_of_finishing", unique = false) 
     private String dateOfFinishing;
     
     @Column(name = "account_number", unique = false)
     private String accountNumber;
     
     @Column(name = "project_number", unique = false)
     private String projectNumber;
     
     @Column(name = "invoice_number", unique = false)
     private String invoiceNumber;
     
     @Column(name = "paid", unique = false)
     private boolean paid;
     
     //omitted gettes&setters
}

Here is DTO class:

public class InvoiceDto {
    
    private String projectNumber;
    private int invoiceId;
    
    public(int invoiceId, String projectNumber){
         this.invoiceId = invoiceId;
         this.projectNumber = projectNumber;
    }
    
    //omitted gettes&setters
}

I have three layers for my InvoiceTour entity: Contoller , Service and Repository . I will not write entire controller layer just the method which is calling the service layer, which in turns calls repository layer and finally repository layer method which refers to @NamedNativeQuery .

public interface InvoiceTourService {

    List<InvoiceDto> findInvoices(String invoiceNumber, boolean paid);
}

Service for InvoiceTour :

@Service
@Validated
public class InvoiceTourServiceImpl implements InvoiceTourService{
    
    private final InvoiceTourRepository repository;
    
    @Inject
    public InvoiceTourServiceImpl(final InvoiceTourRepository  repository) {
        this.repository = repository;
    }
    
    @Override
    public List<InvoiceDto> findInvoices(String invoiceNumber, boolean paid) {
        
        return repository.findInvoices(invoiceNumber, paid);
    }
}

Repository for InvoiceTour :

@Repository
public interface InvoiceTourRepository extends JpaRepository<InvoiceTour, Integer>{

    @Query(name = "findInvoicesDto", nativeQuery = true)
    List<InvoiceDto> findInvoices(@Param("invoiceNumber") String invoiceNumber, @Param("paid") boolean paid);
}

This produces following exception:

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

Is there a way to extract list of DTO custom POJO s using @SqlResultSetMappings and directly form collection of DTO s?

UPDATED 1 Okey, I tested two more situations:

Case 1:

In WHERE clause, I placed one space between % operators and :invoiceNumber , like so:

LIKE % :invoiceNumber %

In that case I got:

com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'invoice_tour.invoice_number LIKE % 'a' % OR invoice_tour.company_id = 2' at line 1
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_261]
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_261]
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_261]
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_261]
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) ~[mysql-connector-java-5.1.46.jar:5.1.46]
    at com.mysql.jdbc.Util.getInstance(Util.java:408) ~[mysql-connector-java-5.1.46.jar:5.1.46]
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:944) ~[mysql-connector-java-5.1.46.jar:5.1.46]
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3976) ~[mysql-connector-java-5.1.46.jar:5.1.46]

Case 2:

In WHERE clause, I placed no space between % operators and :invoiceNumber , like so:

 LIKE %:invoiceNumber%

In that case I got:

 org.springframework.dao.InvalidDataAccessApiUsageException: Unknown parameter name : invoiceNumber; nested exception is java.lang.IllegalArgumentException: Unknown parameter name : invoiceNumber
        at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:367) ~[spring-orm-5.0.8.RELEASE.jar:5.0.8.RELEASE]
        at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:227) ~[spring-orm-5.0.8.RELEASE.jar:5.0.8.RELEASE]
        at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527) ~[spring-orm-5.0.8.RELEASE.jar:5.0.8.RELEASE]
        at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-5.0.8.RELEASE.jar:5.0.8.RELEASE]
        at org.springframework.dao.support.DataAccessUtils.translateI

fNecessary(DataAccessUtils.java:242) ~[spring-tx-5.0.8.RELEASE.jar:5.0.8.RELEASE]

UPDATED 2

Okey, I tested it one more time, exactly with code that is before UPDATE 1 .

In this case, in WHERE clause code goes like this:

LIKE % :invoiceNumber %

I also turn od sql statment generation in the console and this is what I have:

 SELECT invoice_tour.invoice_id AS invoiceId, invoice_tour.project_number AS projectNumber 
    FROM Invoice_tour invoice_tour WHERE invoice_tour.paid = ? OR invoice_tour.invoice_number LIKE % ? %

Exception I got:

org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:242) ~[spring-orm-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:225) ~[spring-orm-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527) ~[spring-orm-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242) ~[spring-tx-5.0.8.RELEASE.jar:5.0.8.RELEASE]
  1. Try to correct your named query in the following way:
@NamedNativeQuery(
  name = "findInvoicesDto",
  query = 
     "SELECT " +
     "  invoice.invoice_id AS invoiceId, " +
     "  invoice.project_number AS projectNumber " +
     "FROM invoice_tour " +
     "WHERE invoice_tour.paid = :paid OR invoice_tour.invoice_number LIKE :invoiceNumber",
  resultSetMapping = "findInvoicesTourMapping"
)
  1. Check your @SqlResultSetMapping . It should be like this:
@SqlResultSetMapping(name = "findInvoicesTourMapping",
   classes = @ConstructorResult(
      targetClass=com.path.to.dto.invoice.InvoiceDto.class,
      columns = {@ColumnResult(name="invoiceId"),
                 @ColumnResult(name="projectNumber"),
      }
   )
)

Pay attention that @SqlResultSetMapping.name equal to @NamedNativeQuery.resultSetMapping .

  1. You should pass % like this:
@Service
@Validated
public class InvoiceTourServiceImpl implements InvoiceTourService{
    
    private final InvoiceTourRepository repository;

    // ...
    
    @Override
    public List<InvoiceDto> findInvoices(String invoiceNumber, boolean paid) {
        
        return repository.findInvoices("%" + invoiceNumber + "%", paid);
    }
}

Why are you using SQL directly here? JPQL/HQL is perfectly capable to represent this kind of query. By avoiding the use of SQL, you can benefit from some of the abstractions Spring Data JPA provides ie you could remove all the named native query annotations on the entity and instead use the following on the repository:

@Query("FROM InvoiceTour t WHERE t.paid = :paid OR t.invoiceNumber LIKE '%' ||  :invoiceNumber")
List<InvoiceDto> findInvoices(@Param("invoiceNumber") String invoiceNumber, @Param("paid") boolean paid);

You might also like whatBlaze-Persistence Entity Views has to offer.

I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.

A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:

@EntityView(InvoiceTour.class)
public interface InvoiceDto {
    @IdMapping
    Integer getInvoiceId();
    String getProjectNumber();
}

Querying is a matter of applying the entity view to a query, the simplest being just a query by id.

InvoiceDto a = entityViewManager.find(entityManager, InvoiceDto.class, id);

The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

You could use the Spring Data naming convention to construct the query which would look roughly like this:

List<InvoiceDto> findByInvoiceNumberOrPaid(@Param("invoiceNumber") String invoiceNumber, @Param("paid") boolean paid);

So, to summarize. I was able to get desired result thanks to SternK's answer and documentation. So the two following approaches gave me correct result:

Approach 1:

                List<InvoiceDto> invoices = em.createNamedQuery("findInvoicesDto" )
                .setParameter("invoiceNumber", "%" + invoiceNumber + "%")
                .setParameter("paid", paid)
                .getResultList();

The string findInvoicesDto has to match @SqlResultSetMapping.name .

Approach 2:

Is like in SternK's answer.

Note: I didn't expect this syntax with LIKE and % operator in Hibernate. This reminds me of PreparedStatements and JDBC way of writing SQL queries, which I did in previous semester at school :) Maybe this is stupid quesion, but isn't this prone to SQL Injection to some extent?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM