5 min read

Self-referencing entities with Hibernate, JPA, JPQL and Quarkus

A common problem one often faces in relational database design is how to design your entities to allow self-references. Depending on your use-case, this might be something like followers / following that is common among Social Media Platforms.

Example of followers / follows as self-referencing entity data model

Getting Started

We are using JPA (Java Persistence API) with Hibernate, a ORM mapper and the most known JPA persistence implementation to demonstrate how to model your entities correctly to manage self-referencing Many-To-Many relationships.

In addition I like to use Lombok, as it auto-generates methods like Setters and Getters at build-time based on your fields by adding a single annotation, so your code won´t get bloated with boilerplate methods and stays clean.

User Model - Entity containing our user data

As I´ve mentioned, I´m using Quarkus for this example. Quarkus is a new Java framework and is using Eclipse Microprofile, the reference implementation of the Jakarta EE specs and provides interesting concepts such as the Panache ORM features. Letting your entities extend PanacheEntity for example will give you the ID column automatically plus some methods for basic database operations on your entity.

Example:

User.count();
User.persist(...);

Without the need of repositories. I like it for certain smaller operations such as persist, remove, count etc, but I tend to define larger queries in a separate repository, as I don´t like to bloat my entities with too much logic and rather have them as pure data containers.

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Getter
@Setter
@Entity
@Table(name = "user_")
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class User extends PanacheEntity {


    @Column(name = "name")
    private String name;


    @EqualsAndHashCode.Include
    public Long getId() {
        return id;
    }

    @ManyToMany(cascade = CascadeType.ALL, mappedBy = "following")
    private Set<User> followers = new HashSet<>();

    @JoinTable(name = "followers",
            joinColumns = {@JoinColumn(name = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "follower_id")})
    @ManyToMany(cascade = CascadeType.ALL)
    private Set<User> following = new HashSet<>();

    public void addFollower(User toFollow) {
        following.add(toFollow);
        toFollow.getFollowers().add(this);
    }

    public void removeFollower(User toFollow) {
        following.remove(toFollow);
        toFollow.getFollowers().remove(this);
    }

}

As I´m using Postgres, I need to name my database table user_ as user is a reserved keyword. I somewhat can´t come up with better names than user most of the time, so this is my solution in that regard.

UserService - Service that defines operations on our entities

To separate our business logic from our entity and to allow more specific actions such as following by ID we are implementing a new service (which is what we might need if we are receiving the following action by a REST controller with the ID of the other user we want to follow)

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.transaction.Transactional;
import java.math.BigInteger;

@ApplicationScoped
public class UserService {

    @Inject
    UserRepository userRepository;

    @Transactional
    public void follow(Long userId, Long toFollowId) {
        User user = userRepository.findById(userId);
        User toFollow = userRepository.findById(toFollowId);
        user.addFollower(toFollow);
    }

    @Transactional
    public void unfollow(Long userId, Long toUnfollowId) {
        User user = userRepository.findById(userId);
        User toFollow = userRepository.findById(toUnfollowId);
        user.removeFollower(toFollow);
    }

}

Self-Joining with JPA - How to query referenced entities

In order to get all followers or followings of one user, we need to access the references of one user entity, joined by either the follower_id or the following_id dependent on what we want.

First of all we are defining our UserRepository that allows us to perform queries on the entity. You can also use the Quarkus Panache approach, but we are doing it the classic way with a repository.

This repository is empty, but don´t worry, the functions we need are already defined in the PanacheRepository interface.

@ApplicationScoped
public class UserRepository implements PanacheRepository<User> {

}

Afterwards we can enhance our service with getFollowers and getFollowings methods.

@ApplicationScoped
public class UserService {

    @Inject
    UserRepository userRepository;

    @Transactional
    public void follow(Long userId, Long toFollowId) {
        User user = userRepository.findById(userId);
        User toFollow = userRepository.findById(toFollowId);
        user.addFollower(toFollow);
    }

    @Transactional
    public void unfollow(Long userId, Long toUnfollowId) {
        User user = userRepository.findById(userId);
        User toFollow = userRepository.findById(toUnfollowId);
        user.removeFollower(toFollow);
    }

    @Transactional
    public Set<User> getFollowers(Long userId) {
        return userRepository.findById(userId).getFollowers();
    }

    @Transactional
    public Set<User> getFollowing(Long userId) {
        return userRepository.findById(userId).getFollowing();
    }

}

We are using the findById method to fetch our parent user and access the collection inside within a Hibernate Session, which will be handled automatically in the method's context by annotating it with @Transactional.

This call will make Hibernate fetching the items of the collection which weren't available previously as the collections are declared Lazy.

Using the EntityManager

Using the EntityManager is a bit more "low-level", as you can create SQL / JPQL queries and execute them. You can do some projections if you are only interested in certain fields of your entities.

In this example we are only interested in the UUID of the user. One reason for this could be gathering data to be exposed to the frontend and we do not want to give the user data he should not see, such as the internal primary key or the email address etc.

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import java.util.List;
import java.util.UUID;

@ApplicationScoped
public class TestService {

    @Inject
    EntityManager entityManager;
    
    @Inject
    UserRepository userRepository;

    @Transactional
    public void follow(Long userId, Long toFollowId) {
        User user = userRepository.findById(userId);
        User toFollow = userRepository.findById(toFollowId);
        user.addFollower(toFollow);
    }

    @Transactional
    public void unfollow(Long userId, Long toUnfollowId) {
        User user = userRepository.findById(userId);
        User toFollow = userRepository.findById(toUnfollowId);
        user.removeFollower(toFollow);
    }

    @Transactional
    public Set<User> getFollowers(Long userId) {
        return userRepository.findById(userId).getFollowers();
    }

    @Transactional
    public Set<User> getFollowing(Long userId) {
        return userRepository.findById(userId).getFollowing();
    }

    @Transactional
    public List<UUID> getFollowingUUIDs(Long userId) {
        return entityManager.createQuery("select f.uuid from User user left join user.following f where user.id=:id", UUID.class)
                .setParameter("id", userId)
                .getResultList();
    }

    @Transactional
    public List<UUID> getFollowerUUIDs(Long userId) {
        return entityManager.createQuery("select f from User user left join user.followers f where user.id=:id", UUID.class)
                .setParameter("id", userId)
                .getResultList();
    }


}

As the user is referencing itself, we are joining the User table with the user.followers collection. More about JPQL joins here

Summary

You have seen how to define self-referencing entities in Hibernate using Quarkus and querying the references with the JPA and the EntityManager / JPQL way.