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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user