Getting Started with Jakarta Data and Hibernate


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Jakarta Data standardizes stateless repositories based on Jakarta Persistence and Jakarta NoSQL so that you can concentrate on your business code instead of handling the technical details of simple queries.

In this article, I will briefly introduce the new specification and give you an overview of its stateless repositories. In future articles, we will dive deeper into repository definitions and the creation of custom queries.

Dependencies

Like all Jakarta specifications, Jakarta Data only provides an API jar that contains everything you need to implement your application.

<dependency>
	<groupId>jakarta.data</groupId>
	<artifactId>jakarta.data-api</artifactId>
	<version>${jakartaData.version}</version>
</dependency>

You have to provide an implementation at runtime. In the case of Jakarta Data, this implementation can be based on Jakarta Persistence or Jakarta NoSQL.

For this article, I used Hibernate ORM as my Jakarta Data implementation. Starting with version 6.6.0, it also implements the Jakarta Data specification.

<dependency>
	<groupId>org.hibernate.orm</groupId>
	<artifactId>hibernate-core</artifactId>
	<version>${hibernate.version}</version>
</dependency>

In addition to the hibernate-core dependency, you all also need to configure Hibernate’s metamodel generator in your Maven build. It generates Jakarta Data’s repository implementations at compile time.

<build>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-compiler-plugin</artifactId>
			<version>3.11.0</version>
			<configuration>
				<annotationProcessorPaths>
					<path>
						<groupId>org.hibernate.orm</groupId>
						<artifactId>hibernate-jpamodelgen</artifactId>
						<version>${hibernate.version}</version>
					</path>
				</annotationProcessorPaths>
			</configuration>
		</plugin>
	</plugins>
</build>

Entity mappings

In contrast to other specifications and frameworks you might know, Jakarta Data is based on a relatively vague definition of an entity.

It defines an entity as a representation of persistent data. Every entity class has to define an identifier to distinguish different data representations, e.g., a primary key in a relational database table.

Jakarta Data is designed to be compatible with existing specifications, such as Jakarta Persistence and Jakarta NoSQL, rather than introducing a new entity programming model. This compatibility also allows for the use of other vendor-specific implementations.

Or in other words:
If you defined your domain model as JPA/Hibernate entities, you can use them with Jakarta Data.

Repositories in Jakarta Data

Repositories are a popular pattern for persistence layers and the key concept in Jakarta Data. You might already know the pattern and different implementations from Spring Data, Quarkus Panache, or Apache DeltaSpike.

Goals of the repository pattern

Repositories abstract from the underlying persistence framework and what you must do to persist, read, or modify your data. This enables you to concentrate on your business logic and even change the persistence framework and data store without modifying your business logic.

If you design a repository based on your business domain, they also help you structure your code and make it easier to understand and maintain.

Stateless repositories

All Jakarta Data repositories are stateless. They don’t keep track of the entity objects you’ve fetched from your data store and don’t support implicit lazy fetching of associated entities. This gives you easier control over all executed operations but also puts you in charge of triggering the required operations at the right time.

That’s especially important if you decide to use Jakarta Data together with Jakarta Persistence. Usually, JPA’s persistence context keeps the state of all managed entity objects, detects changes automatically, guarantees that only 1 entity object represents a record within the current session, and supports implicit lazy fetching of associated entities.

But that’s no longer the case when using Jakarta Data’s repositories. The repositories are stateless and deactivate all these features.

Hibernate ORM’s Jakarta Data implementation is based on a StatelessSession. Please make sure you’re familiar with it before using Jakarta Data.

Define a repository

Using Jakarta Data, you define a repository by annotating an interface with @Repository. Next, you can add or inherit methods to read and write your data. Your Jakarta Data implementation provides you with implementations of these repositories with all their methods.

Extend a standard repository

The easiest but least flexible approach to adding functionality to your repository is to extend Jakarta Data’s BasicRepository or CrudRepository interfaces. Both interfaces define a set of methods to persist new and update entities, delete existing ones and find them by their identifier.

Here, you can see the basic definition of a custom ChessTournamentRepository that extends Jakarta Data’s CrudRepository.

@Repository
public interface ChessTournamentRepository extends CrudRepository<ChessTournament, Long> { }

Add custom lifecycle methods

You don’t have to extend Jakarta Data’s standard repositories if you don’t want to. You can also define a simple interface, annotate it with @Repository, and add your own methods to write your data.

Jakarta Data provides you with the necessary implementation if you annotate your interface method with one of the following annotations:

  • @Insert – Persist a new entity. When using Hibernate ORM, this calls StatelessSession.insert().
  • @Update – Update an existing entity. When using Hibernate ORM, this calls StatelessSession.update().
  • @Delete – Delete an existing entity. When using Hibernate ORM, this calls StatelessSession.delete().
  • @Save – Persist a new or update an existing entity. When using Hibernate ORM, this calls StatelessSession.upsert().

