Module jakarta.data


module jakarta.data

Jakarta Data standardizes a programming model where data is represented by simple Java classes and where operations on data are represented by interface methods.

The application defines simple Java objects called entities to represent data in the database. Fields or accessor methods designate each entity property. For example,

 @Entity
 public class Product {
     @Id
     public long id;
     public String name;
     public float price;
     public int yearProduced;
     ...
 }
 

A repository is an interface annotated with the Repository annotation. A repository declares methods which perform queries and other operations on entities. For example,

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

     @Insert
     void create(Product prod);

     @OrderBy("price")
     List<Product> findByNameIgnoreCaseLikeAndPriceLessThan(String namePattern, float max);

     @Query("UPDATE Product SET price = price * (1.0 - ?1) WHERE yearProduced <= ?2")
     int discountOldInventory(float rateOfDiscount, int maxYear);

     ...
 }
 

Repository interfaces are implemented by the container/runtime and are made available to applications via the jakarta.inject.Inject annotation. For example,

 @Inject
 Products products;

 ...
 products.create(newProduct);

 found = products.findByNameIgnoreCaseLikeAndPriceLessThan("%cell%phone%", 900.0f);

 numDiscounted = products.discountOldInventory(0.15f, Year.now().getValue() - 1);
 

Jakarta Persistence and Jakarta NoSQL define programming models for entity classes that may be used with Jakarta Data:

  • jakarta.persistence.Entity and the corresponding entity-related annotations of the Jakarta Persistence specification may be used to define entities stored in a relational database, or
  • jakarta.nosql.Entity and the corresponding entity-related annotations of the Jakarta NoSQL specification may be used to define entities stored in a NoSQL database.

A Jakarta Data provider may define its own programming model for entity classes representing some other arbitrary kind of data.

Methods of repository interfaces must be styled according to a well-defined set of conventions, which instruct the container/runtime about the desired data access operation to perform. These conventions consist of patterns of reserved keywords within the method name, method parameters with special meaning, method return types, and annotations placed upon the method and its parameters.

Built-in repository superinterfaces, such as DataRepository, are provided as a convenient way to inherit commonly used methods and are parameterized by the entity type and by its id type. Other built-in repository interfaces, such as BasicRepository, may be used in place of DataRepository and provide a base set of predefined repository operations serving as an optional starting point. The Java application programmer may extend these built-in interfaces, adding custom methods. Alternatively, the programmer may define a repository interface without inheriting the built-in superinterfaces. A programmer may even copy individual method signatures from the built-in repositories to a repository which does not inherit any built-in superinterface. This is possible because the methods of the built-in repository superinterfaces respect the conventions defined for custom repository methods.

The following example shows an entity class, an embeddable class, and a repository interface:

 @Entity
 public class Purchase {
     @Id
     public String purchaseId;
     @Embedded
     public Address address;
     ...
 }

 @Embeddable
 public class Address {
     public int zipCode;
     ...
 }

 @Repository
 public interface Purchases {
     @OrderBy("address.zipCode")
     List<Purchase> findByAddressZipCodeIn(List<Integer> zipCodes);

     @Query("WHERE address.zipCode = ?1")
     List<Purchase> forZipCode(int zipCode);

     @Save
     Purchase checkout(Purchase purchase);
 }
 

Entities

An entity programming model typically specifies an entity-defining annotation that is used to identify entity classes. For Jakarta Persistence, this is jakarta.persistence.Entity. For Jakarta NoSQL, it is jakarta.nosql.Entity. A provider may even have no entity-defining annotation and feature a programming model for entity classes where the entity classes are unannotated.

Furthermore, an entity programming model must define an annotation which identifies the field or property holding the unique identifier of an entity. For Jakarta Persistence, it is jakarta.persistence.Id or jakarta.persistence.EmbeddedId. For Jakarta NoSQL, it is jakarta.nosql.Id. Alternatively, an entity programming model might allow the identifier field or property to be identified via some convention. Every entity has a unique identifier.

An entity has an arbitrary number of persistent fields or properties.

Persistent field names

