How to integrate Jakarta Data with Spring and Hibernate
Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?
What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?
Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, Micronaut, or Play Framework.
So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!
Introduction
In this article, we are going to see how we can integrate Jakarta Data with Spring and Hibernate.
Jakarta Data is a new Jakarta EE specification that provides a common API for building data Repositories and data access objects.
If you are familiar with Spring Data JPA, you will see that Jakarta Data is very much inspired by this Spring API.
Jakarta Data
Just like JPA, Jakarta Data is only a set of interfaces and requires an actual implementation in order to use it in your project. And, just like Hibernate ORM implements the JPA specification, starting with version 6.6, Hibernate implements the Jakarta Data specification as well.
To use Jakarta Data, you need either a Jakarta container or a stand-alone framework, like Quarkus or Spring. Since I’m more familiar with Spring, in this article, I’m going to show you how you can use Jakarta Data with Spring and Hibernate.
Jakarta Data Repositories
Assuming we have the following Post and PostComment entities:

The Jakarta Data PostRepository will look as follows:
@Repository
public interface PostRepository extends BasicRepository<Post, Long> {
⠀
StatelessSession getSession();
⠀
default Post persist(Post post) {
getSession().insert(post);
return post;
}
⠀
@Query("""
select p
from Post p
left join fetch p.comments
where p.title like :titlePrefix
""")
List<Post> findAllByTitleLike(
@Param("titlePrefix") String titlePrefix
);
⠀
@Query("""
select p
from Post p
left join fetch p.comments
where p.id = :id
""")
Post findByIdWithComments(
@Param("id") Long id
);
}
The jakarta.data.repository.Repository is used to mark that the underlying class is a Jakarta Data Repository.
By extending the jakarta.data.repository.BasicRepository, we can inherit various base methods, such findById, save, or delete.
Hibernate implements the Jakarta Data Repositories using the StatelessSession instead of the stateful Session or the JPA EntityManager.
By defining a getSession method, we can reference the underlying StatelessSession that was used when instantiating the Repository object. With the getSession method in place, we can implement default methods that give us the flexibility of calling any method from StatelessSession.
Just like with Spring Data JPA, we can use the jakarta.data.repository.Query annotation to define the JPQL query that a given data access method will execute.
The PostCommentRepository is similar to the aforementioned PostRepository and looks like this:
@Repository
public interface PostCommentRepository extends BasicRepository<PostComment, Long> {
⠀
StatelessSession getSession();
⠀
default Post persist(Post post) {
getSession().insert(post);
return post;
}
⠀
@Find
List<PostComment> findByReview(String review);
⠀
@Query("""
select pc
from PostComment pc
join fetch pc.post p
join fetch p.comments
where p.title like :titlePrefix
""")
List<PostComment> findAllWithPostTitleLike(
@Param("titlePrefix") String titlePrefix
);
⠀
@Query("""
select pc
from PostComment pc
join fetch pc.post p
where pc.id between :minId and :maxId
""")
List<PostComment> findAllWithPostByIds(
@Param("minId") Long minId,
@Param("maxId") Long maxId
);
⠀
@Query("""
delete from PostComment c
where c.post in :posts
""")
void deleteAllByPost(
@Param("posts") List<Post> posts
);
}
Similar to Spring Data JPA, Jakarta Data allows us to define query methods that derive the JPQL query from the method name.
Integrating Jakarta Data with Spring and Hibernate
As I explained in this article, when using Spring Data JPA, Spring takes care of binding the context-specific EntityManager to the Repository proxy so that all Repositories called from within the same transactional Service can share the same transaction context.

