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..3d8abea58 --- /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 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 static 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"));
*
*
*