Each persistent field of an entity or embeddable class is assigned a name:

  • when direct field access is used, the name of a persistent field is simply the name of the Java field, but
  • when property-based access is used, the name of the field is derived from the accessor methods, according to JavaBeans conventions.

Within a given entity class or embeddable class, names assigned to persistent fields must be unique ignoring case.

Furthermore, within the context of a given entity, each persistent field of an embeddable class reachable by navigation from the entity class may be assigned a compound name. The compound name is obtained by concatenating the names assigned to each field traversed by navigation from the entity class to the persistent field of the embedded class, optionally joined by a delimiter.

  • For parameters of a Find method, the delimiter is _.
  • For path expressions within a query, the delimiter is ..
  • For method names in Query by Method Name, the delimiter is _ and it is optional. For example, findByAddress_ZipCode or findByAddressZipCode are both legal.
  • For arguments to constructor methods of Sort, the delimiter is _ or ..
  • For the value member of the OrderBy or By annotation the delimiter is _ or ..

A persistent field name used in a Query by Method Name must not contain a keyword reserved by Query by Method Name.

Persistent field types (basic types)

The following is a list of valid basic entity attribute types. These can be used as the types of repository method parameters for the respective entity attribute.

Basic Types for Entity Attributes
Category Basic Types Notes
Primitives and primitive wrappers boolean and Boolean
byte and Byte
char and Character
double and Double
float and Float
int and Integer
long and Long
short and Short
Binary data byte[] Not sortable.
Enumerated types enum types It is provider-specific whether sorting is based on Enum.ordinal() or Enum.name(). The Jakarta Persistence default of ordinal can be overridden with the jakarta.persistence.Enumerated annotation.
Large numbers BigDecimal
BigInteger
Textual data String
Time and Dates Instant
LocalDate
LocalDateTime
LocalTime
Universally unique identifier UUID

A Jakarta Data provider might allow additional entity attribute types.

Lifecycle methods

A lifecycle method makes changes to persistent data in the data store. A lifecycle method must be annotated with a lifecycle annotation such as Insert, Update, Save, or Delete. The method must accept a single parameter, whose type is either:

  • the class of the entity, or
  • List<E> or E[] where E is the class of the entities.

The annotated method must be declared void, or, except in the case of @Delete, have a return type that is the same as the type of its parameter.

Lifecycle Annotations
Annotation Description Example
Delete deletes entities @Delete
public void remove(person);
Insert creates new entities @Insert
public List<Employee> add(List<Employee> newEmployees);
Save update if exists, otherwise insert @Save
Product[] saveAll(Product... products)
Update updates an existing entity @Update
public boolean modify(Product modifiedProduct);

Refer to the API documentation for Insert, Update, Delete, and Save for further information about these annotations.

JDQL query methods

The Query annotation specifies that a method executes a query written in Jakarta Data Query Language (JDQL) or Jakarta Persistence Query Language (JPQL). A Jakarta Data provider is not required to support the complete JPQL language, which targets relational data stores.

Each parameter of the annotated method must either:

  • have exactly the same name (the parameter name in the Java source, or a name assigned by @Param) 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.

The Param annotation associates a method parameter with a named parameter. The Param annotation is unnecessary when the method parameter name matches the name of a named parameter and the application is compiled with the -parameters compiler option making parameter names available at runtime.

 // example using named parameters
 @Query("where age between :min and :max order by age")
 List<Person> peopleInAgeRange(int min, int max);
 
 // example using an ordinal parameter
 @Query("where ssn = ?1 and deceased = false")
 Optional<Person> person(String ssn);
 

Refer to the API documentation for @Query for further information.

Query by Method Name

Repository methods following the Query by Method Name pattern must include the By keyword in the method name and must not include the @Find annotation, @Query annotation, or any lifecycle annotations on the method or any data access related annotations on the method parameters. Query conditions are determined by the portion of the method name following the By keyword.

Query By Method Name
Prefix Description Example
countBy counts the number of entities countByAgeGreaterThanEqual(ageLimit)
deleteBy for delete operations deleteByStatus("DISCONTINUED")
existsBy for determining existence existsByYearHiredAndWageLessThan(2022, 60000)
find...By for find operations findByHeightBetween(minHeight, maxHeight)
updateBy for simple update operations updateByIdSetModifiedOnAddPrice(productId, now, 10.0)

