Skip to content

Latest commit

 

History

History
877 lines (633 loc) · 44.9 KB

repository.asciidoc

File metadata and controls

877 lines (633 loc) · 44.9 KB

Repository Interfaces

A Jakarta Data repository is a Java interface annotated with @Repository. A repository interface may declare:

  • abstract (non-default) methods, and

  • concrete (default) methods.

A concrete method may call other methods of the repository, including abstract methods.

Every abstract method of the interface is usually either:

A repository may declare lifecycle methods for a single entity type, or for multiple related entity types. Similarly, a repository might have query methods which return different entity types.

A repository interface may inherit methods from a superinterface. A superinterface of a repository interface must either:

  • be one of the built-in generic repository supertypes defined by this specification, DataRepository, BasicRepository, or CrudRepository, or

  • be a non-generic toplevel interface with no type parameters, whose abstract methods likewise declare no type parameters, and which does not itself directly or indirectly inherit any generic interface or any interface whose abstract methods declare type parameters.

A Jakarta Data implementation must treat abstract methods inherited by a repository interface as if they were directly declared by the repository interface.

Repositories perform operations on entities. For repository methods that are annotated with @Insert, @Update, @Save, or @Delete, the entity type is determined from the method parameter type. For repository methods that are annotated with @Find, the entity type is determined by the annotation value member, if an entity type is explicitly specified. Otherwise, for find and delete methods where the return type is an entity, array of entity, or parameterized type such as List<MyEntity> or Page<MyEntity>, the entity type is determined from the method return type. For count, exists, and other find and delete methods that do not return the entity or accept the entity as a parameter, the entity type cannot be determined from the method signature and a primary entity type must be defined for the repository.

Users of Jakarta Data declare a primary entity type for a repository by inheriting from a built-in repository super interface, such as BasicRepository, and specifying the primary entity type as the first type variable. For repositories that do not inherit from a super interface with a type parameter to indicate the primary entity type, lifecycle methods on the repository determine the primary entity type. To do so, all lifecycle methods where the method parameter is a type, an array of type, or is parameterized with a type that is annotated as an entity, must correspond to the same entity type. The primary entity type is assumed for methods that do not otherwise specify an entity type, such as countByPriceLessThan. Methods that require a primary entity type raise MappingException if a primary entity type is not provided.

Note
A Jakarta Data provider might go beyond what is required by this specification and support abstract methods which do not fall into any of the above categories. Such functionality is not defined by this specification, and so applications with repositories which declare such methods are not portable between providers.

The subsections below specify the rules that an abstract method declaration must observe so that the Jakarta Data implementation is able to provide an implementation of the abstract method.

  • If every abstract method of a repository complies with the rules specified below, then the Jakarta Data implementation must provide an implementation of the repository.

  • Otherwise, if a repository declares an abstract method which does not comply with the rules specified below, or makes use of functionality which is not supported by the Jakarta Data implementation, then an error might be produced by the Jakarta Data implementation at build time or at runtime.

The portability of a given repository interface between Jakarta Data implementations depends on the portability of the entity types it uses. If an entity class is not portable between given implementations, then any repository which uses the entity class is also unportable between those implementations.

Note
Additional portability guarantees may be provided by specifications which extend this specification, specializing to a given class of datastore.

Lifecycle methods

A lifecycle method is an abstract method annotated with a lifecycle annotation. Lifecycle methods allow the program to make changes to persistent data in the data store.

A lifecycle method must be annotated with a lifecycle annotation. The method signature of the lifecycle method, including its return type, must follow the requirements that are specified by the Javadoc of the lifecycle annotation.

Lifecycle method signatures follow one of these generic patterns:

@Lifecycle
void lifecycle(Entity e);
@Lifecycle
Entity lifecycle(Entity e);

where Lifecycle is a lifecycle annotation, lifecycle is the arbitrary name of the method, and Entity is either E, List<E>, or E[], where E is a concrete entity class. In this context, any variadic parameter declared E…​ is treated as if it were declared with type E[].

This specification defines four built-in lifecycle annotations: @Insert, @Update, @Delete, and @Save. The semantics of these annotations is defined in their Javadoc.

For example:

@Insert
void insertBook(Book book);

Lifecycle methods are not guaranteed to be portable between all providers.

Jakarta Data providers must support lifecycle methods to the extent that the data store is capable of the corresponding operation. If the data store is not capable of the operation, the Jakarta Data provider must raise UnsupportedOperationException when the operation is attempted, per the requirements of the Javadoc for the lifecycle annotation, or the Jakarta Data provider must report the error at compile time.

