Specification: Jakarta Data

Version: 1.0.0-SNAPSHOT

Status: Draft

Release: June 16, 2023

Copyright (c) {inceptionYear} , {currentYear} Eclipse Foundation.

Eclipse Foundation Specification License

By using and/or copying this document, or the Eclipse Foundation document from which this statement is linked, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions:

Permission to copy, and distribute the contents of this document, or the Eclipse Foundation document from which this statement is linked, in any medium for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the document, or portions thereof, that you use:

  • link or URL to the original Eclipse Foundation document.

  • All existing copyright notices, or if one does not exist, a notice (hypertext is preferred, but a textual representation is permitted) of the form: "Copyright (c) [$date-of-document] Eclipse Foundation, Inc. <<url to this license>>"

Inclusion of the full text of this NOTICE must be provided. We request that authorship attribution be provided in any software, documents, or other items or products that you create pursuant to the implementation of the contents of this document, or any portion thereof.

No right to create modifications or derivatives of Eclipse Foundation documents is granted pursuant to this license, except anyone may prepare and distribute derivative works and portions of this document in software that implements the specification, in supporting materials accompanying such software, and in documentation of such software, PROVIDED that all such works include the notice below. HOWEVER, the publication of derivative works of this document for use as a technical specification is expressly prohibited.

The notice is:

"Copyright (c) [$date-of-document] Eclipse Foundation. This software or document includes material copied from or derived from [title and URI of the Eclipse Foundation specification document]."

Disclaimers

THIS DOCUMENT IS PROVIDED "AS IS," AND THE COPYRIGHT HOLDERS AND THE ECLIPSE FOUNDATION MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR TITLE; THAT THE CONTENTS OF THE DOCUMENT ARE SUITABLE FOR ANY PURPOSE; NOR THAT THE IMPLEMENTATION OF SUCH CONTENTS WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.

THE COPYRIGHT HOLDERS AND THE ECLIPSE FOUNDATION WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE DOCUMENT OR THE PERFORMANCE OR IMPLEMENTATION OF THE CONTENTS THEREOF.

The name and trademarks of the copyright holders or the Eclipse Foundation may NOT be used in advertising or publicity pertaining to this document or its contents without specific, written prior permission. Title to copyright in this document will at all times remain with copyright holders.

Jakarta Data

1. Introduction

The Jakarta Data specification provides an API for easier data access. A Java developer can split the persistence from the model with several features, such as the ability to compose custom query methods on a Repository interface where the framework will implement it.

There is no doubt about the importance of data around the application. We often talk about a stateless application, where we delegate the application’s state to the database.

Dealing with a database is one of the biggest challenges within a software architecture. In addition to choosing one of several options on the market, it is necessary to consider the persistence integrations. Jakarta Data makes life easier for Java developers.

1.1. Goals

Jakarta Data works in a tight integration between Java and a persistence layer, where it has the following specification goals:

  • Be a persistence agnostic API. Therefore, through abstractions, it will connect different types of databases and storage sources.

  • Be a pluggable and extensible API. Even when the API won’t support a particular behavior of a storage engine, it might provide an extensible API to make it possible.

1.2. Non-Goals

As with any software component, these decisions come with trade-offs and the following non-goals:

  • Provide specific features of Jakarta Persistence, Jakarta NoSQL, etc. Those APIs have their own specifications.

  • Replace the Jakarta Persistence or Jakarta NoSQL specifications. Indeed, Jakarta Data will work as a complement to these specifications as an agnostic API.

1.3. Conventions

1.4. Jakarta Data Project Team

This specification is being developed as part of Jakarta Data project under the Jakarta EE Specification Process. It is the result of the collaborative work of the project committers and various contributors.

1.4.1. Project Leads

1.4.3. Mentor

1.4.4. Contributors

The complete list of Jakarta Data contributors may be found here.

2. Repository

In Domain-Driven Design (DDD) the repository pattern encapsulates the logic required to access data sources. The repository pattern consolidates data access functionality, providing better maintainability and decoupling the infrastructure or technology used to access databases from the domain model layer.

repository structure

This pattern focuses on the closest proximity of entities and hides where the data comes from.

The Repository pattern is a well-documented way of working with a data source. In the book Patterns of Enterprise Application Architecture, Martin Fowler describes a repository as follows:

