Fix ambiguity in the JpaExecutor (#2640)

* Fix ambiguity in the `JpaExecutor`

Since we can't mutate a provided `ParameterSourceFactory` with the
provided `jpaParameters`, we have to reject such a combined
configuration in favor of the advice to configure params on the
provided `ParameterSourceFactory`

* Throw `IllegalStateException` when `ParameterSourceFactory` and
`jpaParameters` are configured together on the `JpaExecutor`
* Some refactoring for `JpaExecutor` to have a consistent code style
* Fix tests according new logic
* Document such an ambiguity configuration to avoid confusion

**Cherry-pick to 5.0.x**

* * Use `assertThatThrownBy()` in the `JpaExecutorTests`
* Fix "No BeanFactory" warning in the `JpaExecutorTests`

* * Resolve potential NPE and add `@NonNullApi` with `@Nullable` on the `poll()`
This commit is contained in:
Artem Bilan
2018-11-27 11:00:40 -05:00
committed by Gary Russell
parent f6e9e6649a
commit d981171972
7 changed files with 363 additions and 340 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2018 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.
@@ -36,6 +36,7 @@ import org.springframework.integration.jpa.support.parametersource.BeanPropertyP
import org.springframework.integration.jpa.support.parametersource.ExpressionEvaluatingParameterSourceFactory;
import org.springframework.integration.jpa.support.parametersource.ParameterSource;
import org.springframework.integration.jpa.support.parametersource.ParameterSourceFactory;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
@@ -59,11 +60,12 @@ import org.springframework.util.CollectionUtils;
* </ul>
*
* If neither entityClass nor any other query is specified then the entity-class
* is "guessed" from the {@link Message} payload.
* is "guessed" from the {@link Message} payload.idExpression
*
* @author Gunnar Hillert
* @author Amol Nayak
* @author Artem Bilan
*
* @since 2.2
*
*/
@@ -71,39 +73,39 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
private final JpaOperations jpaOperations;
private volatile List<JpaParameter> jpaParameters;
private List<JpaParameter> jpaParameters;
private volatile Class<?> entityClass;
private Class<?> entityClass;
private volatile String jpaQuery;
private String jpaQuery;
private volatile String nativeQuery;
private String nativeQuery;
private volatile String namedQuery;
private String namedQuery;
private volatile Expression maxResultsExpression;
private Expression maxResultsExpression;
private volatile Expression firstResultExpression;
private Expression firstResultExpression;
private volatile Expression idExpression;
private Expression idExpression;
private volatile PersistMode persistMode = PersistMode.MERGE;
private PersistMode persistMode = PersistMode.MERGE;
private volatile ParameterSourceFactory parameterSourceFactory = null;
private ParameterSourceFactory parameterSourceFactory = null;
private volatile ParameterSource parameterSource;
private ParameterSource parameterSource;
private volatile boolean flush = false;
private boolean flush = false;
private volatile int flushSize = 0;
private int flushSize = 0;
private volatile boolean clearOnFlush = false;
private boolean clearOnFlush = false;
private volatile boolean deleteAfterPoll = false;
private boolean deleteAfterPoll = false;
private volatile boolean deleteInBatch = false;
private boolean deleteInBatch = false;
private volatile boolean expectSingleResult = false;
private boolean expectSingleResult = false;
/**
* Indicates that whether only the payload of the passed in {@link Message}
@@ -111,11 +113,11 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
* default a {@link BeanPropertyParameterSourceFactory} implementation is
* used for the sqlParameterSourceFactory property.
*/
private volatile Boolean usePayloadAsParameterSource = null;
private Boolean usePayloadAsParameterSource = null;
private volatile BeanFactory beanFactory;
private BeanFactory beanFactory;
private volatile EvaluationContext evaluationContext;
private EvaluationContext evaluationContext;
/**
* Constructor taking an {@link EntityManagerFactory} from which the
@@ -161,274 +163,6 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
this.evaluationContext = evaluationContext;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* Verify and sets the parameters. E.g. initializes the to be used
* {@link ParameterSourceFactory}.
*/
@Override
public void afterPropertiesSet() {
if (!CollectionUtils.isEmpty(this.jpaParameters)) {
if (this.parameterSourceFactory == null) {
ExpressionEvaluatingParameterSourceFactory expressionSourceFactory =
new ExpressionEvaluatingParameterSourceFactory(this.beanFactory);
expressionSourceFactory.setParameters(this.jpaParameters);
this.parameterSourceFactory = expressionSourceFactory;
}
else {
if (!(this.parameterSourceFactory instanceof ExpressionEvaluatingParameterSourceFactory)) {
throw new IllegalStateException("You are providing 'JpaParameters'. "
+ "Was expecting the the provided jpaParameterSourceFactory "
+ "to be an instance of 'ExpressionEvaluatingJpaParameterSourceFactory', "
+ "however the provided one is of type '" + this.parameterSourceFactory.getClass().getName() + "'");
}
}
if (this.usePayloadAsParameterSource == null) {
this.usePayloadAsParameterSource = false;
}
}
else {
if (this.parameterSourceFactory == null) {
this.parameterSourceFactory = new BeanPropertyParameterSourceFactory();
}
if (this.usePayloadAsParameterSource == null) {
this.usePayloadAsParameterSource = true;
}
}
if (this.flushSize > 0) {
this.flush = true;
}
else if (this.flush) {
this.flushSize = 1;
}
if (this.evaluationContext == null) {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.beanFactory);
}
}
/**
* Execute the actual Jpa Operation. Call this method, if you need access to
* process return values. This 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.
* @param message The message.
* @return Either the number of affected entities when using a JPQL query.
* When using a merge/persist the updated/inserted itself is returned.
*/
public Object executeOutboundJpaOperation(final Message<?> message) {
final Object result;
ParameterSource parameterSource = null;
if (this.jpaQuery != null || this.nativeQuery != null || this.namedQuery != null) {
parameterSource = this.determineParameterSource(message);
}
if (this.jpaQuery != null) {
result = this.jpaOperations.executeUpdate(this.jpaQuery, parameterSource);
}
else if (this.nativeQuery != null) {
result = this.jpaOperations.executeUpdateWithNativeQuery(this.nativeQuery, parameterSource);
}
else if (this.namedQuery != null) {
result = this.jpaOperations.executeUpdateWithNamedQuery(this.namedQuery, parameterSource);
}
else {
if (PersistMode.PERSIST.equals(this.persistMode)) {
this.jpaOperations.persist(message.getPayload(), this.flushSize, this.clearOnFlush);
result = message.getPayload();
}
else if (PersistMode.MERGE.equals(this.persistMode)) {
result = this.jpaOperations.merge(message.getPayload(), this.flushSize, this.clearOnFlush);
}
else if (PersistMode.DELETE.equals(this.persistMode)) {
this.jpaOperations.delete(message.getPayload());
if (this.flush) {
this.jpaOperations.flush();
}
result = message.getPayload();
}
else {
throw new IllegalStateException(String.format("Unsupported PersistMode: '%s'", this.persistMode.name()));
}
}
return result;
}
/**
* Execute a (typically retrieving) JPA operation. The <i>requestMessage</i>
* can be used to provide additional query parameters using
* {@link JpaExecutor#parameterSourceFactory}. If the
* <i>requestMessage</i> parameter is null then
* {@link JpaExecutor#parameterSource} is being used for providing query parameters.
* @param requestMessage May be null.
* @return The payload object, which may be null.
*/
@SuppressWarnings("unchecked")
public Object poll(final Message<?> requestMessage) {
final Object payload;
if (this.idExpression != null) {
Object id = this.idExpression.getValue(this.evaluationContext, requestMessage);
Class<?> entityClass = this.entityClass;
if (entityClass == null) {
entityClass = requestMessage.getPayload().getClass();
}
payload = this.jpaOperations.find(entityClass, id);
}
else {
final List<?> result;
int maxNumberOfResults = this.evaluateExpressionForNumericResult(requestMessage, this.maxResultsExpression);
if (requestMessage == null) {
result = this.doPoll(this.parameterSource, 0, maxNumberOfResults);
}
else {
int firstResult = 0;
if (this.firstResultExpression != null) {
firstResult = this.getFirstResult(requestMessage);
}
ParameterSource parameterSource = this.determineParameterSource(requestMessage);
result = this.doPoll(parameterSource, firstResult, maxNumberOfResults);
}
if (result.isEmpty()) {
payload = null;
}
else {
if (this.expectSingleResult) {
if (result.size() == 1) {
payload = result.iterator().next();
}
else {
throw new MessagingException(requestMessage,
"The Jpa operation returned more than 1 result object but expectSingleResult was 'true'.");
}
}
else {
payload = result;
}
}
}
if (payload != null && this.deleteAfterPoll) {
if (payload instanceof Iterable) {
if (this.deleteInBatch) {
this.jpaOperations.deleteInBatch((Iterable<Object>) payload);
}
else {
for (Object entity : (Iterable<?>) payload) {
this.jpaOperations.delete(entity);
}
}
}
else {
this.jpaOperations.delete(payload);
}
if (this.flush) {
this.jpaOperations.flush();
}
}
return payload;
}
private int getFirstResult(final Message<?> requestMessage) {
return this.evaluateExpressionForNumericResult(requestMessage, this.firstResultExpression);
}
private int evaluateExpressionForNumericResult(final Message<?> requestMessage, Expression expression) {
int evaluatedResult = 0;
if (expression != null) {
Object evaluationResult = expression.getValue(this.evaluationContext, requestMessage);
if (evaluationResult != null) {
if (evaluationResult instanceof Number) {
evaluatedResult = ((Number) evaluationResult).intValue();
}
else if (evaluationResult instanceof String) {
try {
evaluatedResult = Integer.parseInt((String) evaluationResult);
}
catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Value " + evaluationResult + " passed as cannot be " +
"parsed to a number, expected to be numeric");
}
}
else {
throw new IllegalArgumentException("Expected the value to be a Number" +
" got " + evaluationResult.getClass().getName());
}
}
}
return evaluatedResult;
}
private ParameterSource determineParameterSource(final Message<?> requestMessage) {
ParameterSource parameterSource;
if (this.usePayloadAsParameterSource) {
parameterSource = this.parameterSourceFactory.createParameterSource(requestMessage.getPayload());
}
else {
parameterSource = this.parameterSourceFactory.createParameterSource(requestMessage);
}
return parameterSource;
}
/**
* Execute the JPA operation. Delegates to {@link JpaExecutor#poll(Message)}.
* @return The object or null.
*/
public Object poll() {
return this.poll(null);
}
protected List<?> doPoll(ParameterSource jpaQLParameterSource, int firstResult, int maxNumberOfResults) {
List<?> payload = null;
if (this.jpaQuery != null) {
payload = this.jpaOperations.getResultListForQuery(this.jpaQuery, jpaQLParameterSource,
firstResult, maxNumberOfResults);
}
else if (this.nativeQuery != null) {
payload = this.jpaOperations.getResultListForNativeQuery(this.nativeQuery, this.entityClass, jpaQLParameterSource,
firstResult, maxNumberOfResults);
}
else if (this.namedQuery != null) {
payload = this.jpaOperations.getResultListForNamedQuery(this.namedQuery, jpaQLParameterSource,
firstResult, maxNumberOfResults);
}
else if (this.entityClass != null) {
payload = this.jpaOperations.getResultListForClass(this.entityClass, firstResult, maxNumberOfResults);
}
else {
throw new IllegalStateException("For the polling operation, one of "
+ "the following properties must be specified: "
+ "query, namedQuery or entityClass.");
}
return payload;
}
/**
* Sets the class type which is being used for retrieving entities from the
* database.
@@ -517,7 +251,8 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
/**
* If set to {@code true} the {@link javax.persistence.EntityManager#clear()} will be called,
* and only if the {@link javax.persistence.EntityManager#flush()} was called after performing persistence operations.
* and only if the {@link javax.persistence.EntityManager#flush()} was called after performing persistence
* operations.
* @param clearOnFlush defaults to 'false'.
* @see #setFlush(boolean)
* @see #setFlushSize(int)
@@ -628,4 +363,270 @@ public class JpaExecutor implements InitializingBean, BeanFactoryAware {
this.setMaxResultsExpression(new LiteralExpression("" + maxNumberOfResults));
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* Verify and sets the parameters. E.g. initializes the to be used
* {@link ParameterSourceFactory}.
*/
@Override
public void afterPropertiesSet() {
if (!CollectionUtils.isEmpty(this.jpaParameters)) {
if (this.parameterSourceFactory == null) {
ExpressionEvaluatingParameterSourceFactory expressionSourceFactory =
new ExpressionEvaluatingParameterSourceFactory(this.beanFactory);
expressionSourceFactory.setParameters(this.jpaParameters);
this.parameterSourceFactory = expressionSourceFactory;
}
else {
throw new IllegalStateException("The 'jpaParameters' and 'parameterSourceFactory' " +
"are mutually exclusive. Consider to configure parameters on the provided " +
"'parameterSourceFactory': " + this.parameterSourceFactory);
}
if (this.usePayloadAsParameterSource == null) {
this.usePayloadAsParameterSource = false;
}
}
else {
if (this.parameterSourceFactory == null) {
this.parameterSourceFactory = new BeanPropertyParameterSourceFactory();
}
if (this.usePayloadAsParameterSource == null) {
this.usePayloadAsParameterSource = true;
}
}
if (this.flushSize > 0) {
this.flush = true;
}
else if (this.flush) {
this.flushSize = 1;
}
if (this.evaluationContext == null) {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.beanFactory);
}
}
/**
* Execute the actual Jpa Operation. Call this method, if you need access to
* process return values. This 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.
* @param message The message.
* @return Either the number of affected entities when using a JPQL query.
* When using a merge/persist the updated/inserted itself is returned.
*/
public Object executeOutboundJpaOperation(final Message<?> message) {
final Object result;
ParameterSource parameterSource = null;
if (this.jpaQuery != null || this.nativeQuery != null || this.namedQuery != null) {
parameterSource = this.determineParameterSource(message);
}
if (this.jpaQuery != null) {
result = this.jpaOperations.executeUpdate(this.jpaQuery, parameterSource);
}
else if (this.nativeQuery != null) {
result = this.jpaOperations.executeUpdateWithNativeQuery(this.nativeQuery, parameterSource);
}
else if (this.namedQuery != null) {
result = this.jpaOperations.executeUpdateWithNamedQuery(this.namedQuery, parameterSource);
}
else {
switch (this.persistMode) {
case PERSIST:
this.jpaOperations.persist(message.getPayload(), this.flushSize, this.clearOnFlush);
result = message.getPayload();
break;
case MERGE:
result = this.jpaOperations.merge(message.getPayload(), this.flushSize, this.clearOnFlush);
break;
case DELETE:
this.jpaOperations.delete(message.getPayload());
if (this.flush) {
this.jpaOperations.flush();
}
result = message.getPayload();
break;
default:
throw new IllegalStateException("Unsupported PersistMode: " + this.persistMode.name());
}
}
return result;
}
/**
* Execute the JPA operation. Delegates to {@link JpaExecutor#poll(Message)}.
* @return The object or null.
*/
public Object poll() {
return poll(null);
}
/**
* Execute a (typically retrieving) JPA operation. The <i>requestMessage</i>
* can be used to provide additional query parameters using
* {@link JpaExecutor#parameterSourceFactory}. If the
* <i>requestMessage</i> parameter is null then
* {@link JpaExecutor#parameterSource} is being used for providing query parameters.
* @param requestMessage May be null.
* @return The payload object, which may be null.
*/
public Object poll(@Nullable final Message<?> requestMessage) {
final Object payload;
if (this.idExpression != null) {
Object id = this.idExpression.getValue(this.evaluationContext, requestMessage); // NOSONAR It can be null
Assert.state(id != null, "The 'idExpression' cannot evaluate to null.");
Class<?> entityClass = this.entityClass;
if (entityClass == null && requestMessage != null) {
entityClass = requestMessage.getPayload().getClass();
}
payload = this.jpaOperations.find(entityClass, id);
}
else {
final List<?> result;
int maxNumberOfResults = evaluateExpressionForNumericResult(requestMessage, this.maxResultsExpression);
if (requestMessage == null) {
result = doPoll(this.parameterSource, 0, maxNumberOfResults);
}
else {
int firstResult = 0;
if (this.firstResultExpression != null) {
firstResult = getFirstResult(requestMessage);
}
ParameterSource parameterSource = determineParameterSource(requestMessage);
result = doPoll(parameterSource, 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.");
}
}
else {
payload = result;
}
}
}
if (payload != null && this.deleteAfterPoll) {
if (payload instanceof Iterable) {
if (this.deleteInBatch) {
this.jpaOperations.deleteInBatch((Iterable<?>) payload);
}
else {
for (Object entity : (Iterable<?>) payload) {
this.jpaOperations.delete(entity);
}
}
}
else {
this.jpaOperations.delete(payload);
}
if (this.flush) {
this.jpaOperations.flush();
}
}
return payload;
}
protected List<?> doPoll(ParameterSource jpaQLParameterSource, int firstResult, int maxNumberOfResults) {
List<?> payload;
if (this.jpaQuery != null) {
payload =
this.jpaOperations.getResultListForQuery(this.jpaQuery, jpaQLParameterSource,
firstResult, maxNumberOfResults);
}
else if (this.nativeQuery != null) {
payload =
this.jpaOperations.getResultListForNativeQuery(this.nativeQuery, this.entityClass,
jpaQLParameterSource, firstResult, maxNumberOfResults);
}
else if (this.namedQuery != null) {
payload =
this.jpaOperations.getResultListForNamedQuery(this.namedQuery, jpaQLParameterSource,
firstResult, maxNumberOfResults);
}
else if (this.entityClass != null) {
payload = this.jpaOperations.getResultListForClass(this.entityClass, firstResult, maxNumberOfResults);
}
else {
throw new IllegalStateException("For the polling operation, one of "
+ "the following properties must be specified: "
+ "query, namedQuery or entityClass.");
}
return payload;
}
private int getFirstResult(final Message<?> requestMessage) {
return evaluateExpressionForNumericResult(requestMessage, this.firstResultExpression);
}
private int evaluateExpressionForNumericResult(@Nullable final Message<?> requestMessage,
@Nullable Expression expression) {
int evaluatedResult = 0;
if (expression != null) {
Object evaluationResult = expression.getValue(this.evaluationContext, requestMessage); // NOSONAR can be null
if (evaluationResult != null) {
if (evaluationResult instanceof Number) {
evaluatedResult = ((Number) evaluationResult).intValue();
}
else if (evaluationResult instanceof String) {
try {
evaluatedResult = Integer.parseInt((String) evaluationResult);
}
catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Value " + evaluationResult + " passed as cannot be " +
"parsed to a number, expected to be numeric");
}
}
else {
throw new IllegalArgumentException("Expected the value to be a Number got "
+ evaluationResult.getClass().getName());
}
}
}
return evaluatedResult;
}
private ParameterSource determineParameterSource(final Message<?> requestMessage) {
ParameterSource parameterSource;
if (this.usePayloadAsParameterSource) {
parameterSource = this.parameterSourceFactory.createParameterSource(requestMessage.getPayload());
}
else {
parameterSource = this.parameterSourceFactory.createParameterSource(requestMessage);
}
return parameterSource;
}
}