There is no special programming model for lifecycle annotations. The Jakarta Data implementation automatically recognizes the lifecycle annotations it supports.

Note

A Jakarta Data provider might extend this specification to define additional lifecycle annotations, or to support lifecycle methods with signatures other than the usual signatures defined above. For example, a provider might support "merge" methods declared as follows:

@Merge
Book mergeBook(Book book);

Such lifecycle methods are not portable between Jakarta Data providers.

Annotated query methods

An annotated query method is an abstract method annotated by a query annotation type. The query annotation specifies a query in some datastore-native query language.

Each parameter of an annotated query method must either:

  • have exactly the same name and type as a named parameter of the query,

  • have exactly the same type and position within the parameter list of the method as a positional parameter of the query, or

  • be of type Limit, Order, PageRequest, or Sort.

A repository with annotated query methods with named parameters must be compiled so that parameter names are preserved in the class file (for example, using javac -parameters), or the parameter names must be specified explicitly using the @Param annotation.

An annotated query method must not also be annotated with a lifecycle annotation.

The return type of the annotated query method must be consistent with the result type of the query specified by the query annotation.

Note

The result type of a query depends on datastore-native semantics, and so the return type of an annotated query method cannot be specified here. However, Jakarta Data implementations are strongly encouraged to support the following return types:

  • for a query which returns a single result of type T, the type T itself, or Optional<T>,

  • for a query which returns many results of type T, the types List<T>, Page<T>, and T[].

Furthermore, implementations are encouraged to support void as the return type for a query which never returns a result.

This specification defines the built-in @Query annotation, which may be used to specify a query written in the [Jakarta Data Query Language] defined in the next chapter.

For example, using a named parameter:

@Query("where title like :title order by title asc, id asc")
Page<Book> booksByTitle(String title, PageRequest pageRequest);
@Query("where p.name = :prodname")
Optional<Product> findByName(@Param("prodname") String name);

Or, using a positional parameter:

@Query("delete from Book where isbn = ?1")
void deleteBook(String isbn);

Programs which make use of annotated query methods are not in general portable between providers. However, when the @Query annotation specifies a query written in JDQL, the annotated query method is portable between providers to the extent to which its semantics can be implemented on the underlying data store.

Note

A Jakarta Data provider might extend this specification to define its own query annotation types. For example, a provider might define a @SQL annotation for declaring queries written in SQL.

There is no special programming model for query annotations. The Jakarta Data implementation automatically recognizes the query annotations it supports.

Parameter-based automatic query methods

A parameter-based automatic query method is an abstract method annotated with an automatic query annotation.

Each automatic query method must be assigned an entity type. The rules for inferring the entity type depend on the semantics of the automatic query annotation. Typically:

  • If the automatic query method returns an entity type, the method return type identifies the entity. For example, the return type might be E, Optional<E>, E[], Page<E>, or List<E>, where E is an entity class. Then the automatic query method would be assigned the entity type E.

  • If the query does not return an entity type, the entity assigned to the automatic query method is the primary entity type of the repository.

Jakarta Data infers a query based on the parameters of the method. Each parameter must either:

  • have exactly the same type and name as a persistent attribute of the entity class, or

  • be of type Limit, Order, PageRequest, or Sort.

Parameter names map parameters to persistent attributes. A repository with parameter-based automatic query methods must either:

  • be compiled so that parameter names are preserved in the class file (for example, using javac -parameters), or

  • explicitly specify the name of the persistent attribute mapped by each parameter of an automatic query method using the @By annotation.

The attribute name specified using @By may be a compound name, as specified below in [Persistent Attribute Names].

This specification defines the built-in automatic query annotations @Find and @Delete. The semantics of these annotations are specified in their Javadoc. Note that @Delete is both a lifecycle annotation and an automatic query annotation. The signature of a repository method annotated @Delete must be used to disambiguate the interpretation of the @Delete annotation.

For example:

@Find
Book bookByIsbn(String isbn);

@Find
List<Book> booksByYear(Year year, Sort<Book> order, Limit limit);

@Find
Page<Book> find(@By("year") Year publishedIn,
                @By("genre") Category type,
                Order<Book> sortBy,
                PageRequest pageRequest);

Automatic query methods annotated with @Find or @Delete are portable between providers.

Note

