Contents

Overview

The Helidon SE Data Repository provides a unified API for working with database queries.

Data repository queries are an abstraction over Object–Relational Mapping, or ORM. This enables interfaces with query definitions to be translated into implementation classes at compile time.

The Helidon Data Repository supports Jakarta Persistence and major providers such as EclipseLink and Hibernate.

Maven Coordinates

To enable Data Repository, add the following dependency to your project’s pom.xml (see Managing Dependencies).

<dependency>
    <groupId>io.helidon.data</groupId>
    <artifactId>helidon-data</artifactId>
</dependency>
<dependency>
    <groupId>io.helidon.data.jakarta.persistence</groupId>
    <artifactId>helidon-data-jakarta-persistence</artifactId>
</dependency>
Copied

The Jakarta Persistence provider, such as EclipseLink, and the JDBC driver, such as MySQL, are required at runtime:

<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>org.eclipse.persistence.jpa</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>org.eclipse.persistence.core</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
Copied

Annotation Processor

Both the entity model and data repository interfaces require a specific annotation-processor configuration:

<annotationProcessorPaths>
    <path>
        <groupId>io.helidon.bundles</groupId>
        <artifactId>helidon-bundles-apt</artifactId>
        <version>${helidon.version}</version>
    </path>
    <path>
        <groupId>io.helidon.data.jakarta.persistence</groupId>
        <artifactId>helidon-data-jakarta-persistence-codegen</artifactId>
        <version>${helidon.version}</version>
    </path>
</annotationProcessorPaths>
Copied

Usage

The Data Repository provides an API and tooling for implementing database queries through interface method prototypes.

There are two ways in which such a query can be defined:

  • using the method name as the query definition

Optional<Pet> findByName(String name);
Copied
  • using a method annotated with @Data.Query

@Data.Query("SELECT p FROM Pet p WHERE p.category.name = :categoryName")
List<Pet> selectPetsByCategory(String categoryName);
Copied

Helidon Config

You must configure the data repository before using it.

In the example below, Helidon Config sets up the data repository using the EclipseLink provider and a MySQL database as a custom connection:

data:
  persistence-units:
    jakarta:
      - connection:
          username: "user"
          password: "password"
          url: "jdbc:mysql://localhost:3306/pets"
          jdbc-driver-class-name: "com.mysql.cj.jdbc.Driver"
          # EclipseLink properties
          properties:
            eclipselink.target-database: "MySQL"
            eclipselink.target-server: "None"
            jakarta.persistence.schema-generation.database.action: "none"
Copied

In the next example, Helidon Config sets up the data repository using the Hibernate provider and a MySQL database as a Hikari DataSource:

data:
  sources:
    sql:
      - name: "example"
        provider.hikari:
          username: "user"
          password: "changeit"
          url: "jdbc:mysql://localhost:3306/pets"
          jdbc-driver-class-name: "com.mysql.cj.jdbc.Driver"
  persistence-units:
    jakarta:
      - data-source: "example"
        properties:
          hibernate.dialect: "org.hibernate.dialect.MySQLDialect"
          jakarta.persistence.schema-generation.database.action: "drop-and-create"
Copied

DataSource is defined in a separate node, and its name is set to "example". This name is referenced in the corresponding persistence-units configuration node.

SE Application

The runtime initialization of the data repository is managed by the service registry. You can obtain repository interface instances using the service registry API:

private final KeeperRepository repository = Services.get(KeeperRepository.class);
Copied

Repository Interface

Data repository interfaces are annotated with @Data.Repository and extend the Data.GenericRepository interface.

The @Data.Repository annotation takes no arguments. The Data.GenericRepository declares no methods but has two generic type parameters, E and ID. E represents the persistence entity type and ID represents the type of the entity’s primary key attribute. Composite primary keys are not supported.

The Data.GenericRepository interface is extended by additional interfaces that add specific features:

InterfaceDescription
Data.GenericRepository<E, ID>Root interface with entity type and primary key type as generic arguments.
Data.BasicRepository<E, ID>Extends GenericRepository; adds a set of basic entity life-cycle operations.
Data.CrudRepository<E, ID>Extends BasicRepository; adds insert and update methods to provide full CRUD support.
Data.PageableRepository<E, ID>Extends GenericRepository; adds pagination support.

Repository Interface Methods

A repository interface may contain three kinds of methods:

  • Methods inherited from an ancestor interface

  • Methods with a query defined via the @Data.Query annotation

  • Methods with a query defined via the method name

The following PetRepository interface contains all of these: inherited methods from CrudRepository, the methods findByName and listNameOrderByName defined by method name, and selectPetsByCategory defined by the @Data.Query annotation:

@Data.Repository
public interface PetRepository extends Data.CrudRepository<Pet, Integer> {

    Optional<Pet> findByName(String name);

    Slice<String> listNameOrderByName(PageRequest request);

    @Data.Query("SELECT p FROM Pet p WHERE p.category.name = :categoryName")
    List<Pet> selectPetsByCategory(String categoryName);

}
Copied

Method with Query Defined by Method Name

This method type infers the query based on the method name and does not use a specific annotation.

The general method name syntax is illustrated below:

qbmn syntax

All parts of the pattern are optional except the return type keyword, such as get, find, list, stream, count and exists.

Method Name Prefix and Return Type

A method can have a user-defined prefix. The prefix is a sequence of letters and digits that does not match any return type keyword. This prefix has no influence on the query and can be used to distinguish between methods that have the same query but different return types. If a prefix is used, the following query return type keyword must start with a capital letter.

Optional<Keeper> findByName(String name);
Number countByName(String name);
long longCountByName(String name);
Copied

The query return type depends on the return type keyword:

KeywordReturn TypeDescription
countNumeric typeNumber of rows matching the query criteria
existsboolean or BooleanWhether at least one matching row exists
getQuery row typeSingle result that throws an exception if there are zero or multiple results
findOptional<…>Zero or single result that throws an exception if there are multiple results
listCollection or ListAll matching rows
listSlice or PagePageable result set
streamStreamStream of matching rows

Validation of the keyword–return type mapping is not fully enforced by the code generator, though this may change in future releases.

Projection in Method Name

The projection part is optional and follows directly after the return-type keyword. It consists of expression and property components:

KeywordExampleDescription
DistinctlistDistinctNameByTrainer_NameReturns only unique values.
First<number>listFirst10ByAgeReturns up to <number> rows.
MingetMinPointsReturns the minimum property value. Requires numeric type.
MaxgetMaxPointsReturns the maximum property value. Requires numeric type.
SumgetSumPointsReturns the sum of values. Requires numeric type.
AvggetAvgPointsReturns the average value. Requires floating point type.

The property part is the entity property name and it can contain underscores. An underscore is interpreted as a dot, which means navigation to a related entity attribute. For example, Keeper_Name on the Pet entity translates to the JPQL query SELECT p.keeper.name FROM Pet p.

@Entity
public class Pet {
    Keeper keeper;
}

@Entity
public class Keeper {
    String name;
}

@Data.Repository
public interface PetRepository extends Data.GenericRepository<Pet, Integer> {
    List<String> listKeeper_Name();
}
Copied
Criteria in Method Name

The criteria part of the method name is optional and represents the WHERE clause of the query. It is a logical expression composed of individual conditions joined by the AND and OR operators. A single criteria condition is the property, optionally followed by a set of criteria keywords. For example, NameIgnoreCaseNotEndsWith consists of the entity property name and the keywords IgnoreCase, Not, and EndsWith.

Criteria condition keywords are of two types:

  • IgnoreCase and Not modifiers that can appear before the condition keyword

  • the condition keyword itself, such as EndsWith

A condition keyword can consume method arguments. Each keyword consumes an exact number of arguments. Method arguments are consumed in the same order as the condition keywords appear in the method name.

Criteria modifiers:

KeywordDescription
NotNegates the next condition
IgnoreCaseMakes the next condition case-insensitive

