From c982cd638e560c441bc389fb640015bcfcd20a53 Mon Sep 17 00:00:00 2001
From: Nathan Rauh
- * Product[] findByNameLike(String namePattern, Limit limit, Sort<?>... sorts); + * @Find + * Product[] namedLike(@By(_Product.NAME) @Is(Like.class) String namePattern, + * Limit limit, + * Sort<?>... sorts); * * ... - * mostExpensive50 = products.findByNameLike(pattern, Limit.of(50), Sort.desc("price")); + * mostExpensive50 = products.namedLike(pattern, Limit.of(50), Sort.desc("price")); * ... - * secondMostExpensive50 = products.findByNameLike(pattern, Limit.range(51, 100), Sort.desc("price")); + * secondMostExpensive50 = products.namedLike(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 cf88eb28c..0846322eb 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> withHoursOver( + * @By(_Employee.HOURSWORKED) @Is(GreaterThan.class) 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.withHoursOver(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.withHoursOver(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.withHoursOver(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 147e2532d..106656440 100644 --- a/api/src/main/java/jakarta/data/page/PageRequest.java +++ b/api/src/main/java/jakarta/data/page/PageRequest.java @@ -37,23 +37,28 @@ * regular parameters of the query itself. For example:
* *+ * @Find * @OrderBy("age") * @OrderBy("ssn") - * Page<Person> findByAgeBetween(int minAge, int maxAge, PageRequest pageRequest); + * Page<Person> agedBetween(@By("age") @Is(AtLeast.class) int minAge, + * @By("age") @Is(AtMost.class) int maxAge, + * PageRequest pageRequest); ** *
This method might be called as follows:
* *- * Page<Person> page = people.findByAgeBetween(35, 59, - * PageRequest.ofSize(100)); + * Page<Person> page = people.agedBetween( + * 35, 59, + * PageRequest.ofSize(100)); * List<Person> results = page.content(); * ... * while (page.hasNext()) { - * page = people.findByAgeBetween(35, 59, - * page.nextPageRequest().withoutTotal()); + * 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 2f31e4ed3..5472e1622 100644 --- a/api/src/main/java/jakarta/data/repository/By.java +++ b/api/src/main/java/jakarta/data/repository/By.java @@ -22,6 +22,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import jakarta.data.constraint.EqualTo; /** *
Annotates a parameter of a repository method, specifying a mapping to @@ -33,7 +34,11 @@ * to the unique identifier attribute. * *
Arguments to the annotated parameter are compared to values of the - * mapped attribute.
+ * mapped attribute. The {@link EqualTo#value(Object) equality} comparison is + * default. Use the {@link Is#value() @Is} annotation to choose a different + * subtype of {@link jakarta.data.constraint Constraint} to be the comparison. + * + * *The attribute 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..57b45f20d --- /dev/null +++ b/api/src/main/java/jakarta/data/repository/Is.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024,2025 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; + +import jakarta.data.constraint.AtLeast; +import jakarta.data.constraint.AtMost; +import jakarta.data.constraint.Constraint; +import jakarta.data.constraint.EqualTo; +import jakarta.data.constraint.GreaterThan; +import jakarta.data.constraint.In; +import jakarta.data.constraint.LessThan; +import jakarta.data.constraint.Like; +import jakarta.data.constraint.NotEqualTo; +import jakarta.data.constraint.NotIn; +import jakarta.data.constraint.NotLike; + +/** + *
Annotates a parameter of a repository {@link Find} or {@link Delete} + * method, indicating how an entity attribute is compared with the parameter's + * value.
+ * + *The {@code @Is} annotation's {@link #value()} supplies the type of + * comparison as a subtype of {@link jakarta.data.constraint Constraint}.
+ * + *The {@link By} annotation must annotate the same parameter to indicate + * the entity attribute name, or otherwise, if the {@code -parameters} compile + * option is enabled, the persistent field is inferred by matching the name of + * the method parameter.
+ * + *For example,
+ * + *+ * @Repository + * public interface Products extends CrudRepository<Product, Long> { + * + * // Find Product entities where the price attribute is less than a maximum value. + * @Find + * List<Product> pricedBelow(@By(_Product.PRICE) @Is(LessThan.class) float max); + * + * // Find a page of Product entities where the name field matches a pattern. + * @Find + * Page<Product> search(@By(_Product.NAME) @Is(Like.class) String pattern, + * PageRequest pagination, + * Order<Product> order); + * + * // Remove Product entities with any of the unique identifiers listed. + * @Delete + * void remove(@By(ID) @Is(In.class) List<Long> productIds); + * } + *+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Is { + + /** + *
A subtype of {@link jakarta.data.constraint Constraint} that + * indicates how the entity attribute is compared with a value.
+ * + *The constraint subtype must have a static builder method that accepts + * as its only parameter a value compatible with the type (or if primitive, + * a wrapper for the type) of the repository method parameter to which the + * {@code @Is} annotation is applied. The repository method parameter type + * must also be consistent with the respective entity attribute type. This + * list indicates the constraint subtypes that can be used and links to the + * applicable builder method for each:
+ * + *The following example involves a {@code Person} entity that has a + * {@code birthYear} attribute of type {@code int}. It compares the year in + * which a person was born against a minimum and maximum year that are + * supplied as parameters to a repository method:
+ * + *+ * @Find + * @OrderBy(_Person.BIRTHYEAR) + * List<Person> bornWithin(@By(_Person.BIRTHYEAR) @Is(AtLeast.class) int minYear, + * @By(_Person.BIRTHYEAR) @Is(AtMost.class) int maxYear); + *+ * + *
The default constraint is the + * {@linkplain EqualTo#value(Object) equality} comparison.
+ * + * @return the type of comparison operation. + */ + @SuppressWarnings("rawtypes") + Class extends Constraint> value() default EqualTo.class; +} diff --git a/api/src/main/java/jakarta/data/repository/OrderBy.java b/api/src/main/java/jakarta/data/repository/OrderBy.java index a7ab9a853..a6458e4d9 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(AtMost.class) double maxPrice); *
A repository method with an {@code @OrderBy} annotation must not @@ -116,8 +117,9 @@ *
For example,
* *+ * @Find * @OrderBy("age") - * Stream<Person> findByLastName(String lastName); + * Stream<Person> withLastName(@By("lastName") 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 010509fcc..1c68fc539 100644 --- a/api/src/main/java/jakarta/data/repository/Repository.java +++ b/api/src/main/java/jakarta/data/repository/Repository.java @@ -38,8 +38,9 @@ * @Repository * public interface Products extends DataRepository<Product, Long> { * + * @Find * @OrderBy("price") - * List<Product> findByNameLike(String namePattern); + * List<Product> namedLike(@By("name") @Is(Like.class) String namePattern); * * @Query("UPDATE Product SET price = price - (price * ?1) WHERE price * ?1 <= ?2") * int putOnSale(float rateOfDiscount, float maxDiscount); @@ -53,7 +54,7 @@ * Products products; * * ... - * found = products.findByNameLike("%Printer%"); + * found = products.namedLike("%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 25162e19d..021de152f 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -34,6 +34,7 @@ import jakarta.data.repository.Find; import jakarta.data.repository.First; 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; @@ -83,6 +84,12 @@ * @OrderBy("price") * List<Product> findByNameIgnoreCaseLikeAndPriceLessThan(String namePattern, float max); * + * @Find + * List<Product> search( + * @By(_Product.NAME) @Is(Like.class) String namePattern, + * Restriction<Product> restriction, + * Order<Product> sortBy); + * * @Query(""" * UPDATE Product SET price = price * (1.0 - ?1) * WHERE producedOn <= ?2 @@ -104,7 +111,13 @@ * ... * products.create(newProduct); * - * found = products.findByNameIgnoreCaseLikeAndPriceLessThan("%cell%phone%", 900.0f); + * phones = products.findByNameIgnoreCaseLikeAndPriceLessThan("%cell%phone%", 900.0f); + * + * chargers = products.search("%charger%", + * Restrict.all(_Product.description.contains("USB-C"), + * _Product.price.lessThan(30.0f)), + * Order.by(_Product.price.desc(), + * _Product.id.asc())); * * numDiscounted = products.discountOldInventory(0.15f, * LocalDate.now().minusYears(1)); @@ -164,8 +177,10 @@ * * @Repository * public interface Purchases { + * @Find * @OrderBy("address.zipCode") - * List<Purchase> findByAddressZipCodeIn(List<Integer> zipCodes); + * List<Purchase> forZipCodes( + * @By("address.zipCode") @Is(In.class) List<Integer> zipCodes); * * @Query("WHERE address.zipCode = ?1") * List<Purchase> forZipCode(int zipCode); @@ -350,7 +365,9 @@ * categories: *
Categories 1 and 2 above identify the name of an entity attribute against - * which a supplied value is compared. Subtypes of {@code Constraint} indicate + * which a supplied value is compared. A subtype of {@code Constraint} is used + * as the parameter type or supplied by the {@link Is} annotation to indicate * the type of comparison. The equality comparison is used in the absence of a * {@code Constraint} subtype when no comparison is indicated.
* @@ -417,7 +440,7 @@ * ** @Find - * Stream<Person> livingInZipCode(@("address.zipCode") int zip); + * Stream<Person> livingInZipCode(@By("address.zipCode") int zip); ** *
The {@code _} character may be used in a method parameter name to @@ -797,12 +820,20 @@ * *
* // Query by Method Name - * Vehicle[] findFirst50ByMakeAndModelAndYear(String makerName, String model, int year, Sort<?>... sorts); + * Vehicle[] findFirst50ByMakeAndModelAndYearBetween(String makerName, + * String model, + * int minYear, + * int maxYear, + * Order<Vehicle> sorts); * * // parameter-based conditions * @Find * @First(50) - * Vehicle[] searchFor(String make, String model, int year, Sort<?>... sorts); + * Vehicle[] search(String make, + * String model, + * @By(_Vehicle.YEAR) @Is(AtLeast.class) int minYear, + * @By(_Vehicle.YEAR) @Is(AtMost.class) int maxYear, + * Order<Vehicle> sorts); ** *
- * Page<Product> findByNameLikeAndPriceBetween(String pattern, - * float minPrice, - * float maxPrice, - * PageRequest pageRequest, - * Order<Product> order); + * @Find + * Page<Product> pricedWithin(@By("name") @Is(Like.class) String pattern, + * @By("price") @Is(AtLeast.class) float minPrice, + * @By("price") @Is(AtMost.class) float maxPrice, + * PageRequest pageRequest, + * Order<Product> order); * * ... * PageRequest page1Request = PageRequest.ofSize(25); * - * page1 = products.findByNameLikeAndPriceBetween( - * namePattern, minPrice, maxPrice, page1Request, - * Order.by(Sort.desc("price"), Sort.asc("id")); + * page1 = products.pricedWithin( + * namePattern, + * minPrice, + * maxPrice, + * page1Request, + * Order.by(Sort.desc("price"), + * Sort.asc("id"))); ** *
To supply sort criteria dynamically without using pagination, an @@ -880,13 +916,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.class) 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.nameLiked(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 @@ -894,13 +934,17 @@ * For example,
* *- * Product[] findByNameLike(String pattern, Limit max, {@code Sort>...} sortBy); + * @Find + * Product[] namedLike(@By("name") @Is(Like.class) 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.namedLike(namePattern, + * Limit.of(25), + * Sort.desc("price"), + * Sort.desc("amountSold"), + * Sort.asc("name")); ** *
A subtype of {@link jakarta.data.constraint Constraint} that * indicates how the entity attribute is compared with a value.
* - *The constraint subtype must have a static builder method that accepts + *
The constraint subtype must have a static method that accepts * as its only parameter a value compatible with the type (or if primitive, * a wrapper for the type) of the repository method parameter to which the * {@code @Is} annotation is applied. The repository method parameter type * must also be consistent with the respective entity attribute type. This * list indicates the constraint subtypes that can be used and links to the - * applicable builder method for each:
+ * applicable static method for each: * *