A Jakarta Data provider might extend this specification to define its own automatic query annotation types. In this case, an automatic query method is not portable between providers.

Resource accessor methods

A resource accessor method is a method with no parameters which returns a type supported by the Jakarta Data provider. The purpose of this method is to provide the program with direct access to the data store.

For example, if the Jakarta Data provider is based on JDBC, the return type might be java.sql.Connection or javax.sql.DataSource. Or, if the Jakarta Data provider is backed by Jakarta Persistence, the return type might be jakarta.persistence.EntityManager.

The Jakarta Data provider recognizes the connection types it supports and implements the method such that it returns an instance of the type of resource. If the resource type implements java.lang.AutoCloseable and the resource is obtained within the scope of a default method of the repository, then the Jakarta Data provider automatically closes the resource upon completion of the default method. If the method for obtaining the resource is invoked outside the scope of a default method of the repository, then the user is responsible for closing the resource instance.

Note
A Jakarta Data implementation might allow a resource accessor method to be annotated with additional metadata providing information about the connection.

For example:

Connection connection();

default void cleanup() {
    try (Statement s = connection().createStatement()) {
        s.executeUpdate("truncate table books");
    }
}

A repository may have at most one resource accessor method.

Conflicting Repository Method Annotations

Annotations like @Find, @Query, @Insert, @Update, @Delete, and @Save are mutually-exclusive. A given method of a repository interface may have at most one:

  • @Find annotation,

  • lifecycle annotation, or

  • query annotation.

If a method of a repository interface has more than one such annotation, the annotated repository method must raise UnsupportedOperationException every time it is called. Alternatively, a Jakarta Data provider is permitted to reject such a method declaration at compile time.

Special Parameters for Limits, Sorting, and Pagination

An annotated, parameter-based, or Query by Method Name query method may have special parameters of type Limit, Order, Sort, or PageRequest if the method return type indicates that the method may return multiple entities, that is, if the return type is:

  • an array type,

  • List or Stream, or

  • Page or CursoredPage.

A special parameter controls which query results are returned to the caller of a repository method, or in what order the results are returned:

  • a Limit allows the query results to be limited to a given range defined in terms of an offset and maximum number of results,

  • a Sort or Order allows the query results to be sorted by a given entity attribute or list of attributes, respectively, and

  • a PageRequest splits results into pages. A parameter of this type must be declared when the repository method returns a Page of results, as specified below in Offset-based Pagination, or a CursoredPage, as specified in Cursor-based Pagination.

A repository method must throw UnsupportedOperationException if it has:

  • more than one parameter of type PageRequest or Limit,

  • a parameter of type PageRequest and a parameter of type Limit,

  • a @First annotation and a parameter of type PageRequest or Limit,

  • a parameter of type PageRequest or Limit, in combination with the keyword First,

  • a @First annotation, in combination with the keyword First, or

  • more than one parameter of type Order.

Alternatively, a Jakarta Data provider is permitted to reject such a repository method declaration at compile time.

A repository method must throw DataException if the database is incapable of ordering the query results using the given sort criteria.

The following example demonstrates the use of special parameters:

@Repository
public interface ProductRepository extends BasicRepository<Product, Long> {

    @Find
    Page<Product> findByName(String name, PageRequest pageRequest, Order<Product> order);

    @Query("where name like :pattern")
    List<Product> findByNameLike(String pattern, Limit max, Sort<?>... sorts);

}

An instance of Sort may be obtained by specifying an entity attribute name:

Sort nameAscending = Sort.asc("name");

Even better, the static metamodel may be used to obtain an instance of Sort in a typesafe way:

Sort<Employee> nameAscending = _Employee.name.asc();

This PageRequest specifies a starting page and maximum page size:

PageRequest pageRequest = PageRequest.ofPage(1).size(20);
List<Product> first20 = products.findByName(name, pageRequest,
                            Order.by(_Product.price.desc(),
                                     _Product.id.asc()));

Precedence of Sort Criteria

The specification defines different ways of providing sort criteria on queries. This section discusses how these different mechanisms relate to each other.

Sort Criteria within Query Language

Sort criteria can be hard-coded directly within query language by making use of the @Query annotation. A repository method that is annotated with @Query with a value that contains an ORDER BY clause (or query language equivalent) must not provide sort criteria via the other mechanisms.

A repository method that is annotated with @Query with a value that does not contain an ORDER BY clause and ends with a WHERE clause (or query language equivalents to these) can use other mechanisms that are defined by this specification for providing sort criteria.