A repository performs the tasks of an intermediary between the domain model layers and data mapping, acting in a similar way to a set of domain objects in memory. Client objects declaratively build queries and send them to the repositories for answers. Conceptually, a repository encapsulates a set of objects stored in the database and operations that can be performed on them, providing a way that is closer to the persistence layer. Repositories also support the purpose of separating, clearly and in one direction, the dependency between the work domain and the data allocation or mapping.

It also becomes very famous in Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans.

2.1. Repositories on Jakarta Data

A repository abstraction aims to significantly reduce the boilerplate code required to implement data access layers for various persistence stores.

The parent interface in Jakarta Data repository abstraction is DataRepository.

By default, Jakarta Data has support for three interfaces. However, the core is extensible. Therefore, a provider might extend one or more interfaces to a specific data target.

Repositories types
  • Interface to generic CRUD operations on a repository for a specific type. This one we can see more often on several Java implementations.

  • Interface with generic CRUD operations using the pagination feature.

  • Interface for generic CRUD operations on a repository for a specific type. This repository follows reactive paradigms.

From the Java developer perspective, create an interface that is annotated with the Repository annotation and optionally extends one of the built-in repository interfaces.

So, given a Product entity where the ID is a long type, the repository would be:

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

}

There is no nomenclature restriction to make mandatory the Repository suffix. Such as, you might represent the repository of the Car’s entity as a Garage instead of CarRepository.

@Repository
public interface Garage extends CrudRepository<Car, String> {

}

2.2. Entity Classes

Entity classes are simple Java objects with fields or accessor methods designating each entity property.

You may use jakarta.persistence.Entity and the corresponding entity-related annotations of the Jakarta Persistence specification in the same package (such as jakarta.persistence.Id and jakarta.persistence.Column) to define and customize entities for relational databases.

You may use jakarta.nosql.Entity and the corresponding entity-related annotations of the Jakarta NoSQL specification in the same package (such as jakarta.nosql.Id and jakarta.nosql.Column) to define and customize entities for NoSQL databases.

Applications are recommended not to mix Entity annotations from different models for the sake of clarity and to allow for the Entity annotation to identify which provider is desired in cases where multiple types of Jakarta Data providers are available.

Repository implementations will search for the Entity annotation(s) they support and ignore other annotations.

2.3. Queries Methods

In Jakarta Data, besides finding by an ID, custom queries can be written in two ways:

  • Through Query annotation: It will create a method annotated with the @Query with the query.

  • Using query by method convention: Using some pattern vocabulary will provide a query.

Due to the variety of data sources, those resources might not work; it varies based on the Jakarta Data implementation and the database engine, which can provide queries on more than a Key or ID or not, such as a Key-value database.

2.3.1. Using the Query Annotation

The Query’s annotation will support a search expression as a String. The specification won’t define the query syntax, which might vary between vendors and data sources, such as SQL, JPA-QL, Cypher, CQL, etc.

@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
  @Query("SELECT p FROM Products p WHERE p.name=?1")  // example in JPQL
  Optional<Product> findByName(String name);
}

Jakarta Data also includes the Param annotation to define a binder annotation, where as with the query expression, each vendor will express the syntax freely such as ?, @, etc..

@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
  @Query("SELECT p FROM Products p WHERE p.name=:name")  // example in JPQL
  Optional<Product> findByName(@Param("name") String name);
}

2.3.2. Query by Method

The Query by method mechanism allows for creating query commands by conventions.

E.g.:

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

  List<Product> findByName(String name);

  @OrderBy("price")
  List<Product> findByNameLike(String namePattern);

  @OrderBy(value = "price", descending = true)
  List<Product> findByNameLikeAndPriceLessThan(String namePattern, float priceBelow);

}

The parsing query method name has two parts: the subject and the property.

The first part defines the query’s subject or condition, and the second the condition value; both forms the predicate.

A predicate can refer only to a direct property of the managed entity. We also have the option to handle entities with another class on them.

2.3.3. Entity Property Names

Within an entity, property names must be unique ignoring case. For simple entity properties, the field or accessor method name serves as the entity property name. In the case of embedded classes, entity property names are computed by concatenating the field or accessor method names at each level.

Assume an Order entity has an Address with a ZipCode. In that case, the access is order.address.zipCode. This form is used within annotations, such as @Query,