View File

@@ -1,4 +1,5 @@
/**
* Provides core classes of the JPA module.
*/
@org.springframework.lang.NonNullApi
package org.springframework.integration.jpa.core;

View File

@@ -90,12 +90,13 @@
<xsd:annotation>
<xsd:documentation>
<![CDATA[
Provides a mechanism to provide
Provides a mechanism to configure
parameters for the queries that are either based
on the Java Persistence Query Language (JPQL) or
native SQL queries.
Parameters can also be provided for Named Queries.
Mutually exclusive with the 'parameter-source-factory' attribute.
]]>
</xsd:documentation>
</xsd:annotation>
@@ -129,12 +130,12 @@
<xsd:union memberTypes="xsd:boolean xsd:string" />
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="parameter-source-factory"
type="xsd:string">
<xsd:attribute name="parameter-source-factory" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<xsd:documentation>
Reference to a ParameterSourceFactory.
Mutually exclusive with the 'parameter' sub-element(s).
</xsd:documentation>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.jpa.support.parametersource.ParameterSourceFactory" />
@@ -188,8 +189,8 @@
parameters for the queries that are either based
on the Java Persistence Query Language (JPQL) or
native SQL queries.
Parameters can also be provided for Named Queries.
Mutually exclusive with the 'parameter-source-factory' attribute.
]]>
</xsd:documentation>
</xsd:annotation>
@@ -222,8 +223,8 @@
parameters for the queries that are either based
on the Java Persistence Query Language (JPQL) or
native SQL queries.
Parameters can also be provided for Named Queries.
Mutually exclusive with the 'parameter-source-factory' attribute.
]]>
</xsd:documentation>
</xsd:annotation>
@@ -395,8 +396,8 @@
</xsd:appinfo>
<xsd:documentation>
The parameter source factory that would be used for evaluating the
parameters of the response JPA QL that would be evaluated
JPA outbound gateway
parameters of the response JPA QL that would be evaluated JPA outbound gateway.
Mutually exclusive with the 'parameter' sub-element(s).
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>