On the other hand, when using Jakarta Data, Spring no longer offers this automatic transaction management since it does not support this technology. Therefore, we will need to provide our own transaction and connection management mechanism so that we can propagate the transactional context from the Service layer to the Jakarta Data Repositories.
To achieve this goal, we are going to provide a StatelessSession Proxy object to the Jakarta Data layer so that each Repository can resolve the associated StatelessSession from the Spring-based TransactionSynchronizationManager context.
@Bean
public StatelessSession statelessSession(
EntityManagerFactory entityManagerFactory) {
return (StatelessSession) Proxy.newProxyInstance(
StatelessSession.class.getClassLoader(),
new Class[]{StatelessSession.class},
new StatelessSessionInvocationHandler(entityManagerFactory)
);
}
⠀
@Bean
public PostRepository postRepository(
StatelessSession statelessSession) {
return ReflectionUtils.newInstance(
"com.vladmihalcea.hpjp.spring.data.jakarta.repository.PostRepository_",
new Object[]{statelessSession},
new Class[]{StatelessSession.class}
);
}
⠀
@Bean
public PostCommentRepository postCommentRepository(
StatelessSession statelessSession) {
return ReflectionUtils.newInstance(
"com.vladmihalcea.hpjp.spring.data.jakarta.repository.PostCommentRepository_",
new Object[]{statelessSession},
new Class[]{StatelessSession.class}
);
}
The StatelessSessionInvocationHandler implements the Java InvocationHandler and delegates the method calls of the StatelessSession Proxy object to a StatelessSession instance that we are creating and storing in the TransactionSynchronizationManager in a similar way in which Spring Data JPA stores the EntityManager Proxy object.
The StatelessSessionInvocationHandler looks as follows:
public class StatelessSessionInvocationHandler
implements InvocationHandler {
⠀
private static final List<String> OBJECT_METHODS = Arrays.stream(
Object.class.getDeclaredMethods()
).map(Method::getName).toList();
⠀
private final StatelessSession DUMMY_OBJECT;
⠀
public StatelessSessionInvocationHandler(
EntityManagerFactory entityManagerFactory) {
SessionFactoryImplementor sessionFactory = entityManagerFactory
.unwrap(SessionFactoryImplementor.class);
DUMMY_OBJECT = sessionFactory
.withStatelessOptions()
.openStatelessSession();
⠀
sessionFactory.addObserver(new SessionFactoryObserver() {
@Override
public void sessionFactoryClosed(
SessionFactory factory) {
DUMMY_OBJECT.close();
}
});
}
⠀
@Override
public Object invoke(
Object proxy,
Method method,
Object[] args){
if (OBJECT_METHODS.contains(method.getName())) {
return ReflectionUtils.invokeMethod(
DUMMY_OBJECT,
method,
args
);
}
EntityManager entityManager = TransactionSynchronizationManager
.getResourceMap()
.values()
.stream()
.filter(EntityManagerHolder.class::isInstance)
.map(eh -> ((EntityManagerHolder) eh).getEntityManager())
.findAny()
.orElse(null);
⠀
Session session = entityManager.unwrap(Session.class);
Connection connection = session.doReturningWork(conn -> conn);
StatelessSession statelessSession = (StatelessSession)
TransactionSynchronizationManager.getResource(
new StatelessSessionKey(entityManager)
);
if (statelessSession == null) {
SessionFactoryImplementor sessionFactory = entityManager
.getEntityManagerFactory()
.unwrap(SessionFactoryImplementor.class);
⠀
final StatelessSession statelessSession_ = sessionFactory
.openStatelessSession(connection);
session.addEventListeners(new BaseSessionEventListener() {
@Override
public void end() {
statelessSession_.close();
TransactionSynchronizationManager.unbindResource(
new StatelessSessionKey(entityManager)
);
}
});
⠀
statelessSession = statelessSession_;
TransactionSynchronizationManager.bindResource(
new StatelessSessionKey(entityManager),
statelessSession
);
}
⠀
return ReflectionUtils.invokeMethod(
statelessSession,
method,
args
);
}
⠀
public static class StatelessSessionKey {
public StatelessSessionKey(EntityManager entityManager) {
this.entityManager = entityManager;
}
⠀
private final EntityManager entityManager;
⠀
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof StatelessSessionKey that))
return false;
return Objects.equals(
entityManager,
that.entityManager
);
}
⠀
@Override
public int hashCode() {
return Objects.hash(entityManager);
}
}
}
On the Service layer, we have the following ForumService that calls the PostRepository and PostCommentRepository from within a Spring transactional context:
@Service
@Transactional(readOnly = true)
public class ForumService {
⠀
@Inject
private PostRepository postRepository;
⠀
@Inject
private PostCommentRepository postCommentRepository;
⠀
public List<PostComment> findCommentsByReview(String review) {
return postCommentRepository.findByReview(review);
}
⠀
@Transactional
public Post addPost(String title) {
Post post = new Post();
post.setTitle(title);
⠀
return postRepository.persist(post);
}
⠀
@Transactional
public PostComment addPostComment(String review, Long postId) {
PostComment comment = new PostComment()
.setReview(review)
.setCreatedOn(LocalDateTime.now())
.setPost(new Post().setId(postId));
⠀
postCommentRepository.save(comment);
return comment;
}
⠀
@Transactional
public void updateComment(PostComment comment) {
postCommentRepository.save(comment);
}
⠀
@Transactional
public void removePostComment(Long id) {
postCommentRepository.deleteById(id);
}
}
Testing Time
When adding a parent Post and a child PostComment entities:
Post post = forumService.addPost(
"High-Performance Java Persistence"
);
PostComment comment = forumService.addPostComment(
"Awesome!",
post.getId()
);
Hibernate will generate the following SQL INSERT statements:
Query:["select nextval('post_SEQ')"]
Query:["insert into post (title,id)
values (?,?)
"],
Params:[(High-Performance Java Persistence, 1)]
Query:["select nextval('post_comment_SEQ')"]
Query:["insert into post_comment (created_on,parent_id,post_id,review,status,votes,id)
values (?,?,?,?,?,?,?)
"],
Params:[(2024-08-19 12:14:56.2930118, -5, 1, Awesome!, -6, 0, 1)]
When updating the PostComment entity:
comment.setReview("Highly recommended");
forumService.updateComment(comment);
Hibernate will generate the following SQL statement:
Query:["
merge into post_comment as t using (
select
cast(? as bigint) id,
cast(? as timestamp(6)) created_on,
cast(? as bigint) parent_id,
cast(? as bigint) post_id,
cast(? as text) review,
cast(? as smallint) status,
cast(? as integer) votes
) as s on (t.id=s.id)
when not matched then
insert (id, created_on, parent_id, post_id, review, status, votes)
values (s.id, s.created_on, s.parent_id, s.post_id, s.review, s.status, s.votes)<br />
when matched then
update set
created_on=s.created_on,
parent_id=s.parent_id,
post_id=s.post_id,
review=s.review,
status=s.status,
votes=s.votes
"],
Params:[(1, 2024-08-19 12:29:42.0198604, -5, 1, Highly recommended, -6, 0)]
Awesome, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
The Jakarta Data is still a young project and doesn’t really match Spring Data JPA.
At the moment, Jakarta Data might be more appealing if you are running a Jakarta EE application on an application server, such as Payara, or if you are using Quarkus. For a Spring application, not only is the integration part more tricky, but the number of features Spring Data JPA provides greatly outpaces what Jakarta Data currently offers.