@Repository
public interface OrderRepository extends CrudRepository<Order, Long> {

  @Query("SELECT order FROM Order order WHERE order.address.zipCode=?1")
  List<Order> withZipCode(ZipCode zipCode);

}

For queries by method name, the resolution algorithm starts by interpreting the whole part (AddressZipCode) as the property and checks the domain class for a property with that name (uncapitalized). If the algorithm succeeds, it uses that property.

@Repository
public interface OrderRepository extends CrudRepository<Order, Long> {

  Stream<Order> findByAddressZipCode(ZipCode zipCode);

}

Although this should work for most cases, to resolve this ambiguity, you can use _ inside your method name to manually define traversal points.

@Repository
public interface OrderRepository extends CrudRepository<Order, Long> {

  Stream<Order> findByAddress_ZipCode(ZipCode zipCode);

}
Define as a priority following standard Java naming conventions, camel case, using underscore as the last resort.

In queries by method name, Id is an alias for the entity property that is designated as the id. Entity property names that are used in queries by method name must not contain reserved words.

2.3.3.1. Query Methods Keywords

The following table lists the subject keywords generally supported by Jakarta Data.

Keyword Description

findBy

General query method returning the repository type.

deleteBy

Delete query method returning either no result (void) or the delete count.

countBy

Count projection returning a numeric result.

existsBy

Exists projection, returning typically a boolean result.

Jakarta Data implementations support the following list of predicate keywords to the extent that the database is capable of the behavior. A repository method will raise jakarta.data.exceptions.DataException or a more specific subclass of the exception if the database does not provide the requested functionality.

Keyword Description Method signature Sample

And

The and operator.

findByNameAndYear

Or

The or operator.

findByNameOrYear

Between

Find results where the property is between the given values

findByDateBetween

Empty

Find results where the property is an empty collection or has a null value.

deleteByPendingTasksEmpty

LessThan

Find results where the property is less than the given value

findByAgeLessThan

GreaterThan

Find results where the property is greater than the given value

findByAgeGreaterThan

LessThanEqual

Find results where the property is less than or equal to the given value

findByAgeLessThanEqual

GreaterThanEqual

Find results where the property is greater than or equal to the given value

findByAgeGreaterThanEqual

Like

Finds string values "like" the given expression

findByTitleLike

IgnoreCase

Requests that string values be compared independent of case for query conditions and ordering.

findByStreetNameIgnoreCaseLike

In

Find results where the property is one of the values that are contained within the given list

findByIdIn

Null

Finds results where the property has a null value.

findByYearRetiredNull

True

Finds results where the property has a boolean value of true.

findBySalariedTrue

False

Finds results where the property has a boolean value of false.

findByCompletedFalse

OrderBy

Specify a static sorting order followed by the property path and direction of ascending.

findByNameOrderByAge

OrderBy____Desc

Specify a static sorting order followed by the property path and direction of descending.

findByNameOrderByAgeDesc

OrderBy____Asc

Specify a static sorting order followed by the property path and direction of ascending.

findByNameOrderByAgeAsc

OrderBy____(Asc|Desc)*(Asc|Desc)

Specify several static sorting orders

findByNameOrderByAgeAscNameDescYearAsc

Logical Operator Precedence

For relational databases, the logical operator And takes precedence over Or, meaning that And is evaluated on conditions before Or when both are specified on the same method. For other database types, the precedence is limited to the capabilities of the database. For example, some graph databases are limited to precedence in traversal order.

2.4. Special Parameter Handling

Jakarta Data also supports particular parameters to define pagination and sorting.

Jakarta Data recognizes, when specified on a repository method after the query parameters, specific types, like Limit, Pageable, and Sort, to dynamically apply limits, pagination, and sorting to queries. The following example demonstrates these features:

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

  List<Product> findByName(String name, Pageable pageable);

  List<Product> findByNameLike(String pattern, Limit max, Sort... sorts);

}

You can define simple sorting expressions by using property names.

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

You can combine sorting with a starting page and maximum page size by using property names.

Pageable pageable = Pageable.ofSize(20).page(1).sortBy(Sort.desc("price"));
first20 = products.findByNameLike(name, pageable);

2.5. 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.

2.5.1. 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.

2.5.2. Static Mechanisms for Sort Criteria

Sort criteria is 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 is evaluated first. When static sort criteria sorts entities to the same position, dynamic sort criteria is applied to further order those entities.