Here, you can see an example of a simple ChessPlayerRepository that defines methods to persist a new ChessPlayer entity and update and delete an existing one.

@Repository
public interface ChessPlayerRepository {
    
    @Insert
    void persist(ChessPlayer player);

    @Update
    void update(ChessPlayer player);

    @Delete
    void delete(ChessPlayer player);

}

Finders

You can define simple finder methods by annotating a method that returns one or more entities with @Find.

Jakarta Data then generates a query with an equal predicate for every method parameter. It expects that for each method parameter, there is an entity attribute with the same name and type. If you want to use a different method parameter name, you can annotate it with a @By annotation and specify the entity attribute’s name.

Let’s take a look at a practical example. Here, we have a basic finder method that returns all ChessPlayer entities with the provided first and last name.

@Repository
public interface ChessPlayerRepository {

    @Find
    ChessPlayer findByName(String firstName, String lastName);

}

When you call the findByName method in your business code and use Hibernate ORM as your Jakarta Data implementation, it executes the following SQL statement.

11:49:49,518 DEBUG [org.hibernate.SQL] - 
    select
        cp1_0.id,
        cp1_0.firstName,
        cp1_0.lastName,
        cp1_0.version 
    from
        ChessPlayer cp1_0 
    where
        cp1_0.firstName=? 
        and cp1_0.lastName=?

Queries

Jakarta Data’s @Query annotation allows you to define your own query using the Jakarta Data Query Language (JDQL). JDQL’s feature set is a subset of the Jakarta Persistence Query Language (JPQL), and different Jakarta Data implementations can provide additional query annotations to support vendor- or datastore-specific queries.

If you’re using Hibernate ORM as your Jakarta Data implementation, you can also use Hibernate’s @HQL and @SQL annotations to define your queries using Hibernate’s JPQL extension or native SQL statements.

The supported return types also depend on your Jakarta Data implementation. All implementations are encouraged to support a single entity class T, Optional<T>, List<T>, Page<T>, and T[] as return types. When using Hibernate ORM as your Jakarta Data implementation, you can use all return types that are supported for JPQL queries.

Here is an example of a simple query method that returns all ChessPlayers with a matching lastName.

@Repository
public interface ChessPlayerRepository {

    @Query("where lastName like :lastName")
    List<ChessPlayer> findByLastNameLike(String lastName);

}

As mentioned earlier, Jakarta Data repositories are stateless, and Jakarta Data doesn’t support implicit lazy fetching. If you want to use a lazily fetched association in your business code, you must fetch it when reading the entity object. Using a JOIN FETCH clause in your query is the easiest way to do that.

@Repository
public interface ChessPlayerRepository {

    @Query("FROM ChessPlayer p JOIN FETCH tournaments t WHERE p.lastName = :lastName")
    ChessPlayer findByLastNameWithTournament(String lastName);

}

Working with Repositories

The Jakarta Data specification integrates with Jakarta’s CDI specification for dependency injection, the Jakarta Transaction specification for transaction handling, and others. That makes the repositories easy to use. You can inject a repository implementation into your business code, and it will follow your transaction declaration.

Within your code, you can use your entity objects almost as you know from JPA or Spring Data JPA. The only things you need to be aware of are:

  • Jakarta Data’s repositories are stateless. Modified entities are not automatically updated in your data store. So, you have to trigger the update programmatically whenever you want to persist your changes.
ChessPlayer player = new ChessPlayer();
player.setFirstName("Tohrben");
player.setLastName("Janssen");
playerRepo.persist(player);

player.setFirstName("Thorben");
playerRepo.update(player);
  • Jakarta Data doesn’t support implicit lazy fetching. When fetching an entity, you have to initialize all required associations.
  • If you fetch the same entity object multiple times, your Jakarta Data implementation returns different objects.
ChessPlayer player = new ChessPlayer();
player.setFirstName("Thorben");
player.setLastName("Janssen");

playerRepo.persist(player);

ChessPlayer fetchedPlayer = playerRepo.findByName("Thorben", "Janssen");
assertThat(player.getId()).isEqualTo(fetchedPlayer.getId());

// You can fetch the same entity object multiple times!!!
assertThat(player).isNotEqualTo(fetchedPlayer);

Conclusion

Jarkata Data is a new specification introduced as part of Jakarta EE 11. It standardizes a repository abstraction on top of Jakarta Persistence and Jakarta NoSQL.

As you saw in this article, you define a repository as an interface. Your Jakarta Data implementation then provides the required implementation. If you’re using Hibernate ORM as your Jakarta Data provider, the Metamodel Generator generates the repository implementations at compile time.