The conditions are defined by the portion of the repository method name (referred to as the Predicate) that follows the By keyword, in the same order specified. Most conditions, such as Like or LessThan, correspond to a single method parameter. The exception to this rule is Between, which corresponds to two method parameters.

Key-value and Wide-Column databases raise UnsupportedOperationException for queries on attributes other than the identifier/key.

Reserved keywords for Query by Method Name

Reserved for Predicate
Keyword Applies to Description Example Unavailable In
And conditions Requires both conditions to be satisfied in order to match an entity. findByNameLikeAndPriceLessThanEqual(namePattern, maxPrice) Key-value
Wide-Column
Between numeric, strings, time Requires that the entity's attribute value be within the range specified by two parameters, inclusive of the parameters. The minimum is listed first, then the maximum. findByAgeBetween(minAge, maxAge) Key-value
Wide-Column
Contains strings Requires that a substring of the entity's attribute value matches the parameter value, which can be a pattern. findByNameContains(middleName) Key-value
Wide-Column
Document
Graph
EndsWith strings Requires that the characters at the end of the entity's attribute value match the parameter value, which can be a pattern. findByNameEndsWith(surname) Key-value
Wide-Column
Document
Graph
False boolean Requires that the entity's attribute value has a boolean value of false. findByCanceledFalse() Key-value
Wide-Column
GreaterThan numeric, strings, time Requires that the entity's attribute value be larger than the parameter value. findByStartTimeGreaterThan(startedAfter) Key-value
Wide-Column
GreaterThanEqual numeric, strings, time Requires that the entity's attribute value be at least as big as the parameter value. findByAgeGreaterThanEqual(minimumAge) Key-value
Wide-Column
IgnoreCase strings Requires case insensitive comparison. For query conditions as well as ordering, the IgnoreCase keyword can be specified immediately following the entity property name. countByStatusIgnoreCaseNotLike("%Delivered%")
findByZipcodeOrderByStreetIgnoreCaseAscHouseNumAsc(55904)
Key-value
Wide-Column
Document
Graph
In all attribute types Requires that the entity's attribute value be within the list that is the parameter value. findByNameIn(names) Key-value
Wide-Column
Document
Graph
LessThan numeric, strings, time Requires that the entity's attribute value be less than the parameter value. findByStartTimeLessThan(startedBefore) Key-value
Wide-Column
LessThanEqual numeric, strings, time Requires that the entity's attribute value be at least as small as the parameter value. findByAgeLessThanEqual(maximumAge) Key-value
Wide-Column
Like strings Requires that the entity's attribute value match the parameter value, which can be a pattern. findByNameLike(namePattern) Key-value
Wide-Column
Document
Graph
Not condition Negates a condition. deleteByNameNotLike(namePattern)
findByStatusNot("RUNNING")
Key-value
Wide-Column
Null nullable types Requires that the entity's attribute has a null value. findByEndTimeNull()
findByAgeNotNull()
Key-value
Wide-Column
Document
Graph
Or conditions Requires at least one of the two conditions to be satisfied in order to match an entity. findByPriceLessThanEqualOrDiscountGreaterThanEqual(maxPrice, minDiscount) Key-value
Wide-Column
StartsWith strings Requires that the characters at the beginning of the entity's attribute value match the parameter value, which can be a pattern. findByNameStartsWith(firstTwoLetters) Key-value
Wide-Column
Document
Graph
True boolean Requires that the entity's attribute value has a boolean value of true. findByAvailableTrue() Key-value
Wide-Column


Reserved for Subject
Keyword Applies to Description Example Unavailable In
First find...By Limits the amount of results that can be returned by the query to the number that is specified after First, or absent that to a single result. findFirst25ByYearHiredOrderBySalaryDesc(int yearHired)
findFirstByYearHiredOrderBySalaryDesc(int yearHired)
Key-value
Wide-Column
Document
Graph


