Skip to content

Commit f7dd876

Browse files
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 <[email protected]> Co-authored-by: Gary Russell <[email protected]>
1 parent 6641b1f commit f7dd876

File tree

8 files changed

+125
-131
lines changed

8 files changed

+125
-131
lines changed

spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/JdbcOutboundGateway.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -221,9 +221,6 @@ protected Object handleRequestMessage(Message<?> requestMessage) {
221221
list = this.poller.doPoll(sqlQueryParameterSource);
222222
}
223223
Object payload = list;
224-
if (list.isEmpty()) {
225-
return null;
226-
}
227224
if (list.size() == 1 && (this.maxRows == null || this.maxRows == 1)) {
228225
payload = list.get(0);
229226
}

spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaExecutor.java

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -51,7 +51,6 @@
5151
* <li>JpQl Named Query</li>
5252
* <li>Sql Native Named Query</li>
5353
* </ul>.
54-
*
5554
* When objects are being retrieved, it also possibly to:
5655
*
5756
* <ul>
@@ -176,8 +175,8 @@ public void setEntityClass(Class<?> entityClass) {
176175
* @param jpaQuery The provided JPA query must neither be null nor empty.
177176
*/
178177
public void setJpaQuery(String jpaQuery) {
179-
Assert.isTrue(this.nativeQuery == null && this.namedQuery == null, "You can define only one of the "
180-
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
178+
Assert.isTrue(this.nativeQuery == null && this.namedQuery == null,
179+
"Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
181180
Assert.hasText(jpaQuery, "jpaQuery must neither be null nor empty.");
182181
this.jpaQuery = jpaQuery;
183182
}
@@ -190,8 +189,8 @@ public void setJpaQuery(String jpaQuery) {
190189
* @param nativeQuery The provided SQL query must neither be null nor empty.
191190
*/
192191
public void setNativeQuery(String nativeQuery) {
193-
Assert.isTrue(this.namedQuery == null && this.jpaQuery == null, "You can define only one of the "
194-
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
192+
Assert.isTrue(this.namedQuery == null && this.jpaQuery == null,
193+
"Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
195194
Assert.hasText(nativeQuery, "nativeQuery must neither be null nor empty.");
196195
this.nativeQuery = nativeQuery;
197196
}
@@ -202,8 +201,8 @@ public void setNativeQuery(String nativeQuery) {
202201
* @param namedQuery Must neither be null nor empty
203202
*/
204203
public void setNamedQuery(String namedQuery) {
205-
Assert.isTrue(this.jpaQuery == null && this.nativeQuery == null, "You can define only one of the "
206-
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
204+
Assert.isTrue(this.jpaQuery == null && this.nativeQuery == null,
205+
"Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
207206
Assert.hasText(namedQuery, "namedQuery must neither be null nor empty.");
208207
this.namedQuery = namedQuery;
209208
}
@@ -312,7 +311,6 @@ public void setParameterSource(ParameterSource parameterSource) {
312311
* <p>If set to <code>false</code>, the complete result list is returned as the
313312
* payload.
314313
* @param expectSingleResult true if a single object is expected.
315-
*
316314
*/
317315
public void setExpectSingleResult(boolean expectSingleResult) {
318316
this.expectSingleResult = expectSingleResult;
@@ -339,7 +337,7 @@ public void setIdExpression(Expression idExpression) {
339337
}
340338

341339
/**
342-
* Set the expression for maximum number of results expression. It has be a non null value
340+
* Set the expression for maximum number of results expression. It has to be a non-null value
343341
* Not setting one will default to the behavior of fetching all the records
344342
* @param maxResultsExpression The maximum results expression.
345343
*/
@@ -413,7 +411,7 @@ else if (this.flush) {
413411

414412
/**
415413
* Execute the actual Jpa Operation. Call this method, if you need access to
416-
* process return values. This methods return a Map that contains either
414+
* process return values. These methods return a Map that contains either
417415
* the number of affected entities or the affected entity itself.
418416
*<p>Keep in mind that the number of entities effected by the operation may
419417
* not necessarily correlate with the number of rows effected in the database.
@@ -497,7 +495,7 @@ public Object poll(@Nullable final Message<?> requestMessage) {
497495
payload = this.jpaOperations.find(entityClazz, id);
498496
}
499497
else {
500-
final List<?> result;
498+
List<?> result;
501499
int maxNumberOfResults = evaluateExpressionForNumericResult(requestMessage, this.maxResultsExpression);
502500
if (requestMessage == null) {
503501
result = doPoll(this.parameterSource, 0, maxNumberOfResults);
@@ -511,27 +509,18 @@ public Object poll(@Nullable final Message<?> requestMessage) {
511509
result = doPoll(paramSource, firstResult, maxNumberOfResults);
512510
}
513511

514-
if (result.isEmpty()) {
515-
payload = null;
516-
}
517-
else {
518-
if (this.expectSingleResult) {
519-
if (result.size() == 1) {
520-
payload = result.iterator().next();
521-
}
522-
else if (requestMessage != null) {
523-
throw new MessagingException(requestMessage,
524-
"The Jpa operation returned more than 1 result for expectSingleResult mode.");
525-
}
526-
else {
527-
throw new MessagingException(
528-
"The Jpa operation returned more than 1 result for expectSingleResult mode.");
529-
}
512+
if (this.expectSingleResult && !result.isEmpty()) {
513+
if (result.size() == 1) {
514+
payload = result.iterator().next();
530515
}
531516
else {
532-
payload = result;
517+
throw new MessagingException(requestMessage, // NOSONAR
518+
"The Jpa operation returned more than 1 result for expectSingleResult mode.");
533519
}
534520
}
521+
else {
522+
payload = result;
523+
}
535524
}
536525

537526
checkDelete(payload);

spring-integration-jpa/src/main/java/org/springframework/integration/jpa/inbound/JpaPollingChannelAdapter.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import org.springframework.integration.endpoint.AbstractMessageSource;
2020
import org.springframework.integration.jpa.core.JpaExecutor;
2121
import org.springframework.util.Assert;
22+
import org.springframework.util.ObjectUtils;
2223

2324
/**
2425
* Polling message source that produces messages from the result of the provided:
@@ -30,11 +31,8 @@
3031
* <li>JpQl Named Query</li>
3132
* <li>Sql Native Named Query</li>
3233
* </ul>.
33-
*
3434
* After the objects have been polled, it also possibly to either:
3535
*
36-
* executes an update after the select possibly to updated the state of selected records
37-
*
3836
* <ul>
3937
* <li>executes an update (per retrieved object or for the entire payload)</li>
4038
* <li>delete the retrieved object</li>
@@ -73,13 +71,14 @@ protected void onInit() {
7371

7472
/**
7573
* Use {@link JpaExecutor#poll()} to executes the JPA operation.
76-
* If {@link JpaExecutor#poll()} returns null, this method will return
77-
* <code>null</code>. Otherwise, a new {@link org.springframework.messaging.Message}
78-
* is constructed and returned.
74+
* Return {@code null} if result of {@link JpaExecutor#poll()} is {@link ObjectUtils#isEmpty}.
75+
* The empty collection means there is no data to retrieve from DB at the moment therefore
76+
* no reason to emit an empty message from this message source.
7977
*/
8078
@Override
8179
protected Object doReceive() {
82-
return this.jpaExecutor.poll();
80+
Object result = this.jpaExecutor.poll();
81+
return ObjectUtils.isEmpty(result) ? null : result;
8382
}
8483

8584
@Override

spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/JpaExecutorTests.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,13 @@ public void testSetMultipleQueryTypes() {
9494

9595
assertThatIllegalArgumentException()
9696
.isThrownBy(() -> executor.setNamedQuery("NamedQuery"))
97-
.withMessage("You can define only one of the "
98-
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
97+
.withMessage("Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
9998

10099
assertThat(TestUtils.getPropertyValue(executor, "namedQuery")).isNull();
101100

102101
assertThatIllegalArgumentException()
103102
.isThrownBy(() -> executor.setNativeQuery("select * from Student"))
104-
.withMessage("You can define only one of the "
105-
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
103+
.withMessage("Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
106104

107105
assertThat(TestUtils.getPropertyValue(executor, "nativeQuery")).isNull();
108106

@@ -112,8 +110,7 @@ public void testSetMultipleQueryTypes() {
112110

113111
assertThatIllegalArgumentException()
114112
.isThrownBy(() -> executor2.setJpaQuery("select s from Student s"))
115-
.withMessage("You can define only one of the "
116-
+ "properties 'jpaQuery', 'nativeQuery', 'namedQuery'");
113+
.withMessage("Only one of the properties 'jpaQuery', 'nativeQuery', 'namedQuery' can be defined");
117114

118115
assertThat(TestUtils.getPropertyValue(executor2, "jpaQuery")).isNull();
119116
}

spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundGatewayTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -160,7 +160,7 @@ public void testDeleteMany() {
160160
List<StudentDomain> allStudents = this.studentService.getAllStudents();
161161
assertThat(allStudents).hasSize(3);
162162
this.studentService.deleteStudents(allStudents);
163-
assertThat(this.studentService.getAllStudents()).isNull();
163+
assertThat(this.studentService.getAllStudents()).isEmpty();
164164
}
165165

166166
}

src/reference/asciidoc/jdbc.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,10 @@ It can also have a `SqlParameterSourceFactory` injected to control the binding o
365365
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`.
366366
It lets you specify a `MessagePreparedStatementSetter` bean reference, which implements more sophisticated `PreparedStatement` preparation before its execution.
367367

368+
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".
369+
This caused an extra configuration in applications where handling of empty lists is a part of downstream logic.
370+
See <<./splitter.adoc#split-stream-and-flux,Splitter Discard Channel>> for possible empty list handling option.
371+
368372
See <<jdbc-outbound-channel-adapter>> for more information about `MessagePreparedStatementSetter`.
369373

370374
[[jdbc-message-store]]

0 commit comments

Comments
 (0)