From c982cd638e560c441bc389fb640015bcfcd20a53 Mon Sep 17 00:00:00 2001 From: Nathan Rauh Date: Tue, 24 Jun 2025 09:07:20 -0500 Subject: [PATCH 1/2] @Is annotation that specifies a Constraint type --- api/src/main/java/jakarta/data/Limit.java | 9 +- .../java/jakarta/data/page/CursoredPage.java | 17 ++- .../java/jakarta/data/page/PageRequest.java | 17 ++- .../main/java/jakarta/data/repository/By.java | 7 +- .../main/java/jakarta/data/repository/Is.java | 120 ++++++++++++++++++ .../java/jakarta/data/repository/OrderBy.java | 8 +- .../jakarta/data/repository/Repository.java | 5 +- api/src/main/java/module-info.java | 94 ++++++++++---- 8 files changed, 230 insertions(+), 47 deletions(-) create mode 100644 api/src/main/java/jakarta/data/repository/Is.java diff --git a/api/src/main/java/jakarta/data/Limit.java b/api/src/main/java/jakarta/data/Limit.java index 0af233827..1617284fe 100644 --- a/api/src/main/java/jakarta/data/Limit.java +++ b/api/src/main/java/jakarta/data/Limit.java @@ -33,12 +33,15 @@ * example,

* *
- * 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 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:

*
    *
  1. The parameter has exactly the same type and name as an attribute of the - * entity class. The name is assigned by the {@link By @By} annotation. + * entity class. The {@link By @By} annotation assigns the name. + * The {@link Is @Is} annotation, which is optional, supplies a subtype of + * {@link jakarta.data.constraint Constraint} that indicates the comparison. * If the {@code By} annotation is missing, the method parameter name must * match the name of an entity attribute and the repository must be compiled * with the {@code -parameters} compiler option so that parameter names are @@ -361,6 +378,11 @@ * * @Find * @OrderBy("price") + * List<Product> discounted( + * @By("discount") @Is(AtLeast.class) float minAmount); + * + * @Find + * @OrderBy("price") * Product[] named(String name); * *
  2. @@ -402,7 +424,8 @@ *

    Comparisons

    * *

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

    Special parameters

    @@ -861,18 +892,23 @@ * For example,

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

    Restrictions

    From dbafaeb8ea3838cd20dc7c4bbce9dd69cabde5ac Mon Sep 17 00:00:00 2001 From: Nathan Rauh Date: Tue, 24 Jun 2025 10:19:48 -0500 Subject: [PATCH 2/2] Review comment to omit the word builder and just say static method --- api/src/main/java/jakarta/data/repository/Is.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/jakarta/data/repository/Is.java b/api/src/main/java/jakarta/data/repository/Is.java index 57b45f20d..3d8abea58 100644 --- a/api/src/main/java/jakarta/data/repository/Is.java +++ b/api/src/main/java/jakarta/data/repository/Is.java @@ -77,13 +77,13 @@ *

    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:

    * *
      *
    • {@link AtLeast#min(Comparable) AtLeast}