Supported condition keywords:

KeywordArgsDescription
After1The property value is after the given value. Requires a Comparable property and argument. Intended for date and time. Effectively equivalent to GreaterThan.
Before1The property value is before the given value. Requires a Comparable property and argument. Intended for date and time. Effectively equivalent to LessThan.
Contains1The property value contains the given value. Requires a String property and argument.
EndsWith1The property value ends with the given value. Requires a String property and argument.
StartsWith1The property value starts with the given value. Requires a String property and argument.
Equal1The property value is equal to the given value.
LessThan1The property value is less than the given value. Requires a Comparable property and argument.
LessThanEqual1The property value is less or equal to the given value. Requires a Comparable property and argument.
GreaterThan1The property value is greater than the given value. Requires a Comparable property and argument.
GreaterThanEqual1The property value is greater or equal to the given value. Requires a Comparable property and argument.
Between2The property value is between the given values. Requires Comparable properties and arguments.
Like1The property value is LIKE the given value. Requires a String property and argument.
In1The property value is in the given collection. Requires a Collection argument.
Empty0The property value is empty. Requires a Collection property.
Null0The property value is NULL.
True0The property value is true. Requires boolean or Boolean property.
False0The property value is false. Requires boolean or Boolean property.

An example repository method with criteria:

// Returns Keeper entity with keepr.name matching provided name
// or throws an exception when no such entity exists
Keeper getByName(String name);
// Returns list of Keeper entities with keepr.age > provided age value
List<Keeper> listByAgeGreaterThan(int age);
// Checks whether at least one entity with keepr.age between provided
// min and max values exists
boolean existsByAgeBetween(int min, int max);
Copied

Logical operators:

KeywordDescription
AndLogical AND
OrLogical OR

An example repository method with criteria and logical operator:

Optional<Keeper> findByNameAndAge(String name, int age);
Copied

In JPQL, operator precedence places AND above OR as defined in Jakarta Persistence 3.1, section 4.6.6. The same rule applies to SQL.

Ordering in Method Name

The ordering part of the method name is optional and represents the ORDER BY clause of the query. It is a list of ordering rules. A single ordering rule is the property optionally followed by a direction keyword. If more than one ordering rule is present, the rules must be separated by direction keywords, so only the last keyword is optional.

Ordering keywords:

KeywordDescription
AscThe returned collection is sorted in ascending order.
DescThe returned collection is sorted in descending order.

The default direction is ascending when the keyword is omitted after the property.

An example repository method with ordering:

List<Keeper> listAllOrderByAgeAscName();
Copied
Method Name Grammar

The formal grammar for method names is as follows:

        method-name  :: <query> | <delete>

        query        :: <action> [ <projection> ] [ "By" <criteria>  [ "OrderBy" <order> ] ]
                            | <all-action> "All" [ "OrderBy" <order> ]
        delete       :: <del-action> "By" <criteria> ] | <del-action> [ "All" ]

        action       :: <action-l> | <prefix> <action-u>
        all-action   :: <all-action-l> | [ <prefix> ] <all-action-u>
        del-action   :: "delete" | [ <prefix> ] "Delete"

        prefix       :: [a-zA-Z0-9]*
        action-l     :: "count" |  "exists" | "get" | <all-action-l>
        action-u     :: "Count" |  "Exists" | "Get" | <all-action-u>
        all-action-l :: "find" | "list" | "stream"
        all-action-u :: "Find" | "List" | "Stream"

        projection   :: [ <expression> ] [ <property> ]
        expression   :: "First" <number> [ "Distinct" ] | "Distinct"
                            | "Max" | "Min" | "Sum" | "Avg"
        property     :: <identifier> [ "_" <identifier> ]
        identifier   :: [a-zA-Z][a-zA-Z0-9]*
        number       :: [0-9]+

        criteria     :: <condition> { <logical-operator> <condition> }
        condition    :: <property> [ [ "Not" ] [ "IgnoreCase" ] <operator>  ]
        operator     :: "After" | "Before" | Contains" | "EndsWith" | "StartsWith" | "Equal"
                            | "LessThan" | "LessThanEqual" | "GreaterThan" | "GreaterThanEqual"
                            | "Between" | "Like" | "In" | "Empty" | "Null" | "True" | "False"
        logical-operator :: "And" | "Or"

        order        :: <property> [ <direction> [ <order> ] ]
        direction    :: "Asc" | "Desc"