Static Mechanisms for Sort Criteria

Sort criteria are provided statically for a repository method by using the OrderBy keyword or by annotating the method with one or more @OrderBy annotations. The OrderBy keyword cannot be intermixed with the @OrderBy annotation or the @Query annotation. Static sort criteria takes precedence over dynamic sort criteria in that static sort criteria are evaluated first. When static sort criteria sorts entities to the same position, dynamic sort criteria are applied to further order those entities.

Dynamic Mechanisms for Sort Criteria

Sort criteria are provided dynamically to repository methods either via Sort parameters or via a Order parameter that has one or more Sort values.

Examples of Sort Criteria Precedence

In the following examples, the query results are sorted by age, using the dynamic sorting criteria passed to the sorts parameter to break ties between records with the same age.

@Query("WHERE u.age > ?1")
@OrderBy(_User.AGE)
Page<User> findByNamePrefix(String namePrefix,
                            PageRequest pagination,
                            Order<User> sorts);
@Query("WHERE u.age > ?1")
@OrderBy(_User.AGE)
List<User> findByNamePrefix(String namePrefix, Sort<?>... sorts);

Pagination in Jakarta Data

Dividing up large sets of data into pages is a beneficial strategy for data access and retrieval in many applications, including those developed in Java. Pagination helps improve the efficiency of handling large datasets in a way that is also user-friendly. In Jakarta Data, APIs are provided to help Java developers efficiently manage and navigate through data.

Jakarta Data supports two types of pagination: offset-based and cursor-based. These approaches differ in how they manage and retrieve paginated data:

Offset pagination is the more traditional form based on position relative to the first record in the dataset. It is typically used with a fixed page size, where a specified number of records is retrieved starting from a given offset position.

Cursor-based pagination, also known as seek method or keyset pagination, uses a unique key or unique combination of values (referred to as the key) to navigate the dataset relative to the first or last record of the current page. Cursor-based pagination is typically used with fixed page sizes but can accommodate varying the page size if desired. It is more robust when dealing with datasets where the underlying data might change and offers the the potential for improved performance by avoiding the need to scan records prior to the cursor.

The critical differences between offset-based and cursor-based pagination lie in their retrieval methods:

  • Offset-based pagination uses a fixed page size and retrieves data based on page number and size.

  • Cursor-based pagination relies on a unique key or unique combination of values (the key) for an entity relative to which it determines the next page or previous page.

Offset-based Pagination

Offset pagination is a popular method for managing and retrieving large datasets efficiently. It is based on dividing the dataset into pages containing a specified number of elements. This method allows developers to retrieve a subset of the dataset by identifying the page number and the maximum number of elements per page.

Offset pagination is motivated by the need to provide efficient navigation through large datasets. Loading an entire dataset into memory at once can be resource-intensive and lead to performance issues. By breaking the dataset into smaller, manageable pages, offset pagination improves performance, reduces resource consumption, and enhances the overall user experience.

Offset pagination offers several key features that make it a valuable approach for managing and retrieving large datasets in a controlled and efficient manner:

  • Page Size: The maximum number of elements to be included in each page is known as the page size. This parameter determines the subset of data retrieved with each pagination request.

  • Page Number: The page number indicates which subset of the dataset to retrieve. It typically starts from 1, representing the first page, and increments with each subsequent page.

  • Efficient Navigation: Offset pagination allows efficient dataset navigation. By specifying the desired page and page size, developers can control the data retrieved, optimizing memory usage and processing time.

  • Sequential Order: Elements are retrieved sequentially based on predefined criteria, such as ascending or descending order of a specific attribute, like an ID.

Requirements when using Offset Pagination

The following requirements must be met when using offset-based pagination:

  • The repository method signature must return Page. A repository method with return type of Page must raise UnsupportedOperationException if the database is incapable of offset pagination.

  • The repository method signature must accept a PageRequest parameter.

  • Sort criteria must be provided and should be minimal.

  • The combination of provided sort criteria must define a deterministic ordering of entities.

  • The entities within each page must be ordered according to the provided sort criteria.

  • If PageRequest.requestTotal() returns true, the Page should contain accurate information about the total number of pages and total number of elements across all pages. Otherwise, if PageRequest.requestTotal() returns false, the operations Page.totalElements() and Page.totalPages() throw IllegalStateException.

  • Except for the highest numbered page, the Jakarta Data provider must return full pages consisting of the maximum page size number of entities.

  • Page numbers for offset pagination are computed by taking the entity’s 1-based offset after sorting, dividing it by the maximum page size, and rounding up. For example, the 52nd entity is on page 6 when the maximum page size is 10, because 52 / 10 rounded up is 6. Note that the first page number is always 1.