Reserved for Order Clause
Keyword Description Example
Asc Specifies ascending sort order for findBy queries findByAgeOrderByFirstNameAsc(age)
Desc Specifies descending sort order for findBy queries findByAuthorLastNameOrderByYearPublishedDesc(surname)
OrderBy Sorts results of a findBy query according to one or more entity attributes. Multiple attributes are delimited by Asc and Desc, which indicate ascending and descending sort direction. Precedence in sorting is determined by the order in which attributes are listed. findByStatusOrderByYearHiredDescLastNameAsc(empStatus)

Key-value and Wide-Column databases raise UnsupportedOperationException if an order clause is present.

Reserved for future use

The specification does not define behavior for the following keywords, but reserves them as keywords that must not be used as entity attribute names when using Query by Method Name. This gives the specification the flexibility to add them in future releases without introducing breaking changes to applications.

Reserved for query conditions: AbsoluteValue, CharCount, ElementCount, Empty Rounded, RoundedDown, RoundedUp, Trimmed, WithDay, WithHour, WithMinute, WithMonth, WithQuarter, WithSecond, WithWeek, WithYear.

Reserved for find...By and count...By: Distinct.

Reserved for updates: Add, Divide, Multiply, Set, Subtract.

Wildcard characters

Wildcard characters for patterns are determined by the data access provider. For Jakarta Persistence providers, _ matches any one character and % matches 0 or more characters.

Logical operator precedence

For relational databases, the logical operator And is evaluated on conditions before Or when both are specified on the same method. Precedence for other database types is limited to the capabilities of the database.

Return types for Query by Method Name

The following is a table of valid return types. The Method column shows name patterns for Query by Method Name. For example, to identify the valid return types for a method, findNamed(String name, PageRequest pagination), refer to the row for find...By...(..., PageRequest).

Return Types for Query by Method Name
Method Return Types Notes
countBy... long,
int
Jakarta Persistence providers limit the maximum to Integer.MAX_VALUE
deleteBy...,
updateBy...
void,
boolean,
long,
int
Jakarta Persistence providers limit the maximum to Integer.MAX_VALUE
existsBy... boolean For determining existence.
find...By... E,
Optional<E>
For queries returning a single item (or none)
find...By... E[],
List<E>
For queries where it is possible to return more than 1 item.
find...By... Stream<E> The caller must arrange to close all streams that it obtains from repository methods.
find...By...(..., PageRequest) Page<E>, CursoredPage<E> For use with pagination

Parameter-based automatic query methods

The Find annotation indicates that the repository method is a parameter-based automatic query method. In this case, the method name does not determine the semantics of the method, and the query conditions are determined by the method parameters.

Each parameter of the annotated method must either:

  • have exactly the same type and name (the parameter name in the Java source, or a name assigned by @By) as a persistent field or property of the entity class, or
  • be of type Limit, Sort, Order, or PageRequest.

A parameter may be annotated with the By annotation to specify the name of the entity attribute that the argument is to be compared with. If the By annotation is missing, the method parameter name must match the name of an entity attribute and the repository must be compiled with the -parameters compiler option so that parameter names are available at runtime.

Each parameter determines a query condition, and each such condition is an equality condition. All conditions must match for a record to satisfy the query.

 @Find
 @OrderBy("lastName")
 @OrderBy("firstName")
 List<Person> peopleByAgeAndNationality(int age, Country nationality);
 
 @Find
 Optional<Person> person(String ssn);
 

The _ character may be used in a method parameter name to reference an embedded attribute.

 @Find
 @OrderBy("address.zip")
 Stream<Person> peopleInCity(String address_city);
 

The following examples illustrate the difference between Query By Method Name and parameter-based automatic query methods. Both methods accept the same parameters and have the same behavior.

 // Query by Method Name
 Vehicle[] findByMakeAndModelAndYear(String makerName, String model, int year, Sort<?>... sorts);

 // parameter-based conditions
 @Find
 Vehicle[] searchFor(String make, String model, int year, Sort<?>... sorts);
 

For further information, refer to the API documentation for @Find.

Special parameters

A repository method annotated @Query, @Find or following the Query by Method Name pattern may have special parameters of type Limit, Order, Sort, or PageRequest if the method return type indicates that the method may return multiple entities. Special parameters occur after parameters related to query conditions and JDQL query parameters, and enable capabilities such as pagination, limits, and sorting.

