You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2023/05/10 04:24:52 UTC

[camel] branch main updated: CAMEL-19327: camel-jpa support singleResult option (main) (#10045)

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 51b350b450a CAMEL-19327: camel-jpa support singleResult option (main) (#10045)
51b350b450a is described below

commit 51b350b450a4d75bfa1112e1b05c4628ff80b47c
Author: jacekszymanski <ja...@gmail.com>
AuthorDate: Wed May 10 06:24:45 2023 +0200

    CAMEL-19327: camel-jpa support singleResult option (main) (#10045)
    
    * support outputType JpaEndpoint option
    
    * AdditionalQueryParameters no longer nested
    
    * create JpaWithOptionsTestSupport, move support methods from JpaPagingTest
    
    * outputType Query tests
    
    * refactor tests
    
    * add outputType support to processFind
    
    * test outputType on finds
    
    * refactor, the option is now boolean, named singleResult
---
 .../camel/component/jpa/JpaEndpointConfigurer.java |   6 +
 .../camel/component/jpa/JpaEndpointUriFactory.java |   3 +-
 .../org/apache/camel/component/jpa/jpa.json        |   1 +
 .../apache/camel/component/jpa/JpaEndpoint.java    |  14 +++
 .../apache/camel/component/jpa/JpaProducer.java    |  19 ++-
 .../component/jpa/AdditionalQueryParameters.java   |  30 +++++
 .../camel/component/jpa/JpaOutputTypeTest.java     | 140 +++++++++++++++++++++
 .../apache/camel/component/jpa/JpaPagingTest.java  | 105 ++--------------
 .../component/jpa/JpaWithOptionsTestSupport.java   | 119 ++++++++++++++++++
 9 files changed, 341 insertions(+), 96 deletions(-)

diff --git a/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointConfigurer.java b/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointConfigurer.java
index a5642ebc6cf..c0518a34cb2 100644
--- a/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointConfigurer.java
+++ b/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointConfigurer.java
@@ -89,6 +89,8 @@ public class JpaEndpointConfigurer extends PropertyConfigurerSupport implements
         case "sendEmptyMessageWhenIdle": target.setSendEmptyMessageWhenIdle(property(camelContext, boolean.class, value)); return true;
         case "sharedentitymanager":
         case "sharedEntityManager": target.setSharedEntityManager(property(camelContext, boolean.class, value)); return true;
+        case "singleresult":
+        case "singleResult": target.setSingleResult(property(camelContext, boolean.class, value)); return true;
         case "skiplockedentity":
         case "skipLockedEntity": target.setSkipLockedEntity(property(camelContext, boolean.class, value)); return true;
         case "startscheduler":
@@ -179,6 +181,8 @@ public class JpaEndpointConfigurer extends PropertyConfigurerSupport implements
         case "sendEmptyMessageWhenIdle": return boolean.class;
         case "sharedentitymanager":
         case "sharedEntityManager": return boolean.class;
+        case "singleresult":
+        case "singleResult": return boolean.class;
         case "skiplockedentity":
         case "skipLockedEntity": return boolean.class;
         case "startscheduler":
@@ -270,6 +274,8 @@ public class JpaEndpointConfigurer extends PropertyConfigurerSupport implements
         case "sendEmptyMessageWhenIdle": return target.isSendEmptyMessageWhenIdle();
         case "sharedentitymanager":
         case "sharedEntityManager": return target.isSharedEntityManager();
+        case "singleresult":
+        case "singleResult": return target.isSingleResult();
         case "skiplockedentity":
         case "skipLockedEntity": return target.isSkipLockedEntity();
         case "startscheduler":
diff --git a/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointUriFactory.java b/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointUriFactory.java
index 7848eb3859a..7c748ec540a 100644
--- a/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointUriFactory.java
+++ b/components/camel-jpa/src/generated/java/org/apache/camel/component/jpa/JpaEndpointUriFactory.java
@@ -21,7 +21,7 @@ public class JpaEndpointUriFactory extends org.apache.camel.support.component.En
     private static final Set<String> SECRET_PROPERTY_NAMES;
     private static final Set<String> MULTI_VALUE_PREFIXES;
     static {
-        Set<String> props = new HashSet<>(46);
+        Set<String> props = new HashSet<>(47);
         props.add("backoffErrorThreshold");
         props.add("backoffIdleThreshold");
         props.add("backoffMultiplier");
@@ -60,6 +60,7 @@ public class JpaEndpointUriFactory extends org.apache.camel.support.component.En
         props.add("schedulerProperties");
         props.add("sendEmptyMessageWhenIdle");
         props.add("sharedEntityManager");
+        props.add("singleResult");
         props.add("skipLockedEntity");
         props.add("startScheduler");
         props.add("timeUnit");
diff --git a/components/camel-jpa/src/generated/resources/org/apache/camel/component/jpa/jpa.json b/components/camel-jpa/src/generated/resources/org/apache/camel/component/jpa/jpa.json
index 8ea5e470d86..e07af102abc 100644
--- a/components/camel-jpa/src/generated/resources/org/apache/camel/component/jpa/jpa.json
+++ b/components/camel-jpa/src/generated/resources/org/apache/camel/component/jpa/jpa.json
@@ -65,6 +65,7 @@
     "firstResult": { "kind": "parameter", "displayName": "First Result", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": -1, "description": "Set the position of the first result to retrieve." },
     "flushOnSend": { "kind": "parameter", "displayName": "Flush On Send", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Flushes the EntityManager after the entity bean has been persisted." },
     "remove": { "kind": "parameter", "displayName": "Remove", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Indicates to use entityManager.remove(entity)." },
+    "singleResult": { "kind": "parameter", "displayName": "Single Result", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "If enabled, a query or a find which would return no results or more than one result, will throw an exception instead." },
     "useExecuteUpdate": { "kind": "parameter", "displayName": "Use Execute Update", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "description": "To configure whether to use executeUpdate() when producer executes a query. When you use INSERT, UPDATE or DELETE statement as a named query, you need to specify this option to 'true'." },
     "usePersist": { "kind": "parameter", "displayName": "Use Persist", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Indicates to use entityManager.persist(entity) instead of entityManager.merge(entity). Note: entityManager.persist(entity) doesn't work for detached entities (where the EntityManager has to execute an UPDATE instead of an [...]
     "lazyStartProducer": { "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may other [...]
diff --git a/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaEndpoint.java b/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaEndpoint.java
index 992385285c7..eff80528168 100644
--- a/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaEndpoint.java
+++ b/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaEndpoint.java
@@ -112,6 +112,8 @@ public class JpaEndpoint extends ScheduledPollEndpoint {
     private Boolean useExecuteUpdate;
     @UriParam(label = "producer")
     private boolean findEntity;
+    @UriParam(label = "producer", defaultValue = "false")
+    private boolean singleResult;
 
     @UriParam(label = "advanced", prefix = "emf.", multiValue = true)
     private Map<String, Object> entityManagerProperties;
@@ -557,6 +559,18 @@ public class JpaEndpoint extends ScheduledPollEndpoint {
         this.findEntity = findEntity;
     }
 
+    public boolean isSingleResult() {
+        return singleResult;
+    }
+
+    /**
+     * If enabled, a query or a find which would return no results or more than one result, will throw an exception
+     * instead.
+     */
+    public void setSingleResult(boolean singleResult) {
+        this.singleResult = singleResult;
+    }
+
     // Implementation methods
     // -------------------------------------------------------------------------
 
diff --git a/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaProducer.java b/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaProducer.java
index 7cee7c0f25c..c44e6c3b7c7 100644
--- a/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaProducer.java
+++ b/components/camel-jpa/src/main/java/org/apache/camel/component/jpa/JpaProducer.java
@@ -22,6 +22,7 @@ import java.util.Map;
 
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.EntityManagerFactory;
+import jakarta.persistence.NoResultException;
 import jakarta.persistence.Query;
 
 import org.apache.camel.Exchange;
@@ -191,7 +192,16 @@ public class JpaProducer extends DefaultProducer {
             } else {
                 target = exchange.getIn();
             }
-            Object answer = isUseExecuteUpdate() ? innerQuery.executeUpdate() : innerQuery.getResultList();
+
+            final Object answer;
+            if (isUseExecuteUpdate()) {
+                answer = innerQuery.executeUpdate();
+            } else if (getEndpoint().isSingleResult()) {
+                answer = innerQuery.getSingleResult();
+            } else {
+                answer = innerQuery.getResultList();
+            }
+
             target.setBody(answer);
 
             if (getEndpoint().isFlushOnSend()) {
@@ -246,6 +256,13 @@ public class JpaProducer extends DefaultProducer {
                 Object answer = entityManager.find(getEndpoint().getEntityType(), key);
                 LOG.debug("Find: {} -> {}", key, answer);
 
+                if (getEndpoint().isSingleResult() && answer == null) {
+                    throw new NoResultException(
+                            String.format(
+                                    "No results for key %s and singleResult requested",
+                                    key));
+                }
+
                 Message target;
                 if (ExchangeHelper.isOutCapable(exchange)) {
                     target = exchange.getMessage();
diff --git a/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/AdditionalQueryParameters.java b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/AdditionalQueryParameters.java
new file mode 100644
index 00000000000..d966268b4b2
--- /dev/null
+++ b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/AdditionalQueryParameters.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.jpa;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(value = RetentionPolicy.RUNTIME)
+@Target(value = { ElementType.METHOD })
+@interface AdditionalQueryParameters {
+
+    String value();
+
+}
diff --git a/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaOutputTypeTest.java b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaOutputTypeTest.java
new file mode 100644
index 00000000000..4874bade0ac
--- /dev/null
+++ b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaOutputTypeTest.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.jpa;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.AnnotatedElement;
+
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.NonUniqueResultException;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.examples.Customer;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class JpaOutputTypeTest extends JpaWithOptionsTestSupport {
+
+    private static final String ENDPOINT_URI = "jpa://" + Customer.class.getName();
+
+    private String queryOrFind;
+
+    @Test
+    @Query
+    @AdditionalQueryParameters("singleResult=true&parameters.seq=% 001")
+    public void testSingleCustomerOKQuery() throws Exception {
+        final Customer customer = runQueryTest(Customer.class);
+
+        assertNotNull(customer);
+    }
+
+    @Test
+    @Query("select c from Customer c")
+    @AdditionalQueryParameters("singleResult=true")
+    public void testTooMuchResults() throws Exception {
+        final Exchange result = doRunQueryTest();
+
+        Assertions.assertInstanceOf(NonUniqueResultException.class, getException(result));
+    }
+
+    @Test
+    @Query
+    @AdditionalQueryParameters("singleResult=true&parameters.seq=% xxx")
+    public void testNoCustomersQuery() throws Exception {
+        final Exchange result = doRunQueryTest();
+
+        Assertions.assertInstanceOf(NoResultException.class, getException(result));
+    }
+
+    @Test
+    @Find
+    @AdditionalQueryParameters("singleResult=true")
+    public void testSingleCustomerOKFind() throws Exception {
+        // ids in the db are not known, so query for a known element and use its id.
+        super.setUp(getEndpointUri());
+
+        final Customer fromDb = (Customer) entityManager
+                .createQuery("select c from Customer c where c.name like '% 001'")
+                .getSingleResult();
+
+        final Exchange result = template.send("direct:start", withBody(fromDb.getId()));
+
+        assertNotNull(result.getIn().getBody(Customer.class));
+    }
+
+    @Test
+    @Find
+    @AdditionalQueryParameters("singleResult=true")
+    public void testNoCustomerFind() throws Exception {
+        final Exchange result = doRunQueryTest(withBody(Long.MAX_VALUE));
+
+        Assertions.assertInstanceOf(NoResultException.class, getException(result));
+    }
+
+    @Override
+    protected String getEndpointUri() {
+        return String.format("%s?%s%s",
+                ENDPOINT_URI,
+                queryOrFind,
+                createAdditionalQueryParameters());
+    }
+
+    @Override
+    public void beforeEach(ExtensionContext context) throws Exception {
+        super.beforeEach(context);
+
+        // a query or a find is necessary - without the annotation test can't continue
+        final AnnotatedElement annotatedElement = context.getElement().get();
+
+        final Find find = annotatedElement.getAnnotation(Find.class);
+        final Query query = annotatedElement.getAnnotation(Query.class);
+
+        if ((find == null) == (query == null)) {
+            throw new IllegalStateException("Test must be annotated with EITHER Find OR Query");
+        }
+
+        if (find != null) {
+            queryOrFind = "findEntity=" + true;
+        } else { // query != null
+            queryOrFind = "query=" + query.value();
+        }
+
+    }
+
+    @Target(ElementType.METHOD)
+    @Retention(RetentionPolicy.RUNTIME)
+    private @interface Find {
+    }
+
+    @Target(ElementType.METHOD)
+    @Retention(RetentionPolicy.RUNTIME)
+    private @interface Query {
+        String value() default "select c from Customer c where c.name like :seq";
+    }
+
+    private static Exception getException(final Exchange exchange) {
+        final Exception exception = exchange.getException();
+
+        return exception != null ? exception : exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class);
+    }
+}
diff --git a/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaPagingTest.java b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaPagingTest.java
index 824c9429c53..32aaf4d3d1a 100644
--- a/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaPagingTest.java
+++ b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaPagingTest.java
@@ -16,47 +16,28 @@
  */
 package org.apache.camel.component.jpa;
 
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import java.lang.reflect.AnnotatedElement;
 import java.util.List;
-import java.util.Optional;
-import java.util.stream.IntStream;
 
-import org.apache.camel.Exchange;
-import org.apache.camel.Processor;
-import org.apache.camel.RoutesBuilder;
-import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.examples.Customer;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtensionContext;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-public class JpaPagingTest extends AbstractJpaMethodSupport {
+public class JpaPagingTest extends JpaWithOptionsTestSupport {
 
     private static final String ENDPOINT_URI = "jpa://" + Customer.class.getName() +
                                                "?query=select c from Customer c order by c.name";
 
-    // should be less than 1000 as numbers in entries' names are formatted for sorting with %03d (or change format)
-    private static final int ENTRIES_COUNT = 30;
-
-    private static final String ENTRY_SEQ_FORMAT = "%03d";
-
-    // both should be less than ENTRIES_COUNT / 2
+    // both should be less than JpaWithOptionsTestSupport.ENTRIES_COUNT / 2
     private static final int FIRST_RESULT = 5;
     private static final int MAXIMUM_RESULTS = 10;
 
-    protected String additionalQueryParameters = "";
-
     @Test
     public void testUnrestrictedQueryReturnsAll() throws Exception {
         final List<Customer> customers = runQueryTest();
 
-        assertEquals(ENTRIES_COUNT, customers.size());
+        assertEquals(JpaWithOptionsTestSupport.ENTRIES_COUNT, customers.size());
     }
 
     @Test
@@ -64,13 +45,13 @@ public class JpaPagingTest extends AbstractJpaMethodSupport {
     public void testFirstResultInUri() throws Exception {
         final List<Customer> customers = runQueryTest();
 
-        assertEquals(ENTRIES_COUNT - FIRST_RESULT, customers.size());
+        assertEquals(JpaWithOptionsTestSupport.ENTRIES_COUNT - FIRST_RESULT, customers.size());
     }
 
     @Test
     public void testMaxResultsInHeader() throws Exception {
-        final List<Customer> customers
-                = runQueryTest(exchange -> exchange.getIn().setHeader(JpaConstants.JPA_MAXIMUM_RESULTS, MAXIMUM_RESULTS));
+        final List<Customer> customers = runQueryTest(
+                withHeader(JpaConstants.JPA_MAXIMUM_RESULTS, MAXIMUM_RESULTS));
 
         assertEquals(MAXIMUM_RESULTS, customers.size());
     }
@@ -100,7 +81,7 @@ public class JpaPagingTest extends AbstractJpaMethodSupport {
         final List<Customer> customers = runQueryTest(
                 withHeader(JpaConstants.JPA_FIRST_RESULT, FIRST_RESULT * 2));
 
-        assertEquals(ENTRIES_COUNT - (FIRST_RESULT * 2), customers.size());
+        assertEquals(JpaWithOptionsTestSupport.ENTRIES_COUNT - (FIRST_RESULT * 2), customers.size());
         assertFirstCustomerSequence(customers, FIRST_RESULT * 2);
     }
 
@@ -115,7 +96,7 @@ public class JpaPagingTest extends AbstractJpaMethodSupport {
     }
 
     @Test
-    @AdditionalQueryParameters("firstResult=" + ENTRIES_COUNT)
+    @AdditionalQueryParameters("firstResult=" + JpaWithOptionsTestSupport.ENTRIES_COUNT)
     public void testFirstResultAfterTheEnd() throws Exception {
         final List<Customer> customers = runQueryTest();
 
@@ -123,77 +104,13 @@ public class JpaPagingTest extends AbstractJpaMethodSupport {
     }
 
     private static void assertFirstCustomerSequence(final List<Customer> customers, final int firstResult) {
-        assertTrue(customers.get(0).getName().endsWith(String.format(ENTRY_SEQ_FORMAT, firstResult)));
-    }
-
-    @SuppressWarnings("unchecked")
-    protected List<Customer> runQueryTest(final Processor... preRun) throws Exception {
-        setUp(getEndpointUri());
-
-        final Exchange result = template.send("direct:start", exchange -> {
-            for (Processor processor : preRun) {
-                processor.process(exchange);
-            }
-        });
-
-        return (List<Customer>) result.getMessage().getBody(List.class);
-    }
-
-    @Override
-    public void beforeEach(ExtensionContext context) throws Exception {
-        super.beforeEach(context);
-
-        final Optional<AnnotatedElement> element = context.getElement();
-
-        if (element.isPresent()) {
-            final AnnotatedElement annotatedElement = element.get();
-            final AdditionalQueryParameters annotation = annotatedElement.getAnnotation(AdditionalQueryParameters.class);
-            if (annotation != null && !annotation.value().isBlank()) {
-                additionalQueryParameters = annotation.value();
-            }
-        }
-    }
-
-    @Override
-    protected void setUp(String endpointUri) throws Exception {
-        super.setUp(endpointUri);
-
-        createCustomers();
-        assertEntitiesInDatabase(ENTRIES_COUNT, Customer.class.getName());
-    }
-
-    protected void createCustomers() {
-        IntStream.range(0, ENTRIES_COUNT).forEach(idx -> {
-            Customer customer = createDefaultCustomer();
-            customer.setName(String.format("%s " + ENTRY_SEQ_FORMAT, customer.getName(), idx));
-            save(customer);
-        });
-    }
-
-    @Override
-    protected RoutesBuilder createRouteBuilder() throws Exception {
-        final String endpointUri = getEndpointUri();
-        return new RouteBuilder() {
-            public void configure() {
-                from("direct:start")
-                        .to(endpointUri);
-            }
-        };
+        assertTrue(customers.get(0).getName().endsWith(
+                String.format(JpaWithOptionsTestSupport.ENTRY_SEQ_FORMAT, firstResult)));
     }
 
     protected String getEndpointUri() {
         return ENDPOINT_URI +
-               (additionalQueryParameters.isBlank() ? "" : "&" + additionalQueryParameters);
-    }
-
-    protected Processor withHeader(final String headerName, final Object headerValue) {
-        return exchange -> exchange.getIn().setHeader(headerName, headerValue);
-    }
-
-    @Retention(RetentionPolicy.RUNTIME)
-    @Target({ ElementType.METHOD })
-    private @interface AdditionalQueryParameters {
-        String value();
+               createAdditionalQueryParameters();
     }
 
 }
diff --git a/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaWithOptionsTestSupport.java b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaWithOptionsTestSupport.java
new file mode 100644
index 00000000000..0394799f362
--- /dev/null
+++ b/components/camel-jpa/src/test/java/org/apache/camel/component/jpa/JpaWithOptionsTestSupport.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.jpa;
+
+import java.lang.reflect.AnnotatedElement;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.IntStream;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.examples.Customer;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+public abstract class JpaWithOptionsTestSupport extends AbstractJpaMethodSupport {
+
+    // should be less than 1000 as numbers in entries' names are formatted for sorting with %03d (or change format)
+    static final int ENTRIES_COUNT = 30;
+    static final String ENTRY_SEQ_FORMAT = "%03d";
+
+    private String additionalQueryParameters = "";
+
+    protected String createAdditionalQueryParameters() {
+        return additionalQueryParameters.isBlank() ? "" : "&" + additionalQueryParameters;
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() throws Exception {
+        final String endpointUri = getEndpointUri();
+        return new RouteBuilder() {
+            public void configure() {
+                from("direct:start").to(endpointUri);
+            }
+        };
+    }
+
+    protected Exchange doRunQueryTest(final Processor... preRun) throws Exception {
+        setUp(getEndpointUri());
+
+        return template.send("direct:start", exchange -> {
+            for (Processor processor : preRun) {
+                processor.process(exchange);
+            }
+        });
+
+    }
+
+    protected <E> E runQueryTest(Class<E> type, final Processor... preRun) throws Exception {
+        final Exchange result = doRunQueryTest(preRun);
+
+        Assertions.assertNull(result.getException());
+        Assertions.assertNull(result.getProperty(Exchange.EXCEPTION_CAUGHT));
+
+        return result.getMessage().getBody(type);
+
+    }
+
+    @SuppressWarnings(value = "unchecked")
+    protected List<Customer> runQueryTest(final Processor... preRun) throws Exception {
+        return (List<Customer>) runQueryTest(List.class, preRun);
+    }
+
+    protected Processor withHeader(final String headerName, final Object headerValue) {
+        return exchange -> exchange.getIn().setHeader(headerName, headerValue);
+    }
+
+    protected Processor withBody(final Object body) {
+        return exchange -> exchange.getIn().setBody(body);
+    }
+
+    @Override
+    public void beforeEach(ExtensionContext context) throws Exception {
+        super.beforeEach(context);
+
+        final Optional<AnnotatedElement> element = context.getElement();
+        if (element.isPresent()) {
+            final AnnotatedElement annotatedElement = element.get();
+            final AdditionalQueryParameters annotation = annotatedElement.getAnnotation(AdditionalQueryParameters.class);
+            if (annotation != null && !annotation.value().isBlank()) {
+                additionalQueryParameters = annotation.value();
+            }
+        }
+    }
+
+    protected abstract String getEndpointUri();
+
+    protected void createCustomers() {
+        IntStream.range(0, ENTRIES_COUNT).forEach(idx -> {
+            Customer customer = createDefaultCustomer();
+            customer.setName(String.format("%s " + ENTRY_SEQ_FORMAT, customer.getName(), idx));
+            save(customer);
+        });
+    }
+
+    @Override
+    protected void setUp(String endpointUri) throws Exception {
+        super.setUp(endpointUri);
+        createCustomers();
+        assertEntitiesInDatabase(ENTRIES_COUNT, Customer.class.getName());
+    }
+
+}