Scenario: Person Entity and People Repository

Consider a scenario with a Person entity and a corresponding People repository:

public class Person {
    private Long id;
    private String name;
}

@Repository
public interface People extends BasicRepository<Person, Long> {
}

The dataset contains the following elements:

[
   {"id":1, "name":"Lin Le Marchant"},
   {"id":2, "name":"Corri Davidou"},
   {"id":3, "name":"Alyse Dadson"},
   {"id":4, "name":"Orelle Roughey"},
   {"id":5, "name":"Jaquith Wealthall"},
   {"id":6, "name":"Boothe Martinson"},
   {"id":7, "name":"Patten Bedell"},
   {"id":8, "name":"Danita Pilipyak"},
   {"id":9, "name":"Harlene Branigan"},
   {"id":10, "name":"Boothe Martinson"}
]

Code Execution:

@Inject
People people;

Page<Person> page =
        people.findAll(PageRequest.ofPage(1).size(2),
                       Order.by(Sort.asc("id")));

Resulting Page Content:

[
   {"id":1, "name":"Lin Le Marchant"},
   {"id":2, "name":"Corri Davidou"}
]

Next Page Execution:

if (page.hasNext()) {
   PageRequest nextPageRequest = page.nextPageRequest();
   Page<Person> page2 = people.findAll(nextPageRequest,
                                       Order.by(Sort.asc("id")));
}

Resulting Page Content:

[
   {"id":3, "name":"Alyse Dadson"},
   {"id":4, "name":"Orelle Roughey"}
]

In this scenario, each page represents a subset of the dataset, and developers can navigate through the pages efficiently using offset pagination.

Offset pagination is a valuable tool for Java developers when dealing with large datasets, providing control, efficiency, and a seamless user experience.

Cursor-based Pagination

Cursor-based pagination aims to reduce missed and duplicate results across pages by querying relative to the observed values of entity attributes that constitute the sorting criteria. Cursor-based pagination can also offer an improvement in performance because it avoids fetching and ordering results from prior pages by causing those results to be non-matching. A Jakarta Data provider appends additional conditions to the query and tracks cursor-based values automatically when CursoredPage is used as the repository method return type. The application invokes nextPageRequest or previousPageRequest on the CursoredPage to obtain a PageRequest which keeps track of the cursor-based values.

For example,

@Repository
public interface CustomerRepository extends BasicRepository<Customer, Long> {
    @Find
    @OrderBy(_Customer.LAST_NAME)
    @OrderBy(_Customer.FIRST_NAME)
    @OrderBy(_Customer.ID)
    CursoredPage<Customer> findByZipcode(int zipcode, PageRequest pageRequest);
}

You can obtain the initial page relative to an offset and subsequent pages relative to the last entity of the current page as follows,

PageRequest pageRequest = PageRequest.ofSize(50);
Page<Customer> page =
        customers.findByZipcode(55901, pageRequest);
if (page.hasNext()) {
  pageRequest = page.nextPageRequest();
  page = customers.findByZipcode(55901, pageRequest);
  ...
}

Or you can obtain the next (or previous) page relative to a known entity,

Customer c = ...
PageRequest p = PageRequest.ofPage(10)
                           .size(50)
                           .afterCursor(Cursor.forKey(c.lastName, c.firstName, c.id));
page = customers.findByZipcode(55902, p);

The sort criteria for a repository method that performs cursor-based pagination must uniquely identify each entity and must be provided by:

  • the @OrderBy annotation or annotations of the repository method,

  • Order or Sort parameters of the repository method, or

  • an OrderBy in Query by Method Name.

The values of the entity attributes of the combined sort criteria define the cursor for cursor-based cursor based pagination. Within the cursor, each entity attribute has the same sorting and order of precedence that it has within the combined sort criteria.

Example of Appending to Queries for Cursor-based Pagination

Without cursor-based pagination, a Jakarta Data provider that is based on Jakarta Persistence might compose the following JPQL for the findByZipcode() repository method from the prior example:

FROM Customer
WHERE zipCode = ?1
ORDER BY lastName ASC, firstName ASC, id ASC