Limits

The number of results returned by a single invocation of a repository find method may be limited by adding a parameter of type Limit. The results may even be limited to a positioned range. For example,

 @Query("WHERE (fullPrice - salePrice) / fullPrice >= ?1 ORDER BY salePrice DESC")
 Product[] highlyDiscounted(float minPercentOff, Limit limit);

 ...
 first50 = products.highlyDiscounted(0.30, Limit.of(50));
 ...
 second50 = products.highlyDiscounted(0.30, Limit.range(51, 100));
 

Pagination

A repository find method with a parameter of type PageRequest allows its results to be split and retrieved in pages. For example,

 Product[] findByNameLikeOrderByAmountSoldDescNameAsc(
           String pattern, PageRequest<Product> pageRequest);
 ...
 page1 = products.findByNameLikeOrderByAmountSoldDescNameAsc(
                  "%phone%", PageRequest.of(Product.class).size(20));
 

Sorting

When a page is requested with a PageRequest, dynamic sorting criteria may be supplied via the method PageRequest.sortBy(Sort) and its overloads. For example,

 Product[] findByNameLike(String pattern, PageRequest<Product> pagination);

 ...
 PageRequest<Product> page1Request = PageRequest.of(Product.class)
                                                .size(25)
                                                .sortBy(Sort.desc("price"),
                                                        Sort.asc("name"));
 page1 = products.findByNameLikeAndPriceBetween(
                 namePattern, minPrice, maxPrice, page1Request);
 

An alternative when using the StaticMetamodel is to obtain the page request from an Order instance, as follows,

 PageRequest<Product> pageRequest = Order.by(_Product.price.desc(),
                                             _Product.name.asc())
                                         .pageSize(25));
 

To supply sort criteria dynamically without using pagination, an instance of Order may be populated with one or more instances of Sort and passed to the repository find method. For example,

 Product[] findByNameLike(String pattern, Limit max, Order<Product> sortBy);

 ...
 found = products.findByNameLike(namePattern, Limit.of(25), Order.by(
                                 Sort.desc("price"),
                                 Sort.desc("amountSold"),
                                 Sort.asc("name")));
 

Generic, untyped Sort criteria can be supplied directly to a repository method with a variable arguments Sort<?>... parameter. For example,

 Product[] findByNameLike(String pattern, Limit max, Sort<?>... sortBy);

 ...
 found = products.findByNameLike(namePattern, Limit.of(25),
                                 Sort.desc("price"),
                                 Sort.desc("amountSold"),
                                 Sort.asc("name"));
 

Repository default methods

A repository interface may declare any number of default methods with user-written implementations.

Resource accessor methods

In advanced scenarios, the application program might make direct use of some underlying resource acquired by the Jakarta Data provider, such as a javax.sql.DataSource, java.sql.Connection, or even an jakarta.persistence.EntityManager.

To expose access to an instance of such a resource, the repository interface may declare an accessor method, a method with no parameters whose return type is the type of the resource, for example, one of the types listed above. When this method is called, the Jakarta Data provider supplies an instance of the requested type of resource.

For example,

 @Repository
 public interface Cars extends BasicRepository<Car, Long> {
     ...

     EntityManager getEntityManager();

     default Car[] advancedSearch(SearchOptions filter) {
         EntityManager em = getEntityManager();
         ... use entity manager
         return results;
     }
 }
 

If the resource type inherits from AutoCloseable and the accessor method is called from within an invocation of a default method of the repository, the Jakarta Data provider automatically closes the resource after the invocation of the default method ends. On the other hand, if the accessor method is called from outside the scope of a default method of the repository, it is not automatically closed, and the application programmer is responsible for closing the resource instance.

Precedence of repository methods

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

  1. If the method is a Java default method, then the provided implementation is used.
  2. If a 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 a 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.
  5. If a method is named according to the conventions of Query by Method Name, then the implementation follows the Query by Method Name pattern.

A repository method which does not fit any of the listed patterns and is not handled as a vendor-specific extension must either cause an error at build time or raise UnsupportedOperationException at runtime.