2.5.3. Dynamic Mechanisms for Sort Criteria

Sort criteria is provided dynamically to repository methods either via Sort parameters or via a Pageable parameter that has one or more Sort values. Sort andPageable containing Sort must not both be provided to the same method.

2.5.4. Examples of Sort Criteria Precedence

The following examples work through scenarios where static and dynamic sort criteria are provided to the same method.

// Sorts first by type. When type is the same, applies the Pageable's sort criteria
Page<User> findByNameStartsWithOrderByType(String namePrefix, Pageable pagination);

// Sorts first by type. When type is the same, applies the criteria in the Sorts
List<User> findByNameStartsWithOrderByType(String namePrefix, Sort... sorts);

// Sorts first by age. When age is the same, applies the Pageable's sort criteria
@OrderBy("age")
Page<User> findByNameStartsWith(String namePrefix, Pageable pagination);

// Sorts first by age. When age is the same, applies the criteria in the Sorts
@OrderBy("age")
List<User> findByNameStartsWith(String namePrefix, Sort... sorts);

// Sorts first by name. When name is the same, applies the Pageable's sort criteria
@Query("SELECT u FROM User u WHERE (u.age > ?1)")
@OrderBy("name")
KeysetAwarePage<User> olderThan(int age, Pageable pagination);

2.6. Keyset Pagination

Keyset pagination aims to reduce missed and duplicate results across pages by querying relative to the observed values of entity properties that constitute the sorting criteria. Keyset 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 keyset values automatically when KeysetAwareSlice or KeysetAwarePage are used as the repository method return type. The application invokes nextPageable or previousPageable on the keyset aware slice or page to obtain a Pageable which keeps track of the keyset values.

For example,

@Repository
public interface CustomerRepository extends CrudRepository<Customer, Long> {
  KeysetAwareSlice<Customer> findByZipcodeOrderByLastNameAscFirstNameAscIdAsc(
                                 int zipcode, Pageable pageable);
}

You can obtain the next page with,

for (Pageable p = Pageable.ofSize(50); p != null; ) {
  page = customers.findByZipcodeOrderByLastNameAscFirstNameAscIdAsc(55901, p);
  ...
  p = page.nextPageable();
}

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

Customer c = ...
Pageable p = Pageable.ofSize(50).afterKeyset(c.lastName, c.firstName, c.id);
page = customers.findByZipcodeOrderByLastNameAscFirstNameAscIdAsc(55902, p);

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

  • OrderBy name pattern of the repository method (as in the examples above) or @OrderBy annotation(s) on the repository method.

  • Sort parameters of the Pageable that is supplied to the repository method.

2.6.1. Example of Appending to Queries for Keyset Pagination

Without keyset pagination, a Jakarta Data provider that is based on Jakarta Persistence might compose the following JPQL for the findByZipcodeOrderByLastNameAscFirstNameAscIdAsc repository method from the prior example:

SELECT o FROM Customer o WHERE (o.zipCode = ?1)
                         ORDER BY o.lastName ASC, o.firstName ASC, o.id ASC

When keyset pagination is used, the keyset values from the Cursor of the Pageable are available as query parameters, allowing the Jakarta Data provider to append additional query conditions. For example,

SELECT o FROM Customer o WHERE (o.zipCode = ?1)
                           AND (   (o.lastName > ?2)
                                OR (o.lastName = ?2 AND o.firstName > ?3)
                                OR (o.lastName = ?2 AND o.firstName = ?3 AND o.id > ?4)
                               )
                         ORDER BY o.lastName ASC, o.firstName ASC, o.id ASC

2.6.2. Avoiding Missed and Duplicate Results

Because searching for the next page of results is relative to a last known position, it is possible with keyset 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 keyset 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 properties which are used as the sort criteria, keyset 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, keyset pagination cannot prevent the entity from being missed or possibly appearing a second time due to its changed values.