When cursor-based pagination is used, the keys values from the Cursor of the PageRequest are available as query parameters, allowing the Jakarta Data provider to append additional query conditions. For example,

FROM Customer
WHERE (zipCode = ?1)
  AND (
         lastName > ?2
      OR lastName = ?2 AND firstName > ?3
      OR lastName = ?2 AND firstName = ?3 AND id > ?4
  )
ORDER BY lastName ASC, firstName ASC, id ASC
Avoiding Missed and Duplicate Results

Because searching for the next page of results is relative to a last known position, it is possible with cursor-based pagination to allow some types of updates to data while pages are being traversed without causing missed results or duplicates to appear. If you add entities to a prior position in the traversal of pages, the shift forward of numerical position of existing entities will not cause duplicates entities to appear in your continued traversal of subsequent pages because cursor-based pagination does not query based on a numerical position. If you remove entities from a prior position in the traversal of pages, the shift backward of numerical position of existing entities will not cause missed entities in your continued traversal of subsequent pages because keyset pagination does not query based on a numerical position.

Other types of updates to data, however, will cause duplicate or missed results. If you modify entity attributes which are used as the sort criteria, cursor-based pagination cannot prevent the same entity from appearing again or never appearing due to the altered values. If you add an entity that you previously removed, whether with different values or the same values, cursor-based pagination cannot prevent the entity from being missed or possibly appearing a second time due to its changed values.

Restrictions on use of Cursor-based Pagination
  • The repository method signature must return CursoredPage. A repository method with return type of CursoredPage must raise UnsupportedOperationException if the database is incapable of cursor-based pagination.

  • The contents of the CursoredPage returned by the repository method must be entities, not entity attributes, records containing a subset of entity attributes, or any other values that are not the entity itself.

  • The repository method signature must accept a PageRequest parameter.

  • Sort criteria must be provided and should be minimal.

  • The combination of provided sort criteria must uniquely identify each entity such that the sort criteria defines a deterministic ordering of entities.

  • The entities within each page must be ordered according to the provided sort criteria.

  • Page numbers for cursor-based pagination are estimated relative to prior page requests or the observed absence of further results and are not accurate. Page numbers must not be relied upon when using cursor-based pagination.

  • Page totals and result totals are not accurate for cursor-based pagination and must not be relied upon.

  • A next or previous page can end up being empty. You cannot obtain a next or previous PageRequest from an empty page because there are no key values relative to which to query.

  • A repository method that is annotated with @Query and performs cursor-based pagination must omit the ORDER BY clause from the provided query and instead must supply the sort criteria via @OrderBy annotations or Sort criteria of PageRequest. The provided query must end with a WHERE clause to which additional conditions can be appended by the Jakarta Data provider. The Jakarta Data provider is not expected to parse query text that is provided by the application.

Cursor-based Pagination Example with Sorts

Here is an example where an application uses @Query to provide a partial query to which the Jakarta Data provider can generate and append additional query conditions and an ORDER BY clause.

@Repository
public interface CustomerRepository extends BasicRepository<Customer, Long> {
    @Query("WHERE totalSpent / totalPurchases > ?1")
    CursoredPage<Customer> withAveragePurchaseAbove(float minimum,
                                                    PageRequest pageRequest,
                                                    Order<Customer> sorts);
}

Example traversal of pages:

Order<Customer> order =
        Order.by(_Customer.yearBorn.desc(),
                 _Customer.name.asc(),
                 _Customer.id.asc());
PageRequest pageRequest = PageRequest.ofSize(25);
do {
    page = customers.withAveragePurchaseAbove(50.0f, pageRequest, order);
    ...
    if (page.hasNext()) {
        pageRequest = page.nextPageRequest();
    }
}
while (page.hasNext());
Example with Before/After Cursor

In this example, the application uses a cursor to request pages in forward and previous direction from a specific value, which is the price for a matching product.

@Repository
public interface Products extends CrudRepository<Product, Long> {
    @Query("where name like ?1")
    CursoredPage<Product> findByNameLike(String namePattern,
                                         PageRequest pageRequest,
                                         Order<Product> sorts);
}

Obtaining the next 10 products that cost $50.00 or more:

float priceMidpoint = 50.0f;
Order<Product> order =
        Order.by(_Product.price.asc(),
                 _Product.id.asc());
PageRequest pageRequest =
        PageRequest.ofPage(5)
                   .size(10)
                   .afterCursor(Cursor.forKey(priceMidpoint, 0L));
