从Spring Boot中的本地查询使用SqlResultSetMapping返回List

huangapple go评论68阅读模式
英文:

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

问题

这是您提供的内容的翻译:

因此,我想从实体表中提取DTO列表,而无需首先获取所有满足某些条件的实体,然后将实体转换为DTO。以下是我目前的代码。

具有以下SQL映射Invoice实体:

@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 getters & setters
}

这是DTO类:

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

我有InvoiceTour实体的三个层次:ControllerServiceRepository。我将只写调用服务层的方法,然后服务层调用存储库层,最后存储库层方法引用@NamedNativeQuery

public interface InvoiceTourService {

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

InvoiceTourService

@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);
	}
}

InvoiceTourRepository

@Repository
public interface InvoiceTourRepository extends JpaRepository<InvoiceTour, Integer>{
	
	@Query(name = "findInvoicesDto", nativeQuery = true)
	List<InvoiceDto> findInvoices(@Param("invoiceNumber") String invoiceNumber, @Param("paid") boolean paid);
}

这产生了以下异常:

无法提取ResultSet;SQL [n/a];嵌套异常是org.hibernate.exception.SQLGrammarException:无法提取ResultSet

是否有一种方式可以使用@SqlResultSetMappings提取自定义DTO的列表,并直接形成DTO集合?

更新1
好的,我测试了另外两种情况:

情况1:

WHERE子句中,我在%运算符和:invoiceNumber之间放置了一个空格,如下所示:

LIKE % :invoiceNumber %

在这种情况下,我得到了:

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

情况2:

WHERE子句中,我在%运算符和:invoiceNumber之间没有放置空格,如下所示:

LIKE %:invoiceNumber%

在这种情况下,我得到了:

org.springframework.dao.InvalidDataAccessApiUsageException: Unknown parameter name : invoiceNumber; nested exception is java.lang.IllegalArgumentException: Unknown parameter name : invoiceNumber

更新2
好的,我又测试了一次,完全使用了更新1之前的代码。

在这种情况下,在WHERE子句中的代码如下:

LIKE % :invoiceNumber %

我还在控制台上生成了SQL语句,这是我得到的:

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 % ? %

我得到了异常:

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

希望这些翻译能对您有所帮助。

英文:

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

Invoice entity with following sql mappings:

@Entity
@SqlResultSetMappings({ 
@SqlResultSetMapping(name = &quot;findInvoicesDtoMapping&quot;,
classes = {@ConstructorResult(targetClass=com.path.to.dto.invoice.InvoiceDto.class,
columns = {@ColumnResult(name=&quot;invoiceId&quot;, type=Integer.class),
@ColumnResult(name=&quot;projectNumber&quot;, type=String.class),
})} ),
})
@NamedNativeQueries(value = {
@NamedNativeQuery(name = &quot;findInvoicesDto&quot;, query = &quot;&quot;
+&quot;SELECT invoice.invoice_id AS invoiceId, invoice.project_number AS projectNumber &quot;  
+&quot;FROM Invoiceinvoice &quot;
+&quot;WHERE invoice_tour.paid = :paid OR invoice_tour.invoice_number LIKE % :invoiceNumber&quot;, 
resultSetMapping = &quot;findInvoicesTourMapping&quot;),
})
@Table(name = &quot;invoice_tour&quot;)
public class InvoiceTour {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = &quot;invoice_id&quot;)
private int invoiceId;
@Column(name = &quot;date_of_finishing&quot;, unique = false) 
private String dateOfFinishing;
@Column(name = &quot;account_number&quot;, unique = false)
private String accountNumber;
@Column(name = &quot;project_number&quot;, unique = false)
private String projectNumber;
@Column(name = &quot;invoice_number&quot;, unique = false)
private String invoiceNumber;
@Column(name = &quot;paid&quot;, unique = false)
private boolean paid;
//omitted gettes&amp;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&amp;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&lt;InvoiceDto&gt; 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&lt;InvoiceDto&gt; findInvoices(String invoiceNumber, boolean paid) {
return repository.findInvoices(invoiceNumber, paid);
}
}

Repository for InvoiceTour:

@Repository
public interface InvoiceTourRepository extends JpaRepository&lt;InvoiceTour, Integer&gt;{
@Query(name = &quot;findInvoicesDto&quot;, nativeQuery = true)
List&lt;InvoiceDto&gt; findInvoices(@Param(&quot;invoiceNumber&quot;) String invoiceNumber, @Param(&quot;paid&quot;) 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 POJOs using @SqlResultSetMappings and directly form collection of DTOs?

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 &#39;invoice_tour.invoice_number LIKE % &#39;a&#39; % OR invoice_tour.company_id = 2&#39; 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

得分: 1

Sure, here's the translated content:

  1. 尝试以以下方式更正您的命名查询:
@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. 检查您的 @SqlResultSetMapping。应该像这样:
@SqlResultSetMapping(name = "findInvoicesTourMapping",
   classes = @ConstructorResult(
      targetClass=com.path.to.dto.invoice.InvoiceDto.class,
      columns = {@ColumnResult(name="invoiceId"),
                 @ColumnResult(name="projectNumber"),
      }
   )
)

请注意 @SqlResultSetMapping.name 应等于 @NamedNativeQuery.resultSetMapping

  1. 您应该像这样传递 %
@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);
    }
}
英文:
  1. Try to correct your named query in the following way:
@NamedNativeQuery(
  name = &quot;findInvoicesDto&quot;,
  query = 
     &quot;SELECT &quot; +
     &quot;  invoice.invoice_id AS invoiceId, &quot; +
     &quot;  invoice.project_number AS projectNumber &quot; +
     &quot;FROM invoice_tour &quot; +
     &quot;WHERE invoice_tour.paid = :paid OR invoice_tour.invoice_number LIKE :invoiceNumber&quot;,
  resultSetMapping = &quot;findInvoicesTourMapping&quot;
)
  1. Check your @SqlResultSetMapping. It should be like this:
@SqlResultSetMapping(name = &quot;findInvoicesTourMapping&quot;,
classes = @ConstructorResult(
targetClass=com.path.to.dto.invoice.InvoiceDto.class,
columns = {@ColumnResult(name=&quot;invoiceId&quot;),
@ColumnResult(name=&quot;projectNumber&quot;),
}
)
)

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&lt;InvoiceDto&gt; findInvoices(String invoiceNumber, boolean paid) {
        
        return repository.findInvoices(&quot;%&quot; + invoiceNumber + &quot;%&quot;, paid);
    }
}

答案2

得分: 1

为什么在这里直接使用SQL?JPQL/HQL 完全可以表示这种类型的查询。通过避免使用SQL,您可以从 Spring Data JPA 提供的一些抽象中受益,即您可以移除实体上所有的命名本地查询注释,改为在存储库上使用以下内容:

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

您可能还会喜欢 Blaze-Persistence Entity Views 提供的功能。

我创建了这个库,以便在 JPA 模型和自定义接口或抽象类定义的模型之间实现简单映射,类似于功能强化版的 Spring Data Projections。思路是您按照所需定义目标结构(领域模型),并通过 JPQL 表达式将属性(getter)映射到实体模型。

对于您的用例,使用 Blaze-Persistence Entity-Views 的 DTO 模型可能如下所示:

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

查询只需将实体视图应用于查询,最简单的方法只是根据 id 进行查询。

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

Spring Data 集成使您几乎可以像使用 Spring Data Projections 一样使用它:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

您可以使用 Spring Data 命名约定构建查询,大致如下所示:

List<InvoiceDto> findByInvoiceNumberOrPaid(@Param("invoiceNumber") String invoiceNumber, @Param("paid") boolean 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 i.e. you could remove all the named native query annotations on the entity and instead use the following on the repository:

@Query(&quot;FROM InvoiceTour t WHERE t.paid = :paid OR t.invoiceNumber LIKE &#39;%&#39; ||  :invoiceNumber&quot;)
List&lt;InvoiceDto&gt; findInvoices(@Param(&quot;invoiceNumber&quot;) String invoiceNumber, @Param(&quot;paid&quot;) boolean paid);

You might also like what Blaze-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&lt;InvoiceDto&gt; findByInvoiceNumberOrPaid(@Param(&quot;invoiceNumber&quot;) String invoiceNumber, @Param(&quot;paid&quot;) boolean paid);

答案3

得分: 0

因此,总结一下。我能够得到期望的结果,要归功于SternK的回答和文档。因此,以下两种方法给了我正确的结果:

方法1:

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

字符串findInvoicesDto必须与@SqlResultSetMapping.name匹配。

方法2:

就像在SternK的回答中一样。

注意:我没有预料到Hibernate中会有带有LIKE%运算符的这种语法。这让我想起了在学校上学期时我在PreparedStatementsJDBC的方式中编写SQL查询的方式 从Spring Boot中的本地查询使用SqlResultSetMapping返回List<InvoiceDto>? 也许这是一个愚蠢的问题,但在某种程度上这难道不容易受到SQL注入的影响吗?

英文:

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&lt;InvoiceDto&gt; invoices = em.createNamedQuery(&quot;findInvoicesDto&quot; )
.setParameter(&quot;invoiceNumber&quot;, &quot;%&quot; + invoiceNumber + &quot;%&quot;)
.setParameter(&quot;paid&quot;, 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 从Spring Boot中的本地查询使用SqlResultSetMapping返回List<InvoiceDto>? Maybe this is stupid quesion, but isn't this prone to SQL Injection to some extent?

huangapple
  • 本文由 发表于 2020年10月6日 01:34:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/64213541.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定