Identifying the type of entity

Most repository methods perform operations related to a type of entity. In some cases, the entity type is explicit within the signature of the repository method, and in other cases, such as countBy... and existsBy... the entity type cannot be determined from the method signature and a primary entity type must be defined for the repository.

Methods where the entity type is explicitly specified

In the following cases, the entity type is determined by the signature of the repository method.

  • For repository methods annotated with Insert, Update, Save, or Delete where the method parameter type is a type, an array of a type, or is parameterized with a type annotated as an entity, such as MyEntity, MyEntity[], or List<MyEntity>, the entity type is determined by the method parameter type.
  • For find and delete methods where the return type is a type, an array of a type, or is parameterized with a type annotated as an entity, such as MyEntity, MyEntity[], or Page<MyEntity>, the entity type is determined by the method return type.

Identifying a primary entity type:

The following precedence, from highest to lowest, is used to determine a primary entity type for a repository.

  1. The primary entity type for a repository interface may be specified explicitly by having the repository interface inherit a superinterface like CrudRepository, where the primary entity type is the argument to the first type parameter of the superinterface. For example, Product, in,
     @Repository
     public interface Products extends CrudRepository<Product, Long> {
         // applies to the primary entity type: Product
         int countByPriceLessThan(float max);
     }
     
  2. Otherwise, if the repository declares lifecycle methods—that is, has methods annotated with a lifecycle annotation like Insert, Update, Save, or Delete, where the method parameter type is a type, an array of a type, or is parameterized with a type annotated as an entity—and all of these methods share the same entity type, then the primary entity type for the repository is that entity type. For example,
     @Repository
     public interface Products {
         @Insert
         List<Product> add(List<Product> p);
    
         @Update
         Product modify(Product p);
    
         @Save
         Product[] save(Product... p);
    
         // applies to the primary entity type: Product
         boolean existsByName(String name);
     }
     

Jakarta Validation

When a Jakarta Validation provider is present, constraints that are defined on repository method parameters and return values are validated according to the section, "Method and constructor validation", of the Jakarta Validation specification.

The jakarta.validation.Valid annotation opts in to cascading validation, causing constraints within the objects that are supplied as parameters or returned as results to also be validated.

Repository methods raise jakarta.validation.ConstraintViolationException if validation fails.

The following is an example of method validation, where the parameter to findByEmailIn must not be the empty set, and cascading validation, where the Email and NotNull constraints on the entity that is supplied to save are validated,

 import jakarta.validation.Valid;
 import jakarta.validation.constraints.Email;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 ...

 @Repository
 public interface AddressBook extends DataRepository<Contact, Long> {

     List<Contact> findByEmailIn(@NotEmpty Set<String> emails);

     @Save
     void save(@Valid Contact c);
 }

 @Entity
 public class Contact {
     @Email
     @NotNull
     public String email;
     @Id
     public long id;
     ...
 }
 

Jakarta Interceptors

A repository interface or method of a repository interface may be annotated with an interceptor binding annotation. In the Jakarta EE environment, or in any other environment where Jakarta Interceptors is available and integrated with Jakarta CDI, the repository implementation is instantiated by the CDI bean container, and the interceptor binding type is declared @Inherited, the interceptor binding annotation is inherited by the repository implementation, and the interceptors bound to the annotation are applied automatically by the implementation of Jakarta Interceptors.

Jakarta Transactions

When Jakarta Transactions is available, repository methods can participate in global transactions. If a global transaction is active on the thread of execution in which a repository method is called, and the data source backing the repository is capable of transaction enlistment, then the repository operation is performed within the context of the global transaction.

The repository operation must not not commit or roll back a transaction which was already associated with the thread in which the repository operation was called, but it might cause the transaction to be marked for rollback if the repository operation fails, that is, it may set the transaction status to Status.STATUS_MARKED_ROLLBACK.

A repository interface or method of a repository interface may be marked with the annotation jakarta.transaction.Transactional. When a repository operation marked @Transactional is called in an environment where both Jakarta Transactions and Jakarta CDI are available, the semantics of this annotation are observed during execution of the repository operation.