CursoredPage<Product> moreExpensive =
        products.findByNameLike(pattern, pageRequest, order);

Obtaining the previous 10 products:

pageRequest =
        moreExpensive.hasContent() && moreExpensive.hasPrevious()
                ? moreExpensive.previousPageRequest()
                : pageRequest.beforeCursor(Cursor.forKey(priceMidpoint, 1L));
CursoredPage<Product> lessExpensive =
        products.findByNameLike(pattern, pageRequest, order);
Example with Combined Sort Criteria

In this example, the application uses OrderBy to define a subset of the sort criteria during development time, but also uses Sort to dynamically determine more fine-grained sorting when all of the static sort criteria matches. In this case the repository query is written to always order Car entities with a vehicle condition of VehicleCondition.NEW ahead of those with VehicleCondition.USED.

@Repository
public interface Products extends CrudRepository<Product, Long> {
    @Find
    @OrderBy(_Car.VEHICLE_CONDITION)
    CursoredPage<Car> find(@By(_Car.MAKE) String manufacturer,
                           @By(_Car.MODEL) String model,
                           PageRequest pageRequest,
                           Order<Car> sorts);
}

The above criteria does not uniquely identify Car entities. After sorting on the vehicle condition, finer grained sorting is provided dynamically by the Order, in this case the vehicle price followed by the unique Vehicle Identification Number (VIN). It is a good practice for the final sort criterion to be a unique identifier of the entity to ensure a deterministic ordering.

Order<Car> order = Order.by(_Car.price.desc(),
                            _Car.vin.asc())
PageRequest page1Request = PageRequest.ofSize(25);
CursoredPage<Car> page1 =
        cars.find(make, model, page1Request, order);

The query results are ordered first by vehicle condition. All resulting entities with the same vehicle condition are subsequently ordered by their price in descending order. All resulting entities with the same vehicle condition and price are ordered alphabetically by their VIN. The end user requests the next page of results. If the application still has access to the page at this point, it can use page.nextPageRequest() to obtain a request for the next page of results. In this case, the Jakarta Data provider computes the cursor from the vehicle condition, price, and VIN of the final Car entity of the page and includes the cursor in the resulting PageRequest instance. Alternatively, the application does not need access to the page if it obtained the cursor or the vehicle condition, price, and VIN values that make up the cursor. In this case, it can construct a new PageRequest,

PageRequest page2Request = PageRequest
             .ofPage(2) // cosmetic when using a cursor
             .size(25)
             .afterCursor(Cursor.forKey(lastCar.vehicleCondition,
                                        lastCar.price,
                                        lastCar.vin));
CursoredPage<Car> page2 =
        cars.find(make, model, page2Request, order);
Scenario: Person Entity and People Repository

This cursor-based pagination scenario uses the same Person entity and example dataset from the offset-based pagination scenario, but orders it by name and then by id,

[
   {"id":3, "name":"Alyse Dadson"},
   {"id":6, "name":"Boothe Martinson"},
   {"id":10, "name":"Boothe Martinson"},
   {"id":2, "name":"Corri Davidou"},
   {"id":8, "name":"Danita Pilipyak"},
   {"id":9, "name":"Harlene Branigan"},
   {"id":5, "name":"Jaquith Wealthall"},
   {"id":1, "name":"Lin Le Marchant"},
   {"id":4, "name":"Orelle Roughey"},
   {"id":7, "name":"Patten Bedell"}
]
@Repository
public interface People extends BasicRepository<Person, Long> {
    @Find
    CursoredPage<Person> findAll(PageRequest pagination,
                                 Order<Person> sorts);
}

Code Execution:

@Inject
People people;

