INT-3333: Return empty list as is from DB gateway (#3907)

* INT-3333: Return empty list as is from DB gateway

Fixes https://jira.spring.io/browse/INT-3333

In `JdbcOutboundGateway` and `JpaOutboundGateway` the empty result list
is treated as "no reply" and therefore `null` is returned cause
the flow to stop at this point.
It is better to return such a result as is and the target application to
decided what to do with it, e.g. a `discardChannel` on a downstream splitter
configuration.

NOTE: the `MongoDbOutboundGateway` doesn't treat an empty result as a `null`

* * Fix language in docs

Co-authored-by: Gary Russell <grussell@vmware.com>

Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
Artem Bilan
2022-10-11 14:39:29 -04:00
committed by GitHub
parent 6641b1f545
commit f7dd876be2
8 changed files with 125 additions and 131 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -221,9 +221,6 @@ public class JdbcOutboundGateway extends AbstractReplyProducingMessageHandler {
list = this.poller.doPoll(sqlQueryParameterSource);
}
Object payload = list;
if (list.isEmpty()) {
return null;
}
if (list.size() == 1 && (this.maxRows == null || this.maxRows == 1)) {
payload = list.get(0);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -51,7 +51,6 @@ import org.springframework.util.CollectionUtils;
* <li>JpQl Named Query</li>
* <li>Sql Native Named Query</li>
* </ul>.
*
* When objects are being retrieved, it also possibly to:
*
* <ul>
@@ -176,8 +175,8 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
* @param jpaQuery The provided JPA query must neither be null nor empty.
*/
public void setJpaQuery(String jpaQuery) {
Assert.isTrue(this.nativeQuery == null && this.namedQuery == null, "You can define only one of the "
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
Assert.isTrue(this.nativeQuery == null && this.namedQuery == null,
"Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
Assert.hasText(jpaQuery, "jpaQuery must neither be null nor empty.");
this.jpaQuery = jpaQuery;
}
@@ -190,8 +189,8 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
* @param nativeQuery The provided SQL query must neither be null nor empty.
*/
public void setNativeQuery(String nativeQuery) {
Assert.isTrue(this.namedQuery == null && this.jpaQuery == null, "You can define only one of the "
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
Assert.isTrue(this.namedQuery == null && this.jpaQuery == null,
"Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
Assert.hasText(nativeQuery, "nativeQuery must neither be null nor empty.");
this.nativeQuery = nativeQuery;
}
@@ -202,8 +201,8 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
* @param namedQuery Must neither be null nor empty
*/
public void setNamedQuery(String namedQuery) {
Assert.isTrue(this.jpaQuery == null && this.nativeQuery == null, "You can define only one of the "
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
Assert.isTrue(this.jpaQuery == null && this.nativeQuery == null,
"Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
Assert.hasText(namedQuery, "namedQuery must neither be null nor empty.");
this.namedQuery = namedQuery;
}
@@ -312,7 +311,6 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
* <p>If set to <code>false</code>, the complete result list is returned as the
* payload.
* @param expectSingleResult true if a single object is expected.
*
*/
public void setExpectSingleResult(boolean expectSingleResult) {
this.expectSingleResult = expectSingleResult;
@@ -339,7 +337,7 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
}
/**
* Set the expression for maximum number of results expression. It has be a non null value
* Set the expression for maximum number of results expression. It has to be a non-null value
* Not setting one will default to the behavior of fetching all the records
* @param maxResultsExpression The maximum results expression.
*/
@@ -413,7 +411,7 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
/**
* Execute the actual Jpa Operation. Call this method, if you need access to
* process return values. This methods return a Map that contains either
* process return values. These methods return a Map that contains either
* the number of affected entities or the affected entity itself.
*<p>Keep in mind that the number of entities effected by the operation may
* not necessarily correlate with the number of rows effected in the database.
@@ -497,7 +495,7 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
payload = this.jpaOperations.find(entityClazz, id);
}
else {
final List<?> result;
List<?> result;
int maxNumberOfResults = evaluateExpressionForNumericResult(requestMessage, this.maxResultsExpression);
if (requestMessage == null) {
result = doPoll(this.parameterSource, 0, maxNumberOfResults);
@@ -511,27 +509,18 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
result = doPoll(paramSource, firstResult, maxNumberOfResults);
}
if (result.isEmpty()) {
payload = null;
}
else {
if (this.expectSingleResult) {
if (result.size() == 1) {
payload = result.iterator().next();
}
else if (requestMessage != null) {
throw new MessagingException(requestMessage,
"The Jpa operation returned more than 1 result for expectSingleResult mode.");
}
else {
throw new MessagingException(
"The Jpa operation returned more than 1 result for expectSingleResult mode.");
}
if (this.expectSingleResult && !result.isEmpty()) {
if (result.size() == 1) {
payload = result.iterator().next();
}
else {
payload = result;
throw new MessagingException(requestMessage, // NOSONAR
"The Jpa operation returned more than 1 result for expectSingleResult mode.");
}
}
else {
payload = result;
}
}
checkDelete(payload);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.integration.jpa.inbound;
import org.springframework.integration.endpoint.AbstractMessageSource;
import org.springframework.integration.jpa.core.JpaExecutor;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Polling message source that produces messages from the result of the provided:
@@ -30,11 +31,8 @@ import org.springframework.util.Assert;
* <li>JpQl Named Query</li>
* <li>Sql Native Named Query</li>
* </ul>.
*
* After the objects have been polled, it also possibly to either:
*
* executes an update after the select possibly to updated the state of selected records
*
* <ul>
* <li>executes an update (per retrieved object or for the entire payload)</li>
* <li>delete the retrieved object</li>
@@ -73,13 +71,14 @@ public class JpaPollingChannelAdapter extends AbstractMessageSource<Object> {
/**
* Use {@link JpaExecutor#poll()} to executes the JPA operation.
* If {@link JpaExecutor#poll()} returns null, this method will return
* <code>null</code>. Otherwise, a new {@link org.springframework.messaging.Message}
* is constructed and returned.
* Return {@code null} if result of {@link JpaExecutor#poll()} is {@link ObjectUtils#isEmpty}.
* The empty collection means there is no data to retrieve from DB at the moment therefore
* no reason to emit an empty message from this message source.
*/
@Override
protected Object doReceive() {
return this.jpaExecutor.poll();
Object result = this.jpaExecutor.poll();
return ObjectUtils.isEmpty(result) ? null : result;
}
@Override

View File

@@ -94,15 +94,13 @@ public class JpaExecutorTests {
assertThatIllegalArgumentException()
.isThrownBy(() -> executor.setNamedQuery("NamedQuery"))
.withMessage("You can define only one of the "
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
.withMessage("Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
assertThat(TestUtils.getPropertyValue(executor, "namedQuery")).isNull();
assertThatIllegalArgumentException()
.isThrownBy(() -> executor.setNativeQuery("select * from Student"))
.withMessage("You can define only one of the "
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
.withMessage("Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
assertThat(TestUtils.getPropertyValue(executor, "nativeQuery")).isNull();
@@ -112,8 +110,7 @@ public class JpaExecutorTests {
assertThatIllegalArgumentException()
.isThrownBy(() -> executor2.setJpaQuery("select s from Student s"))
.withMessage("You can define only one of the "
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
.withMessage("Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
assertThat(TestUtils.getPropertyValue(executor2, "jpaQuery")).isNull();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -160,7 +160,7 @@ public class JpaOutboundGatewayTests {
List<StudentDomain> allStudents = this.studentService.getAllStudents();
assertThat(allStudents).hasSize(3);
this.studentService.deleteStudents(allStudents);
assertThat(this.studentService.getAllStudents()).isNull();
assertThat(this.studentService.getAllStudents()).isEmpty();
}
}

View File

@@ -365,6 +365,10 @@ It can also have a `SqlParameterSourceFactory` injected to control the binding o
Starting with the version 4.2, the `request-prepared-statement-setter` attribute is available on the `<int-jdbc:outbound-gateway>` as an alternative to `request-sql-parameter-source-factory`.
It lets you specify a `MessagePreparedStatementSetter` bean reference, which implements more sophisticated `PreparedStatement` preparation before its execution.
Starting with the version 6.0, the `JdbcOutboundGateway` returns an empty list result as is instead of converting it to `null` as it was before with the meaning "no reply".
This caused an extra configuration in applications where handling of empty lists is a part of downstream logic.
See <<./splitter.adoc#split-stream-and-flux,Splitter Discard Channel>> for possible empty list handling option.
See <<jdbc-outbound-channel-adapter>> for more information about `MessagePreparedStatementSetter`.
[[jdbc-message-store]]

View File

@@ -1011,10 +1011,89 @@ public class JpaJavaApplication {
[[jpa-retrieving-outbound-gateway]]
==== Retrieving Outbound Gateway
The following example shows all the attributes that you can set on a retrieving outbound gateway and describes the key attributes:
The following example demonstrates how to configure a retrieving outbound gateway:
====
[source,xml]
[source, java, role="primary"]
.Java DSL
----
@SpringBootApplication
@EntityScan(basePackageClasses = StudentDomain.class)
public class JpaJavaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(JpaJavaApplication.class)
.web(false)
.run(args);
}
@Autowired
private EntityManagerFactory entityManagerFactory;
@Bean
public IntegrationFlow retrievingGatewayFlow() {
return f -> f
.handle(Jpa.retrievingGateway(this.entityManagerFactory)
.jpaQuery("from Student s where s.id = :id")
.expectSingleResult(true)
.parameterExpression("id", "payload"))
.channel(c -> c.queue("retrieveResults"));
}
}
----
[source, kotlin, role="secondary"]
.Kotlin DSL
----
@Bean
fun retrievingGatewayFlow() =
integrationFlow {
handle(Jpa.retrievingGateway(this.entityManagerFactory)
.jpaQuery("from Student s where s.id = :id")
.expectSingleResult(true)
.parameterExpression("id", "payload"))
channel { queue("retrieveResults") }
}
----
[source, java, role="secondary"]
.Java
----
@SpringBootApplication
@EntityScan(basePackageClasses = StudentDomain.class)
public class JpaJavaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(JpaJavaApplication.class)
.web(false)
.run(args);
}
@Autowired
private EntityManagerFactory entityManagerFactory;
@Bean
public JpaExecutor jpaExecutor() {
JpaExecutor executor = new JpaExecutor(this.entityManagerFactory);
jpaExecutor.setJpaQuery("from Student s where s.id = :id");
executor.setJpaParameters(Collections.singletonList(new JpaParameter("id", null, "payload")));
jpaExecutor.setExpectSingleResult(true);
return executor;
}
@Bean
@ServiceActivator(channel = "jpaRetrievingChannel")
public MessageHandler jpaOutbound() {
JpaOutboundGateway adapter = new JpaOutboundGateway(jpaExecutor());
adapter.setOutputChannelName("retrieveResults");
adapter.setGatewayType(OutboundGatewayType.RETRIEVING);
return adapter;
}
}
----
[source, xml, role="secondary"]
.XML
----
<int-jpa:retrieving-outbound-gateway request-channel=""
auto-startup="true"
@@ -1075,86 +1154,6 @@ Version 3.0 introduced this attribute.
Optional.
====
The remaining attributes are described earlier in this chapter.
See <<jpaInboundChannelAdapterParameters>> and <<jpaOutboundChannelAdapterParameters>>.
==== Configuring with Java Configuration
The following Spring Boot application shows an example of how configure the outbound adapter with Java:
====
[source, java]
----
@SpringBootApplication
@EntityScan(basePackageClasses = StudentDomain.class)
public class JpaJavaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(JpaJavaApplication.class)
.web(false)
.run(args);
}
@Autowired
private EntityManagerFactory entityManagerFactory;
@Bean
public JpaExecutor jpaExecutor() {
JpaExecutor executor = new JpaExecutor(this.entityManagerFactory);
jpaExecutor.setJpaQuery("from Student s where s.id = :id");
executor.setJpaParameters(Collections.singletonList(new JpaParameter("id", null, "payload")));
jpaExecutor.setExpectSingleResult(true);
return executor;
}
@Bean
@ServiceActivator(channel = "jpaRetrievingChannel")
public MessageHandler jpaOutbound() {
JpaOutboundGateway adapter = new JpaOutboundGateway(jpaExecutor());
adapter.setOutputChannelName("retrieveResults");
adapter.setGatewayType(OutboundGatewayType.RETRIEVING);
return adapter;
}
}
----
====
==== Configuring with the Java DSL
The following Spring Boot application shows an example of how to configure the outbound adapter with the Java DSL:
====
[source, java]
----
@SpringBootApplication
@EntityScan(basePackageClasses = StudentDomain.class)
public class JpaJavaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(JpaJavaApplication.class)
.web(false)
.run(args);
}
@Autowired
private EntityManagerFactory entityManagerFactory;
@Bean
public IntegrationFlow retrievingGatewayFlow() {
return f -> f
.handle(Jpa.retrievingGateway(this.entityManagerFactory)
.jpaQuery("from Student s where s.id = :id")
.expectSingleResult(true)
.parameterExpression("id", "payload"))
.channel(c -> c.queue("retrieveResults"));
}
}
----
====
[IMPORTANT]
====
When you choose to delete entities upon retrieval, and you have retrieved a collection of entities, by default, entities are deleted on a per-entity basis.
@@ -1171,6 +1170,12 @@ It does not cascade to related entities.`"
For more information, see https://jcp.org/en/jsr/detail?id=317[JSR 317: Java™ Persistence 2.0]
====
NOTE: Starting with version 6.0, the `Jpa.retrievingGateway()` returns an empty list result when there are no entities returned by the query.
Previously `null` was returned ending the flow, or throwing an exception, depending on `requiresReply`.
Or, to revert to the previous behavior, add a `filter` after the gateway to filter out empty lists.
It requires extra configuration in applications where empty list handling is a part of the downstream logic.
See <<./splitter.adoc#split-stream-and-flux,Splitter Discard Channel>> for possible empty list handling options.
[[outboundGatewaySamples]]
==== JPA Outbound Gateway Samples

View File

@@ -100,6 +100,9 @@ See <<./gateway.adoc#async-gateway, Asynchronous Gateway>> for more information.
The `integrationGlobalProperties` bean is now declared by the framework as an instance of `org.springframework.integration.context.IntegrationProperties` instead of the previously deprecated `java.util.Properties`.
Message handlers which produce a collection as a reply (e.g. `JpaOutboundGateway`, `JdbcOutboundGateway` and other DB-based gateways) now return an empty result list if no records are returned by the query.
Previously, `null` was returned ending the flow, or throwing an exception, depending on `requiresReply`.
[[x6.0-rmi]]
=== RMI Removal