2.6.3. Restrictions on use of Keyset Pagination

  • The repository method signature must return KeysetAwareSlice or KeysetAwarePage. A repository method with return type of KeysetAwareSlice or KeysetAwarePage must raise UnsupportedOperationException if the database is incapable of keyset pagination.

  • The repository method signature must accept a Pageable parameter.

  • Sort criteria must be provided and should be minimal.

  • The combination of provided sort criteria must uniquely identify each entity.

  • Page numbers for keyset 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 keyset pagination.

  • Page totals and result totals are not accurate for keyset pagination and must not be relied upon.

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

  • A repository method that is annotated with @Query and performs keyset pagination must omit the ORDER BY clause from the provided query and instead must supply the sort criteria via @OrderBy annotations or Sort parameters of Pageable. 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.

2.6.4. Keyset 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 CrudRepository<Customer, Long> {
  @Query("SELECT o FROM Customer o WHERE (o.totalSpent / o.totalPurchases > ?1)")
  KeysetAwareSlice<Customer> withAveragePurchaseAbove(float minimum, Pageable pagination);
}

Example traversal of pages:

for (Pageable p = Pageable.ofSize(25).sortBy(Sort.desc("yearBorn"), Sort.asc("name"), Sort.asc("id")));
     p != null; ) {
  page = customers.withAveragePurchaseAbove(50.0f, p);
  ...
  p = page.nextPageable();
}

3. Jakarta Data CDI Extension

To run in environments with Jakarta Contexts and Dependency Injection (CDI), Jakarta Data providers each register their own CDI extension (jakarta.enterprise.inject.spi.Extension) to produce the bean instances that are defined via the Repository annotation and injected via the Inject annotation. The Jakarta Data specification employs several strategies for reducing and avoiding conflicts between Jakarta Data providers. The entity annotation class is used as the primary strategy. The Jakarta Data provider name serves as a secondary strategy.

3.1. Entity Annotation Class

The jakarta.persistence.Entity annotation from the Jakarta Persistence specification can be used by repository entity classes for Jakarta Data providers that are backed by a Jakarta Persistence provider. Other Jakarta Data providers must not support the jakarta.persistence.Entity annotation.

The jakarta.nosql.Entity annotation from the Jakarta NoSQL specification can be used by repository entity classes for Jakarta Data providers that are backed by NoSQL databases. Other Jakarta Data providers must not support the jakarta.nosql.Entity annotation.

Jakarta Data providers that define custom entity annotations must follow the convention that the class name of all supported entity annotation types ends with Entity. This enables Jakarta Data providers to identify if a repository entity class contains entity annotations from different Jakarta Data providers so that the corresponding Repository can be ignored by Jakarta Data providers that should not provide it.

Jakarta Data provider CDI Extensions must ignore all Repository annotations where annotations for the corresponding entity are available at run time and none of the entity annotations are supported by the Jakarta Data provider. Ignoring these Repository annotations allows other Jakarta Data providers to handle them.

3.2. Jakarta Data Provider Name

The entity annotation class will usually be sufficient to avoid conflicts between Jakarta Data providers, but in cases where the entity annotation class is not sufficient, the application can designate the name of the desired Jakarta Data provider on the optional provider attribute of the Repository annotation.

Jakarta Data provider CDI Extensions must ignore all Repository annotations that designate a different provider’s name via the Repository.provider() annotation attribute. Ignoring these annotations allows other Jakarta Data providers to handle them.

4. Interoperability with other Jakarta EE Specifications

When running within a Jakarta EE product, other Jakarta EE Technologies might be available depending on the profile. This section defines how related technologies from other Jakarta EE Specifications interoperate with Jakarta Data.

4.1. Jakarta Transactions Usage

When running in an environment where Jakarta Transactions is available and a global transaction is active on the thread of execution for a repository operation and the data source backing the repository is capable of transaction enlistment, the repository operation enlists the data source resource as a participant in the transaction. The repository operation does not commit or roll back the transaction that was already present on the thread, but it might cause the transaction to be marked as rollback only (jakarta.transaction.Status.STATUS_MARKED_ROLLBACK) if the repository operation fails.

When running in an environment where Jakarta Transactions and Jakarta CDI are available, a repository method can be annotated with the jakarta.transaction.Transactional annotation, which is applied to the execution of the repository method.

4.2. Interceptor Annotations on Repository Methods

When a repository method is annotated with an interceptor binding annotation, the interceptor is bound to the repository bean according to the interceptor binding annotation of the repository interface method, causing the bound interceptor to be invoked around the repository method when it runs. This enables the use of interceptors such as jakarta.transaction.Transactional on repository methods when running in an environment where the Jakarta EE technology that provides the interceptor is available.