Order<Person> order = Order.by(Sort.asc("name"),
                               Sort.asc("id");
PageRequest firstPageRequest = PageRequest.ofSize(4);
CursoredPage<Person> page =
        people.findAll(firstPageRequest, order);

Resulting Page Content:

[
   {"id":3, "name":"Alyse Dadson"},
   {"id":6, "name":"Boothe Martinson"},
   {"id":10, "name":"Boothe Martinson"},
   {"id":2, "name":"Corri Davidou"}
]

Deletion of an Entity:

// The user decides to remove one of the entities that has the same name,
people.deleteById(10);

Next Page Execution:

if (page.hasNext()) {
   PageRequest nextPageRequest = page.nextPageRequest();
   CursoredPage<Person> page2 = people.findAll(nextPageRequest, order);
}

Resulting Page Content:

[
   {"id":8, "name":"Danita Pilipyak"},
   {"id":9, "name":"Harlene Branigan"},
   {"id":5, "name":"Jaquith Wealthall"},
   {"id":1, "name":"Lin Le Marchant"}
]

It should be noted, the above result is different than what would be retrieved with offset-based pagination, where the removal of an entity from the first page shifts the offset for entries 5 through 8 to start from {"id":9, "name":"Harlene Branigan"}, skipping over {"id":8, "name":"Danita Pilipyak"} that becomes offset position 4 after the removal. Cursor-based pagination does not skip the entity because it queries relative to a cursor position, starting from the next entity after {"id":2, "name":"Corri Davidou"}.

Precedence of Repository Methods

The following order, with the lower number having higher precedence, is used when interpreting the meaning of repository methods.

  1. If the method is a Java default method, then its provided implementation is used.

  2. If the method has a resource accessor method return type recognized by the Jakarta Data provider, then the method is implemented as a resource accessor method.

  3. If the method is annotated with a query annotation recognized by the Jakarta Data provider, such as @Query, then the method is implemented to execute the query specified by the query annotation.

  4. If the method is annotated with an automatic query annotation, such as @Find, or with a lifecycle annotation declaring the type of operation, for example, with @Insert, @Update, @Save, or @Delete, and the provider recognizes the annotation, then the annotation determines how the method is implemented, possibly with the help of other annotations present on the method parameters, for example, any @By annotations of the parameters.

  5. If the method is named according to the conventions of Query by Method Name, then the method is implemented according to the Query by Method Name extension to this specification.

A repository method that does not fit any of the above patterns and is not handled as a vendor-specific extension to the specification must either result in an error at build time or raise UnsupportedOperationException at runtime.

Null arguments to repository methods

When a repository method is called with a null value as an argument to one of its parameters, the repository implementation might throw an exception:

  • when a lifecycle method is called with a null entity instance, the repository implementation must throw NullPointerException, or

  • when an annotated or parameter-based query method is called with a null argument, the repository implementation is permitted, but not required, to throw an appropriate exception type.

Note
The behavior of a query method when the method is called with a null argument is not defined by this specification, and is not portable between Jakarta Data providers.

Asynchronous repositories

An asynchronous repository method is a repository method which returns an object representing a value which will eventually be obtained from the database, but which might not yet be available. An asynchronous repository method is permitted to return such an object immediately, and access the database asynchronously.

An asynchronous repository method has a signature of form:

F<R> m(P1 p1, P2 p2, ...)

where:

  • R m(P1 p1, P2 p2, …​) is a legal repository method signature according to the previous sections of this chapter, or, in the special case that R is the type java.lang.Void, void m(P1 p1, P2 p2, …​) is a legal repository method signature according to the previous sections of this chapter, and

  • F is a parameterized type representing a value which might not yet have been computed and which is supported by the Jakarta Data provider.

Note
Every Jakarta Data provider is encouraged, but not required, to support asynchronous repository methods returning java.util.concurrent.CompletionStage.
Note
A repository method annotated with the @Asynchronous annotation from the Jakarta Concurrency specification is permitted to declare the return type java.util.concurrent.CompletionStage. In this case, the Jakarta Data provider must synchronously return an already-completed CompletionStage so that the Jakarta Concurrency provider is able to control the asynchronous behavior.

For example, the following is an asynchronous parameter-based query method that relies on the Jakarta Concurrency @Asynchronous interceptor to control the asynchronous behavior:

@Asynchronous
@Find
CompletionStage<Book> bookByIsbn(String isbn);

This method is an asynchronous lifecycle method that relies on the Jakarta Data provider to control the asynchronous behavior:

@Insert
CompletionStage<Void> insertBook(Book book);

An asynchronous repository is a repository which declares asynchronous repository methods. A repository may declare a mixture of synchronous and asynchronous repository methods if every asynchronous method is annotated with the @Asynchronous annotation, so that Jakarta Concurrency provides the asynchronous behavior. Otherwise, the Jakarta Data provider is not required to support mixing synchronous and asynchronous repository methods within the same repository interface. The @Asynchronous annotation must not be used on repositories implemented using reactive streams.

Note
An asynchronous repository might be backed by a thread pool, or it might be implemented using reactive streams. Such implementation details are concerns of the Jakarta Data provider, and are beyond the scope of this specification.