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. * ** 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. * - *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); *
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")); ** *