Copied

Method with Query Defined by @Data.Query Annotation

This method type must be annotated with @Data.Query. The annotation takes a single String value containing the database query. Currently, JPQL is supported. Method arguments must match the query parameters:

  • For named parameters, each named parameter in the query must correspond to a method argument with the same name. Order does not matter.

@Data.Query("SELECT p FROM Pet p WHERE p.category.name = :categoryName")
List<Pet> selectPetsByCategory(String categoryName);
Copied
  • For indexed parameters, each argument must appear in the same order as in the query. Indexing starts at 1.

@Data.Query("SELECT p.keeper FROM Pet p WHERE k.name = $1 AND p.category.name = $2")
Optional<Keeper> selectKeeper(String name, String category);
Copied

Supported return types include:

  • the query row type such as an entity class, an entity attribute, or a custom projection

  • List, Collection, Stream, or Optional with the query row type as the generic parameter

  • Page or Slice with the query row type as the generic parameter

Pagination

Pagination allows the caller to split a returned data collection into individual pages. When pagination is used, the repository method must have an argument of type PageRequest. The return type of the method is Slice or Page. The PageRequest argument defines the page size and the page index, starting from 0.

Returned page content types:

NameDescription
SliceContains the page data as a List or Stream and a PageRequest to retrieve this page.
PageContains the page data as a List or Stream, the total result size across all pages, and a PageRequest to retrieve this page.

An example repository method with pagination:

Slice<Keeper> listAll(PageRequest pageRequest);
Copied

Dynamic Ordering

The ordering part of the method name defines a static ordering rule that cannot be modified at runtime. Dynamic ordering allows the caller to define an additional ordering rule at runtime. Dynamic ordering is triggered by adding an argument of type Sort to the repository method. Both static and dynamic rules can be used together.

An example repository method with dynamic ordering:

List<Keeper> listByAgeBetween(int min, int max, Sort sort);
Copied

Static ordering rules from the method name are always applied first, and dynamic rules are added after them:

List<Keeper> listByAgeBetweenOrderByAge(int min, int max, Sort sort);
Copied

Persistence Session Access

The caller can access the persistence provider session to implement more complex tasks that the framework does not support directly. This feature is available to a data repository interface that extends the Data.SessionRepository<S> interface.

The generic argument S is the persistence session type, for example EntityManager. The Data.SessionRepository interface provides methods that supply a session managed by the data repository framework, so there is no need to handle the session instance lifecycle.

@Data.Repository
public interface KeeperRepository
        extends Data.GenericRepository<Keeper, Integer>, Data.SessionRepository<EntityManager> {
}
Copied

The session instance is available through the Data.SessionRepository<S> interface methods run and call:

public class PetService {

    private final KeeperRepository repository = Services.get(KeeperRepository.class);

    public List<Keeper> keeperQuery(String name) {
        return repository.call(em -> em.createQuery("SELECT k FROM Keeper k WHERE k.name = :name",
                                                    Keeper.class)
                .setParameter("name", name)
                .getResultList());
    }

    public void updateKeeperName(String name, int id) {
        repository.run(em -> em.createQuery("UPDATE Keeper k SET k.name = :name WHERE k.id = :id",
                                            Keeper.class)
                .setParameter("name", name)
                .setParameter("id", id)
                .executeUpdate());
    }

}
Copied

The session instance is valid only while run or call method is being executed. This instance must not be stored and used after this method has ended.

Transactions

Transaction handling is available through Helidon Transaction API.

To enable Helidon Transaction API, add the following dependency to your project’s pom.xml:

<dependency>
    <groupId>jakarta.transaction</groupId>
    <artifactId>jakarta.transaction-api</artifactId>
</dependency>
Copied

Helidon JTA Transaction support, such as Narayana, may be provided at runtime to enable JTA transaction type:

<dependency>
    <groupId>io.helidon.transaction</groupId>
    <artifactId>helidon-transaction-narayana</artifactId>
    <scope>runtime</scope>
</dependency>
Copied

If JTA transaction support is not provided, Helidon Data runtime will use RESOURCE_LOCAL transaction type.

Transaction Types and Annotations

The Tx class defines several ways how transactional support can be applied to transactional method executions. Those ways are defined in Tx.Type enum. The Tx class also defines annotations that can be used to mark methods for transactional execution based on Tx.Type enum.

EnumAnnotationDescription
MANDATORY@MandatoryA transaction must already be in effect when a method executes. If called outside a transaction context, a TxException is thrown. If called inside a transaction context, method execution continues under that context.
NEW@NewA new transaction is started when a method executes. If called outside a transaction context, a new transaction is begun. If called inside a transaction context, the current transaction is suspended, a new transaction is begun, and the method execution continues inside this new transaction context.
NEVER@NeverNo transaction must be in effect when a method executes. If called outside a transaction context, method execution continues outside a transaction context. If called inside a transaction context, a TxException is thrown.
REQUIRED@RequiredA transaction will be in effect when a method executes. If called outside a transaction context, a new transaction is begun. If called inside a transaction context, method execution continues inside that transaction context.
SUPPORTED@SupportedA transaction may optionally be in effect when a method executes. If called outside a transaction context, method execution continues outside a transaction context. If called inside a transaction context, method execution continues inside that transaction context.
UNSUPPORTED@UnsupportedNo transaction will be in effect when a method executes. If called outside a transaction context, method execution continues outside a transaction context. If called inside a transaction context, the current transaction is suspended, method execution continues outside a transaction context, and the previously suspended transaction is resumed after method execution completes.

Transaction Methods

The Tx class provides several methods for executing tasks within a transaction:

MethodDescription
transaction(Callable<T> task)Executes a task with a managed transaction of type REQUIRED.
transaction(Type type, Callable<T> task)Executes a task with a managed transaction of the specified type.
transaction(CheckedRunnable<Exception> task)Executes a task with a managed transaction of type REQUIRED without returning a result.
transaction(Type type, CheckedRunnable<Exception> task)Executes a task with a managed transaction of the specified type without returning a result.

Usage

The Tx.Type enum is used to control the transactional behavior of methods. By specifying the desired transactional behavior using one of the enum values, developers can ensure that their methods are executed with the correct transactional context. For example, using REQUIRED ensures that a method is always executed within a transaction, while using NEVER ensures that a method is never executed within a transaction.

In this example, the doSomething() method is annotated with @Tx.Required, ensuring that it is always executed within a transaction. The doSomethingElse() method is annotated with @Tx.Never, ensuring that it is never executed within a transaction:

@Service.Singleton
public class PetService {
    @Tx.Required
    public void doSomething() {
        // Method execution will always be within a transaction
    }

    @Tx.Never
    public void doSomethingElse() {
        // Method execution will never be within a transaction
    }
}
Copied

PetService class instance is obtained from service registry.

In this example, lambda expression in the doSomething() method is executed using Tx.transaction method with Tx.Type.REQUIRED argument, ensuring that it is always executed within a transaction. Lambda expression in the doSomethingElse() method is executed using Tx.transaction method with Tx.Type.NEVER argument, ensuring that it is never executed within a transaction:

public class KeeperService {
    public void doSomething() {
        Tx.transaction(Tx.Type.REQUIRED,
                       () -> {
                           // Method execution will always be within a transaction
                       });
    }

    public void doSomethingElse() {
        Tx.transaction(Tx.Type.NEVER,
                       () -> {
                           // Method execution will never be within a transaction
                       });
    }
}
Copied