View File

@@ -16,6 +16,7 @@
package org.springframework.integration.jpa.core;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -36,12 +37,12 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.jpa.support.JpaParameter;
import org.springframework.integration.jpa.support.parametersource.ExpressionEvaluatingParameterSourceFactory;
import org.springframework.integration.jpa.support.parametersource.ParameterSourceFactory;
import org.springframework.integration.jpa.test.entity.StudentDomain;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
/**
@@ -52,8 +53,7 @@ import org.springframework.transaction.annotation.Transactional;
*
* @since 2.2
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@RunWith(SpringRunner.class)
public class JpaExecutorTests {
@Autowired
@@ -70,6 +70,7 @@ public class JpaExecutorTests {
@Test
public void testExecutePollWithNoEntityClassSpecified() {
JpaExecutor jpaExecutor = new JpaExecutor(mock(EntityManager.class));
jpaExecutor.setBeanFactory(mock(BeanFactory.class));
jpaExecutor.afterPropertiesSet();
try {
jpaExecutor.poll();
@@ -191,6 +192,7 @@ public class JpaExecutorTests {
executor.setJpaQuery(query);
executor.setExpectSingleResult(true);
executor.setUsePayloadAsParameterSource(false);
executor.setBeanFactory(mock(BeanFactory.class));
executor.afterPropertiesSet();
return executor;
}
@@ -205,6 +207,7 @@ public class JpaExecutorTests {
executor.setJpaQuery(query);
executor.setExpectSingleResult(true);
executor.setUsePayloadAsParameterSource(true);
executor.setBeanFactory(mock(BeanFactory.class));
executor.afterPropertiesSet();
return executor;
}
@@ -274,4 +277,16 @@ public class JpaExecutorTests {
fail("Expected the test case to throw an exception");
}
@Test
public void testParameterSourceFactoryAndJpaParameters() {
JpaExecutor executor = new JpaExecutor(this.entityManager);
ParameterSourceFactory parameterSourceFactory = new ExpressionEvaluatingParameterSourceFactory();
executor.setParameterSourceFactory(parameterSourceFactory);
executor.setJpaParameters(Collections.singletonList(new JpaParameter("firstName", null, "#this")));
assertThatThrownBy(executor::afterPropertiesSet)
.isExactlyInstanceOf(IllegalStateException.class)
.hasMessageStartingWith("The 'jpaParameters' and 'parameterSourceFactory' are mutually exclusive.");
}
}

View File

@@ -177,7 +177,7 @@ public class JpaTests {
@Test
public void testRetrievingGatewayFlow() {
this.retrievingGatewayFlowInput.send(MessageBuilder.withPayload(1002L).build());
this.retrievingGatewayFlowInput.send(MessageBuilder.withPayload("foo").setHeader("payloadId", 1002L).build());
Message<?> receive = this.retrieveResults.receive(10_000);
assertNotNull(receive);
assertThat(receive.getPayload(), instanceOf(StudentDomain.class));
@@ -252,7 +252,7 @@ public class JpaTests {
.handle(Jpa.retrievingGateway(entityManagerFactory)
.jpaQuery("from Student s where s.id = :id")
.expectSingleResult(true)
.parameterExpression("id", "payload"))
.parameterExpression("id", "headers[payloadId]"))
.channel(c -> c.queue("retrieveResults"));
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2018 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.
@@ -16,10 +16,16 @@
package org.springframework.integration.jpa.outbound;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.util.List;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -38,6 +44,7 @@ import org.springframework.transaction.annotation.Transactional;
* @author Gunnar Hillert
* @author Artem Bilan
* @author Amol Nayak
*
* @since 2.2
*/
@RunWith(SpringJUnit4ClassRunner.class)
@@ -60,21 +67,21 @@ public class JpaOutboundGatewayTests {
@Test
public void getStudent() {
final StudentDomain student = studentService.getStudent(1001L);
Assert.assertNotNull(student);
assertNotNull(student);
}
@Test
public void getAllStudentsStartingFromGivenRecord() {
List<?> students = studentService.getAllStudentsFromGivenRecord(1);
Assert.assertNotNull(students);
Assert.assertEquals(2, students.size());
assertNotNull(students);
assertEquals(2, students.size());
}
@Test
public void getAllStudentsWithMaxNumberOfRecords() {
List<?> students = studentService.getStudents(1);
Assert.assertNotNull(students);
Assert.assertEquals(1, students.size());
assertNotNull(students);
assertEquals(1, students.size());
}
@@ -86,27 +93,24 @@ public class JpaOutboundGatewayTests {
try {
studentService.deleteStudent(student);
fail("IllegalArgumentException is expected");
}
catch (IllegalArgumentException e) {
return;
assertThat(e.getMessage(), startsWith("Removing a detached instance"));
}
Assert.fail("Was expecting a MessageHandlingException to be thrown.");
}
@Test
public void getStudentWithException() {
try {
studentService.getStudentWithException(1001L);
fail("MessageHandlingException is expected");
}
catch (MessagingException e) {
Assert.assertEquals("The Jpa operation returned more than 1 result object but expectSingleResult was 'true'.",
assertEquals("The Jpa operation returned more than 1 result for expectSingleResult mode.",
e.getMessage());
return;
}
Assert.fail("Was expecting a MessageHandlingException to be thrown.");
}
@Test
@@ -114,15 +118,15 @@ public class JpaOutboundGatewayTests {
StudentDomain student = studentService.getStudentWithParameters("First Two");
Assert.assertEquals("First Two", student.getFirstName());
Assert.assertEquals("Last Two", student.getLastName());
assertEquals("First Two", student.getFirstName());
assertEquals("Last Two", student.getLastName());
}
@Test
public void getAllStudents() {
List<StudentDomain> students = studentService.getAllStudents();
Assert.assertNotNull(students);
Assert.assertTrue(students.size() == 3);
assertNotNull(students);
assertEquals(3, students.size());
}
@Test
@@ -130,11 +134,11 @@ public class JpaOutboundGatewayTests {
public void persistStudent() {
final StudentDomain studentToPersist = JpaTestUtils.getTestStudent();
Assert.assertNull(studentToPersist.getRollNumber());
assertNull(studentToPersist.getRollNumber());
final StudentDomain persistedStudent = studentService.persistStudent(studentToPersist);
Assert.assertNotNull(persistedStudent);
Assert.assertNotNull(persistedStudent.getRollNumber());
assertNotNull(persistedStudent);
assertNotNull(persistedStudent.getRollNumber());
}
@@ -143,18 +147,18 @@ public class JpaOutboundGatewayTests {
public void persistStudentUsingMerge() {
final StudentDomain studentToPersist = JpaTestUtils.getTestStudent();
Assert.assertNull(studentToPersist.getRollNumber());
assertNull(studentToPersist.getRollNumber());
final StudentDomain persistedStudent = studentService.persistStudentUsingMerge(studentToPersist);
Assert.assertNotNull(persistedStudent);
Assert.assertNotNull(persistedStudent.getRollNumber());
assertNotNull(persistedStudent);
assertNotNull(persistedStudent.getRollNumber());
}
@Test
public void testRetrievingGatewayInsideChain() {
final StudentDomain student = studentService.getStudent2(1001L);
Assert.assertNotNull(student);
assertNotNull(student);
}
@Test
@@ -162,18 +166,18 @@ public class JpaOutboundGatewayTests {
public void testUpdatingGatewayInsideChain() {
final StudentDomain studentToPersist = JpaTestUtils.getTestStudent();
Assert.assertNull(studentToPersist.getRollNumber());
assertNull(studentToPersist.getRollNumber());
final StudentDomain persistedStudent = studentService.persistStudent2(studentToPersist);
Assert.assertNotNull(persistedStudent);
Assert.assertNotNull(persistedStudent.getRollNumber());
assertNotNull(persistedStudent);
assertNotNull(persistedStudent.getRollNumber());
}
@Test
public void testJpaRepositoryAsService() {
List<StudentDomain> students = this.studentService.getStudentsUsingJpaRepository("F");
Assert.assertEquals(2, students.size());
assertEquals(2, students.size());
}
}

View File

@@ -368,7 +368,7 @@ If `false`, the `List` of entities is sent as the payload of the message.
The value defaults to `false`.
Optional.
<9> An implementation of `org.springframework.integration.jpa.core.JpaOperations` used to perform the JPA operations.
We recommend not providig an implementation of your own but using the default `org.springframework.integration.jpa.core.DefaultJpaOperations` implementation.
We recommend not providing an implementation of your own but using the default `org.springframework.integration.jpa.core.DefaultJpaOperations` implementation.
You can use any of the `entity-manager`, `entity-manager-factory`, or `jpa-operations` attributes.
Optional.
<10> The JPA QL to be executed by this adapter.
@@ -705,7 +705,7 @@ It defaults to `Ordered.LOWEST_PRECEDENCE`.
Optional.
<11> An instance of `o.s.i.jpa.support.parametersource.ParameterSourceFactory` used to get an instance of `o.s.i.jpa.support.parametersource.ParameterSource`, which is used to resolve the values of the parameters in the query.
Ignored if you perform operations by using a JPA entity.
If you use a parameter element, the factory must be of type `ExpressionEvaluatingParameterSourceFactory`, located in the `o.s.i.jpa.support.parametersource` package.
The `parameter` sub-elements are mutually exclusive with the `parameter-source-factory` attribute and must be configured on the provided `ParameterSourceFactory`.
Optional.
<12> Accepts one of the following: `PERSIST`, `MERGE`, or `DELETE`.
Indicates the operation that the adapter needs to perform.
@@ -872,8 +872,8 @@ Consequently, the following parameters is available:
`parameter-source-factory`::
An instance of `o.s.i.jpa.support.parametersource.ParameterSourceFactory` used to get an instance of `o.s.i.jpa.support.parametersource.ParameterSource`.
The `ParameterSource` is used to resolve the values of the parameters provided in the query.
If you perform operations by using a JPA entity, the `parameter-source-factory attribute` is ignored.
If you use a `parameter` element, the factory must be of type `ExpressionEvaluatingParameterSourceFactory` (located in package `o.s.i.jpa.support.parametersource`).
If you perform operations by using a JPA entity, the `parameter-source-factory` attribute is ignored.
The `parameter` sub-elements are mutually exclusive with the `parameter-source-factory` and they have to be configured on the provided `ParameterSourceFactory`.
Optional.
`use-payload-as-parameter-source`::
@@ -1047,7 +1047,7 @@ The following example shows all the attributes that you can set on a retrieving
</int-jpa:retrieving-outbound-gateway>
----
<1> (Since Spring Integration 4.0.) The SpEL expression that determines the `primaryKey` value for `EntityManager.find(Class entityClass, Object primaryKey)` method against the `requestMessage` as the root object of evaluation context.
<1> (Since Spring Integration 4.0) The SpEL expression that determines the `primaryKey` value for `EntityManager.find(Class entityClass, Object primaryKey)` method against the `requestMessage` as the root object of evaluation context.
The `entityClass` argument is determined from the `entity-class` attribute, if present.
Otherwise, it is determined from the `payload` class.
All other attributes are disallowed if you use `id-expression`.
@@ -1075,7 +1075,8 @@ Version 3.0 introduced this attribute.
Optional.
====
The remaining attributes are described earlier in this chapter. See <<jpaInboundChannelAdapterParameters>> and <<jpaOutboundChannelAdapterParameters>>.
The remaining attributes are described earlier in this chapter.
See <<jpaInboundChannelAdapterParameters>> and <<jpaOutboundChannelAdapterParameters>>.
==== Configuring with Java Configuration
@@ -1237,7 +1238,7 @@ The following example uses a retrieving outbound gateway and JPQL to retrieve (s
----
====
Retrieving an Entity by Using `id-expression`
===== Retrieving an Entity by Using `id-expression`
The following example uses a retrieving outbound gateway with `id-expression` to retrieve (find) one and only one entity from the database:
The `primaryKey` is the result of `id-expression` evaluation.