As a Java backend developer, you could know well about the Repository
pattern and the Repository
related facilities that already provided in the popular frameworks, such as Spring Data, Quarkus ORM Panache, Micronaut Data etc. But every framework has it own advantages and limitations.
Jakarta Data is a new Jakarta EE specification which tries to create an universal interfaces to access relational database and none-relational database.
As planned, Jakarta Data 1.0 will be included in the upcoming Jakarta EE 11.
Currently the popular Jakarta Persistence providers including Hibernate and Eclipse Link have implemented this specification in the early stage (because Jakarta Data 1.0 is not released yet).
In this post, we will use the latest Hibernate and try to integrating Jakarta Data into a Spring application.
Generate a simple web Spring project via Spring Intializr.
- Language: Java 21
- Dependencies: Web, ORM, Lombok, Postgres
- Build: Maven
Or just create a simple Maven Java project.
Check out the sample codes that I used to demonstrate Jakarta Data in the post.
Add the following dependencies into your project.
// ...
<hibernate.version>6.6.0.Alpha1</hibernate.version>
<dependencies>
// ...
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>jakarta.data</groupId>
<artifactId>jakarta.data-api</artifactId>
<version>1.0.0-RC1</version>
</dependency>
</dependencies>
Declare a Hibernate StatelessSession
as bean.
@Configuration
public class DataConfig {
@Bean
public StatelessSession statelessSession(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
return entityManagerFactoryBean.getObject().unwrap(SessionFactory.class).openStatelessSession();
}
}
Unlike the Spring Data JPA which depends on the general JPA stateful persistence, Hibernate implements Jakarta Data using the StatelessSession
, which means there is no first-class cache here, every change applied on the Database will be flushed immediately.
More about the Jakarta Data support in Hibernate, read the new branded Hibernate Data Repositories.
Create a simple @Entity
class for test purpose.
@Entity()
@Table(name = "posts")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Post implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
UUID id;
@Column(name = "title")
private String title;
@Column(name = "content")
private String content;
@Enumerated(EnumType.STRING)
@Builder.Default
private Status status = Status.DRAFT;
@Column(name = "created_at")
@CreationTimestamp
private LocalDateTime createdAt;
}
In the above code fragment, the @Data
, @Builder
, @NoArgsConstructor
and @AllArgsConstructor
annotations are from the Lombok project which modify the compiled Post.class
and generate getters/setters and equals
/hashCode
, an inner builder class and a static build method, two constructors in the target Post.class
at compile time.
The @CreationTimestamp
is from Hibernate, which will set the current timestamp when inserting an entity instance. Other annotations are from Jakarta Persistence, it is simple and easy to understand.
Jakarta Data also provides a series of Repository
interfaces: DataRepository
, BasicRepository
, @CrudRepository
, @PageableRepository
, etc. If you have some experience of Spring Data JPA, you should know well the Repository
interface inheritance in Spring Data umbrella projects.
Create a Repository
interface for the Post
entity class we just created. Here let it extend from CrudRepository
which has a collection of built-in methods similar to the popular Spring Data CurdRepository
.
@jakarta.data.repository.Repository
public interface PostRepository extends CrudRepository<Post, UUID> {
}
Here annotate PostRepository
with annotation @Repository
to indicate it is a Jakarta Data Repository interface.
The
@Repository
annotation here is from packagejakarta.data.repository
. Do not use the one provided in Spring.
Jakarta Data specification requires the implementors to process the Repository
at compile time. Hibernate annotation processor (from hibernate-jpamodelgen
maven module) will scan the @Repository
interface and generate the implementation class for the interface.
To make Lombok and other compiler annotation processors work seamlessly, we configure them in order under the configuration/annotationProcessorPaths
node of Maven compiler plugin.
<build>
<finalName>demo</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</annotationProcessorPath>
<annotationProcessorPath>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>
// ...
Next, open a terminal window, switch to the project root, and run the following command to compile the whole project.
mvn clean compile
After the compilation is completed, explore the generated codes in the target/generated-sources/annotations
folder under the project root folder.
If this folder is not recognized by your IDE, add it manually as a Source Set.
Besides the generated meta models for the JPA entity classes, there is a new PostRepository_.java
in the com.example.demo.repository
package.
Open it in your editor view, it should look like:
@Generated("org.hibernate.processor.HibernateProcessor")
public class PostRepository_ implements PostRepository {
/**
* Find {@link Post} by {@link Post#id id}.
*
* @see com.example.demo.repository.PostRepository#deleteById(UUID)
**/
@Override
public void deleteById(@Nonnull UUID id) {
if (id == null) throw new IllegalArgumentException("Null id");
var _builder = session.getFactory().getCriteriaBuilder();
var _query = _builder.createCriteriaDelete(Post.class);
var _entity = _query.from(Post.class);
_query.where(
_builder.equal(_entity.get(Post_.id), id)
);
try {
session.createMutationQuery(_query)
.executeUpdate();
}
catch (NoResultException exception) {
throw new EmptyResultException(exception.getMessage(), exception);
}
catch (NonUniqueResultException exception) {
throw new jakarta.data.exceptions.NonUniqueResultException(exception.getMessage(), exception);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
protected @Nonnull StatelessSession session;
@Inject
public PostRepository_(@Nonnull StatelessSession session) {
this.session = session;
}
public @Nonnull StatelessSession session() {
return session;
}
@Override
public void delete(@Nonnull Post entity) {
if (entity == null) throw new IllegalArgumentException("Null entity");
try {
session.delete(entity);
}
catch (StaleStateException exception) {
throw new OptimisticLockingFailureException(exception.getMessage(), exception);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
@Override
public void deleteAll(@Nonnull List<? extends Post> entities) {
if (entities == null) throw new IllegalArgumentException("Null entities");
try {
for (var _entity : entities) {
session.delete(_entity);
}
}
catch (StaleStateException exception) {
throw new OptimisticLockingFailureException(exception.getMessage(), exception);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
/**
* Find {@link Post}.
*
* @see com.example.demo.repository.PostRepository#findAll(PageRequest,Order)
**/
@Override
public Page<Post> findAll(PageRequest pageRequest, Order<Post> sortBy) {
var _builder = session.getFactory().getCriteriaBuilder();
var _query = _builder.createQuery(Post.class);
var _entity = _query.from(Post.class);
_query.where(
);
var _orders = new ArrayList<org.hibernate.query.Order<? super Post>>();
for (var _sort : sortBy.sorts()) {
_orders.add(by(Post.class, _sort.property(),
_sort.isAscending() ? ASCENDING : DESCENDING,
_sort.ignoreCase()));
}
try {
long _totalResults =
pageRequest.requestTotal()
? session.createSelectionQuery(_query)
.getResultCount()
: -1;
var _results = session.createSelectionQuery(_query)
.setFirstResult((int) (pageRequest.page()-1) * pageRequest.size())
.setMaxResults(pageRequest.size())
.setOrder(_orders)
.getResultList();
return new PageRecord(pageRequest, _results, _totalResults);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
@Override
public Post update(@Nonnull Post entity) {
if (entity == null) throw new IllegalArgumentException("Null entity");
try {
session.update(entity);
return entity;
}
catch (StaleStateException exception) {
throw new OptimisticLockingFailureException(exception.getMessage(), exception);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
@Override
public Post save(@Nonnull Post entity) {
if (entity == null) throw new IllegalArgumentException("Null entity");
try {
session.upsert(entity);
return entity;
}
catch (StaleStateException exception) {
throw new OptimisticLockingFailureException(exception.getMessage(), exception);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
/**
* Find {@link Post}.
*
* @see com.example.demo.repository.PostRepository#findAll()
**/
@Override
public Stream<Post> findAll() {
var _builder = session.getFactory().getCriteriaBuilder();
var _query = _builder.createQuery(Post.class);
var _entity = _query.from(Post.class);
_query.where(
);
try {
return session.createSelectionQuery(_query)
.getResultStream();
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
@Override
public List updateAll(@Nonnull List entities) {
if (entities == null) throw new IllegalArgumentException("Null entities");
try {
for (var _entity : entities) {
session.update(_entity);
}
return entities;
}
catch (StaleStateException exception) {
throw new OptimisticLockingFailureException(exception.getMessage(), exception);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
@Override
public List saveAll(@Nonnull List entities) {
if (entities == null) throw new IllegalArgumentException("Null entities");
try {
for (var _entity : entities) {
session.upsert(_entity);
}
return entities;
}
catch (StaleStateException exception) {
throw new OptimisticLockingFailureException(exception.getMessage(), exception);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
@Override
public List insertAll(@Nonnull List entities) {
if (entities == null) throw new IllegalArgumentException("Null entities");
try {
for (var _entity : entities) {
session.insert(_entity);
}
return entities;
}
catch (ConstraintViolationException exception) {
throw new EntityExistsException(exception.getMessage(), exception);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
/**
* Find {@link Post} by {@link Post#id id}.
*
* @see com.example.demo.repository.PostRepository#findById(UUID)
**/
@Override
public Optional<Post> findById(@Nonnull UUID id) {
if (id == null) throw new IllegalArgumentException("Null id");
try {
return ofNullable(session.get(Post.class, id));
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
@Override
public Post insert(@Nonnull Post entity) {
if (entity == null) throw new IllegalArgumentException("Null entity");
try {
session.insert(entity);
return entity;
}
catch (ConstraintViolationException exception) {
throw new EntityExistsException(exception.getMessage(), exception);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
}
As you see, there is a constructor injection which depends on a Hibernate StatelessSession
bean.
public class PostRepository_ implements PostRepository {
// ...
@Inject
public PostRepository_(@Nonnull StatelessSession session) {
this.session = session;
}
In the Jakarta EE/CDI environment, the Jakarta Data @Repository
can be recognized as CDI beans directly. In a Spring application, we have to declare it as a Spring @Bean
in the configuration.
@Configuration
public class DataConfig {
// ...
@Bean
public PostRepository postRepository(StatelessSession statelessSession) {
return new PostRepository_(statelessSession);
}
// ...
}
Now you can inject a PostRepository
in other beans freely.
@Autowired
PostRepository posts;
var data = List.of(
Post.builder().title("test").content("content").status(Status.PENDING_MODERATION).build(),
Post.builder().title("test1").content("content1").build()
);
data.forEach(this.posts::insert);
var results = posts.findAll();
assertThat(results.toList().size()).isEqualTo(2);
Spring Data JPA allows you create custom derived queries through a method naming convention. For example, to query all posts into a List
by a provided status parameter, you can simply add a method like the following to the Repository interface.
public interface PostRepository<Post, UUID> extends JpaRepository {
List<Post> findByStatus(Status status);
}
In the Jakarta Data world, it provides a collection of annotations(@Query
, @Save
, @Insert
, @Update
, @Delete
, @Find
, @GroupBy
, @OrderBy
, etc.) to archive this customization purpose.
@jakarta.data.repository.Repository
public interface PostRepository extends CrudRepository<Post, UUID> {
@Find
@OrderBy("createdAt")
List<Post> byStatus(Status status);
}
After the project is compiled, it will generate the implementation like this.
/**
* Find {@link Post} by {@link Post#status status}.
*
* @see com.example.demo.repository.PostRepository#byStatus(Status)
**/
@Override
public List<Post> byStatus(Status status) {
var _builder = session.getFactory().getCriteriaBuilder();
var _query = _builder.createQuery(Post.class);
var _entity = _query.from(Post.class);
_query.where(
status==null
? _entity.get(Post_.status).isNull()
: _builder.equal(_entity.get(Post_.status), status)
);
var _orders = new ArrayList<org.hibernate.query.Order<? super Post>>();
_orders.add(by(Post.class, "createdAt", ASCENDING, false));
try {
return session.createSelectionQuery(_query)
.setOrder(_orders)
.getResultList();
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
The following is an example of using it.
var resultsByKeyword = posts.byStatus(Status.PENDING_MODERATION);
assertThat(resultsByKeyword.size()).isEqualTo(1);
More freely, Jakarta Data allows you use these annotations to build your repository without extending Repository interfaces.
Create a simple interface Blogger
, annotate it with @Repository
.
@Repository
public interface Blogger {
@Query("""
SELECT p.id, p.title FROM Post p
WHERE p.status = 'PUBLISHED'
ORDER BY p.createdAt DESC
""")
List<PostSummary> allPublishedPosts();
@Insert
Post newPost(Post post);
}
Compile the project again, it will generate a Blogger_
implementation class.
@Generated("org.hibernate.processor.HibernateProcessor")
public class Blogger_ implements Blogger {
static final String ALL_PUBLISHED_POSTS = "SELECT p.id, p.title FROM Post p\nWHERE p.status = 'PUBLISHED'\nORDER BY p.createdAt DESC\n";
/**
* Execute the query {@value #ALL_PUBLISHED_POSTS}.
*
* @see com.example.demo.Blogger#allPublishedPosts()
**/
@Override
public List<PostSummary> allPublishedPosts() {
try {
return session.createSelectionQuery(ALL_PUBLISHED_POSTS, PostSummary.class)
.getResultList();
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
protected @Nonnull StatelessSession session;
@Inject
public Blogger_(@Nonnull StatelessSession session) {
this.session = session;
}
public @Nonnull StatelessSession session() {
return session;
}
@Override
public Post newPost(@Nonnull Post post) {
if (post == null) throw new IllegalArgumentException("Null post");
try {
session.insert(post);
return post;
}
catch (ConstraintViolationException exception) {
throw new EntityExistsException(exception.getMessage(), exception);
}
catch (PersistenceException exception) {
throw new DataException(exception.getMessage(), exception);
}
}
}
Declare it as a Spring @Bean
in the configuration as well.
// in DataConfig.java
@Bean
public Blogger blogger(StatelessSession statelessSession) {
return new Blogger_(statelessSession);
}
The following insert and query example is using this Blogger
instead.
var data = Post.builder().title("test").content("test content").status(Status.DRAFT).build();
var saved = blogger.newPost(data);
assertThat(this.posts.findById(saved.getId()).isPresent()).isTrue();
saved.setStatus(Status.PUBLISHED);
posts.update(saved);
List<PostSummary> allPublished = blogger.allPublishedPosts();
assertThat(allPublished.size()).isEqualTo(1);
To experience more Jakarta Data features yourself, please check the complete PostRepositoryTest
.
When I prepared the sample codes, I tried to add @BeforeEach
hook method as the following to clean up the sample data for every tests.
@SneakyThrows
@BeforeEach
public void setup() {
var deleted = posts.deleteAll();
log.debug("deleted posts: {}", deleted);
}
And add a deleteAll
method as below into PostRepository
interface.
@Delete
@Transactional
long deleteAll();
When compiling the project and running the tests, it will throw a Jakarta Data TransactionException
.
I have tried to configure HibernateTransactionManager
and JpaTransactionManager
respectively, neither resolves the issue. Spring still does not provide transaction support for Hibernate StatelessSession
, see issue spring-framework#7184. A possible solution is adding Spring Transaction support for Hibernate StatelessSession from scratch, see the useful example codes from this gist.
I consulted this question in Zulip Hibernate users channel, the Hibernate expert provided an extremely simple solution to overcome this barrier temporarily, just need to add a property hibernate.allow_update_outside_transaction=true
to Hibernate configuration.
Check out the complete sample project from my Github and taste the new Jakarta Data specification yourself.