diff --git a/api/src/main/java/jakarta/data/Limit.java b/api/src/main/java/jakarta/data/Limit.java index ca2948bcd..ddd1a150d 100644 --- a/api/src/main/java/jakarta/data/Limit.java +++ b/api/src/main/java/jakarta/data/Limit.java @@ -33,12 +33,15 @@ * For example,

* *
- * Product[] findByNameLike(String namePattern, Limit limit, Sort<?>... sorts);
+ * @Find
+ * Product[] named(@By(_Product.NAME) @Is(LIKE_ANY_CASE) String namePattern,
+ *                 Limit limit,
+ *                 Sort<Product>... sorts);
  * 
  * ...
- * mostExpensive50 = products.findByNameLike(pattern, Limit.of(50), Sort.desc("price"));
+ * mostExpensive50 = products.named(pattern, Limit.of(50), Sort.desc("price"));
  * ...
- * secondMostExpensive50 = products.findByNameLike(pattern, Limit.range(51, 100), Sort.desc("price"));
+ * secondMostExpensive50 = products.named(pattern, Limit.range(51, 100), Sort.desc("price"));
  * 
* *

A repository method may not be declared with: diff --git a/api/src/main/java/jakarta/data/page/CursoredPage.java b/api/src/main/java/jakarta/data/page/CursoredPage.java index 272409bb8..9107373f6 100644 --- a/api/src/main/java/jakarta/data/page/CursoredPage.java +++ b/api/src/main/java/jakarta/data/page/CursoredPage.java @@ -55,23 +55,26 @@ * query parameters) of type {@link PageRequest}, for example:

* *
- * @OrderBy("lastName")
- * @OrderBy("firstName")
- * @OrderBy("id")
- * CursoredPage<Employee> findByHoursWorkedGreaterThan(int hours, PageRequest pageRequest);
+ * @Find
+ * @OrderBy(_Employee.LASTNAME)
+ * @OrderBy(_Employee.FIRSTNAME)
+ * @OrderBy(_Employee.ID)
+ * CursoredPage<Employee> withOvertime(
+ *         @By(_Employee.HOURSWORKED) @Is(GREATER_THAN) int fullTimeHours,
+ *         PageRequest pageRequest);
  * 
* *

In initial page may be requested using an offset-based page request:

* *
- * page = employees.findByHoursWorkedGreaterThan(1500, PageRequest.ofSize(50));
+ * page = employees.withOvertime(40, PageRequest.ofSize(50));
  * 
* *

The next page may be requested relative to the end of the current page, * as follows:

* *
- * page = employees.findByHoursWorkedGreaterThan(1500, page.nextPageRequest());
+ * page = employees.withOvertime(40, page.nextPageRequest());
  * 
* *

Here, the instance of {@link PageRequest} returned by @@ -92,7 +95,7 @@ * PageRequest.ofPage(5) * .size(50) * .afterCursor(Cursor.forKey(emp.lastName, emp.firstName, emp.id)); - * page = employees.findByHoursWorkedGreaterThan(1500, pageRequest); + * page = employees.withOvertime(40, pageRequest); * * *

By making the query for the next page relative to observed values, diff --git a/api/src/main/java/jakarta/data/page/PageRequest.java b/api/src/main/java/jakarta/data/page/PageRequest.java index a79bae3e6..5c121a96f 100644 --- a/api/src/main/java/jakarta/data/page/PageRequest.java +++ b/api/src/main/java/jakarta/data/page/PageRequest.java @@ -37,20 +37,25 @@ * example:

* *
+ * @Find
  * @OrderBy("age")
  * @OrderBy("ssn")
- * Person[] findByAgeBetween(int minAge, int maxAge, PageRequest pageRequest);
+ * Person[] agedBetween(@By("age") @Is(GREATER_THAN_EQ) int minAge,
+ *                      @By("age") @Is(LESS_THAN_EQ) int maxAge,
+ *                      PageRequest pageRequest);
  * 
* *

This method might be called as follows:

* *
- * var page = people.findByAgeBetween(35, 59,
+ * var page = people.agedBetween(
+ *                35, 59,
  *                PageRequest.ofSize(100));
  * var results = page.content();
  * ...
  * while (page.hasNext()) {
- *     page = people.findByAgeBetween(35, 59,
+ *     page = people.agedBetween(
+ *                35, 59,
  *                page.nextPageRequest().withoutTotal());
  *     results = page.content();
  *   ...
diff --git a/api/src/main/java/jakarta/data/repository/By.java b/api/src/main/java/jakarta/data/repository/By.java
index 1834dfa4c..3bc99f136 100644
--- a/api/src/main/java/jakarta/data/repository/By.java
+++ b/api/src/main/java/jakarta/data/repository/By.java
@@ -33,7 +33,10 @@
  *     to the unique identifier field or property.
  * 
  * 

Arguments to the annotated parameter are compared to values of the - * mapped persistent field.

+ * mapped persistent field. The equality comparison is used by default.

+ * + *

For other types of basic comparisons, include the {@link Is} annotation.

+ * *

The field name may be a compound name like {@code address.city}.

* *

For example, for a {@code Person} entity with attributes {@code ssn}, diff --git a/api/src/main/java/jakarta/data/repository/Is.java b/api/src/main/java/jakarta/data/repository/Is.java new file mode 100644 index 000000000..c12eade51 --- /dev/null +++ b/api/src/main/java/jakarta/data/repository/Is.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data.repository; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

Annotates a parameter of a repository {@link Find} or {@link Delete} method, + * indicating how a persistent field is compared against the parameter's value. + * The {@link By} annotation can be used on the same parameter to identify the + * persistent field. Otherwise, if the {@code -parameters} compile option is + * enabled, the the persistent field is inferred by matching the name of the + * method parameter.

+ * + *

For example,

+ * + *
+ * @Repository
+ * public interface Products extends CrudRepository<Product, Long> {
+ *
+ *     // Find all Product entities where the price field is less than a maximum value.
+ *     @Find
+ *     List<Product> pricedBelow(@By(_Product.PRICE) @Is(LESS_THAN) float max);
+ *
+ *     // Find a page of Product entities where the name field matches a pattern, ignoring case.
+ *     @Find
+ *     Page<Product> search(@By(_Product.NAME) @Is(LIKE_ANY_CASE) String pattern,
+ *                          PageRequest pagination,
+ *                          Order<Product> order);
+ *
+ *     // Remove Product entities with any of the unique identifiers listed.
+ *     @Delete
+ *     void remove(@By(ID) @Is(IN) List<Long> productIds);
+ * }
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Is { + + /** + *

The type of comparison operation to use when comparing a persistent + * field against a value that is supplied to a repository method..

+ * + *

The following example compares the year a person was born against + * a minimum and maximum year that are supplied as parameters to a repository + * method:

+ * + *
+     * @Find
+     * @OrderBy(_Person.YEAR_BORN)
+     * List<Person> bornWithin(@By(_Person.YEAR_BORN) @Is(GREATER_THAN_EQ) float minYear,
+     *                         @By(_Person.YEAR_BORN) @Is(LESS_THAN_EQ) float maxYear);
+     * 
+ * + *

The default comparison operation is the {@linkplain #EQUAL equality} + * comparison.

+ * + *

For concise code, it can be convenient for a repository interface to + * statically import one or more constants from this class. For example:

+ * + *
+     * import static jakarta.data.repository.Is.Op.*;
+     * 
+ * + * @return the type of comparison operation. + */ + Op value() default Op.EQUAL; + + /** + *

Comparison operations for the {@link Is} annotation.

+ * + *

For more concise code, it can be convenient to statically import one + * or more comparison operations. For example:

+ * + *
+     * import static jakarta.data.repository.Is.Op.*;
+     * 
+ */ + public static enum Op { + // TODO add JavaDoc with examples to these + ANY_CASE, + EQUAL, + GREATER_THAN, + GREATER_THAN_ANY_CASE, + GREATER_THAN_EQ, + GREATER_THAN_EQ_ANY_CASE, + IN, + LESS_THAN, + LESS_THAN_ANY_CASE, + LESS_THAN_EQ, + LESS_THAN_EQ_ANY_CASE, + LIKE, + LIKE_ANY_CASE, + PREFIXED, + PREFIXED_ANY_CASE, + SUBSTRINGED, + SUBSTRINGED_ANY_CASE, + SUFFIXED, + SUFFIXED_ANY_CASE, + NOT, + NOT_ANY_CASE, + NOT_IN, + NOT_LIKE, + NOT_LIKE_ANY_CASE, + NOT_PREFIXED, + NOT_PREFIXED_ANY_CASE, + NOT_SUBSTRINGED, + NOT_SUBSTRINGED_ANY_CASE, + NOT_SUFFIXED, + NOT_SUFFIXED_ANY_CASE; + } +} diff --git a/api/src/main/java/jakarta/data/repository/OrderBy.java b/api/src/main/java/jakarta/data/repository/OrderBy.java index cd8db4988..bb1d94afa 100644 --- a/api/src/main/java/jakarta/data/repository/OrderBy.java +++ b/api/src/main/java/jakarta/data/repository/OrderBy.java @@ -62,8 +62,9 @@ *

The default sort order is ascending. The {@code descending} member can be * used to specify the sort direction.

*
- * @OrderBy(value = "price", descending = true)
- * {@code Stream} findByPriceLessThanEqual(double maxPrice);
+ * @Find
+ * @OrderBy(value = _Product.PRICE, descending = true)
+ * {@code Stream} pricedBelow(@By(_Product.PRICE) @Is(LESS_THAN) double maxPrice);
  * 
* *

A repository method with an {@code @OrderBy} annotation must not @@ -115,8 +116,9 @@ *

For example,

* *
+     * @Find
      * @OrderBy("age")
-     * Stream<Person> findByLastName(String lastName);
+     * Stream<Person> withLastName(@By("lastName") @Is(ANY_CASE) String surname);
      * 
* * @return entity attribute name. diff --git a/api/src/main/java/jakarta/data/repository/Repository.java b/api/src/main/java/jakarta/data/repository/Repository.java index 6812adcb2..d020f4d4e 100644 --- a/api/src/main/java/jakarta/data/repository/Repository.java +++ b/api/src/main/java/jakarta/data/repository/Repository.java @@ -37,8 +37,9 @@ * @Repository * public interface Products extends DataRepository<Product, Long> { * + * @Find * @OrderBy("price") - * List<Product> findByNameLike(String namePattern); + * List<Product> named(@By("name") @Is(LIKE_ANY_CASE) String namePattern); * * @Query("UPDATE Product SET price = price - (price * ?1) WHERE price * ?1 <= ?2") * int putOnSale(float rateOfDiscount, float maxDiscount); @@ -52,7 +53,7 @@ * Products products; * * ... - * found = products.findByNameLike("%Printer%"); + * found = products.named("%Printer%"); * numUpdated = products.putOnSale(0.15f, 20.0f); *
* diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index f52e8862f..154c372ff 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -28,6 +28,7 @@ import jakarta.data.repository.Delete; import jakarta.data.repository.Find; import jakarta.data.repository.Insert; +import jakarta.data.repository.Is; import jakarta.data.repository.OrderBy; import jakarta.data.repository.Param; import jakarta.data.repository.Query; @@ -69,8 +70,11 @@ * @Insert * void create(Product prod); * + * @Find * @OrderBy("price") - * List<Product> findByNameIgnoreCaseLikeAndPriceLessThan(String namePattern, float max); + * List<Product> search( + * @By("name") @Is(LIKE_ANY_CASE) String namePattern, + * @By("price") @Is(lESS_THAN_EQ) float max); * * @Query("UPDATE Product SET price = price * (1.0 - ?1) WHERE yearProduced <= ?2") * int discountOldInventory(float rateOfDiscount, int maxYear); @@ -90,7 +94,7 @@ * ... * products.create(newProduct); * - * found = products.findByNameIgnoreCaseLikeAndPriceLessThan("%cell%phone%", 900.0f); + * found = products.search("%cell%phone%", 900.0f); * * numDiscounted = products.discountOldInventory(0.15f, Year.now().getValue() - 1); * @@ -149,8 +153,10 @@ * * @Repository * public interface Purchases { + * @Find * @OrderBy("address.zipCode") - * List<Purchase> findByAddressZipCodeIn(List<Integer> zipCodes); + * List<Purchase> forZipCodes( + * @By("address.zipCode") @Is(IN) List<Integer> zipCodes); * * @Query("WHERE address.zipCode = ?1") * List<Purchase> forZipCode(int zipCode); @@ -710,15 +716,19 @@ * with the {@code -parameters} compiler option so that parameter names are * available at runtime.

* - *

Each parameter determines a query condition, and each such condition - * is an equality condition. All conditions must match for a record to + *

Each parameter determines a query condition. By default, each such condition + * is an equality condition. The {@link Is} annotation can be combined with the + * {@link By} annotation to request other types of basic comparisons for a + * repository method parameter. All conditions must match for a record to * satisfy the query.

* *
  * @Find
  * @OrderBy("lastName")
  * @OrderBy("firstName")
- * List<Person> peopleByAgeAndNationality(int age, Country nationality);
+ * List<Person> ofNationalityAndOlderThan(
+ *         Country nationality,
+ *         @By("age") @Is(GREATER_THAN) int minAge);
  * 
* *
@@ -726,6 +736,11 @@
  * Optional<Person> person(String ssn);
  * 
* + *
+ * @Delete
+ * void remove(@By("status") @Is(IN) List<Status> list);
+ * 
+ * *

The {@code _} character may be used in a method parameter name to * reference an embedded attribute.

* @@ -741,11 +756,19 @@ * *
  * // Query by Method Name
- * Vehicle[] findByMakeAndModelAndYear(String makerName, String model, int year, Sort<?>... sorts);
+ * Vehicle[] findByMakeAndModelAndYearBetween(String makerName,
+ *                                            String model,
+ *                                            int minYear,
+ *                                            int maxYear,
+ *                                            Sort<?>... sorts);
  *
  * // parameter-based conditions
  * @Find
- * Vehicle[] searchFor(String make, String model, int year, Sort<?>... sorts);
+ * Vehicle[] search(String make,
+ *                  String model,
+ *                  @By(_Vehicle.YEAR) @Is(GREATER_THAN_EQ) int minYear,
+ *                  @By(_Vehicle.YEAR) @Is(LESS_THAN_EQ) int maxYear,
+ *                  Sort<?>... sorts);
  * 
* *

For further information, refer to the {@linkplain Find API documentation} @@ -783,11 +806,13 @@ * allows its results to be split and retrieved in pages. For example,

* *
- * Product[] findByNameLikeOrderByAmountSoldDescIdAsc(
- *                 String pattern, PageRequest pageRequest);
+ * @Find
+ * @OrderBy(value = _Product.AMOUNT_SOLD, descending = true)
+ * @OrderBy(ID)
+ * Product[] named(@By(_Product.NAME) @Is(LIKE_ANY_CASE) String pattern,
+ *                 PageRequest pageRequest);
  * ...
- * page1 = products.findByNameLikeOrderByAmountSoldDescIdAsc(
- *                 "%phone%", PageRequest.ofSize(20));
+ * page1 = products.named("%phone%", PageRequest.ofSize(20));
  * 
* *

When using pagination, always ensure that the ordering is consistent @@ -801,16 +826,17 @@ * For example,

* *
- * Product[] findByNameLikeAndPriceBetween(String pattern,
- *                                         float minPrice,
- *                                         float maxPrice,
- *                                         PageRequest pageRequest,
- *                                         Order<Product> order);
+ * @Find
+ * Product[] search(@By("name") @Is(LIKE_ANY_CASE) String pattern,
+ *                  @By("price") @Is(GREATER_THAN_EQ) float minPrice,
+ *                  @By("price") @Is(LESS_THAN_EQ) float maxPrice,
+ *                  PageRequest pageRequest,
+ *                  Order<Product> order);
  *
  * ...
  * PageRequest page1Request = PageRequest.ofSize(25);
  *
- * page1 = products.findByNameLikeAndPriceBetween(
+ * page1 = products.search(
  *                 namePattern, minPrice, maxPrice, page1Request,
  *                 Order.by(Sort.desc("price"), Sort.asc("id"));
  * 
@@ -820,13 +846,17 @@ * of {@link Sort} and passed to the repository find method. For example,

* *
- * Product[] findByNameLike(String pattern, Limit max, Order<Product> sortBy);
+ * @Find
+ * Product[] named(@By("name") @Is(LIKE_ANY_CASE) String pattern,
+ *                 Limit max,
+ *                 Order<Product> sortBy);
  *
  * ...
- * found = products.findByNameLike(namePattern, Limit.of(25),
- *                                 Order.by(Sort.desc("price"),
- *                                          Sort.desc("amountSold"),
- *                                          Sort.asc("id")));
+ * found = products.named(namePattern,
+ *                        Limit.of(25),
+ *                        Order.by(Sort.desc("price"),
+ *                                 Sort.desc("amountSold"),
+ *                                 Sort.asc("id")));
  * 
* *

Generic, untyped {@link Sort} criteria can be supplied directly to a @@ -834,13 +864,17 @@ * For example,

* *
- * Product[] findByNameLike(String pattern, Limit max, {@code Sort...} sortBy);
+ * @Find
+ * Product[] named(@By("name") @Is(LIKE_ANY_CASE) String pattern,
+ *                 Limit max,
+ *                 {@code Sort...} sortBy);
  *
  * ...
- * found = products.findByNameLike(namePattern, Limit.of(25),
- *                                 Sort.desc("price"),
- *                                 Sort.desc("amountSold"),
- *                                 Sort.asc("name"));
+ * found = products.named(namePattern,
+ *                        Limit.of(25),
+ *                        Sort.desc("price"),
+ *                        Sort.desc("amountSold"),
+ *                        Sort.asc("name"));
  * 
* *

Repository default methods