Panache – Repository Pattern


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.


Panache is a Quarkus-specific library that handles most of the boilerplate code usually required by JPA-based persistence layers. One of the features it provides is ready-to-use and easily customizable repositories for your entity classes.

Repositories are a very popular pattern for Java-based persistence layers. They encapsulate the database operations you can perform on entity objects and aggregates. That helps to separate the business logic from the database operations and improves the reusability of your code. Martin Fowler defines the pattern as follows:

Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.

Repository definition by Martin Fowler

Similar to Spring Data JPA’s support for this pattern, Panache’s repository classes provide all the basic functionality, and you only need to add your custom queries. In this article, I will show you how to create repositories with Panache, how you can use them to execute standard operations, like persisting a new entity object, and how to define and execute your own queries. To get the most out of this article, you should have a general understanding of Panache. You also might want to read my tutorial on Panache’s support of the active record pattern before or after finishing this article.

Define Your Entity Mappings

Before you can create your repositories, you need to define your entity classes. Panache uses Hibernate, which implements the JPA specification. It doesn’t introduce any additional requirements and can work with all entity mappings supported by Hibernate. Due to that, you can easily migrate an existing persistence layer to Panache and use all of your previous JPA and Hibernate experience.

The following code snippet shows a typical example of a JPA entity class. Hibernate maps the class to the ChessGame table and each attribute to a column with the same name. The id attribute is the identifier and Hibernate uses the database sequence games_sequence to generate unique values when persisting a new entity object. The attributes playerWhite and playerBlack model managed many-to-one associations to the ChessPlayer entity.

@Entity
public class ChessGame {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "game_seq")
    @SequenceGenerator(name = "game_seq", sequenceName = "game_sequence")
    private Long id;

    private LocalDate date;

    private int round;

    @ManyToOne(fetch = FetchType.LAZY)
    private ChessPlayer playerWhite;

    @ManyToOne(fetch = FetchType.LAZY)
    private ChessPlayer playerBlack;

    @Version
    private int version;
	
	...
}

Don’t extend PanacheEntity or PanacheBaseEntity

Your entity classes can also extend Panache’s PanacheEntity and PanacheBaseEntity classes. But I don’t recommend it.

These 2 classes apply the active record pattern to your entity class, which I explained in a previous article. It’s an interesting pattern that solves the same problem as the repository pattern. Both separate your database operations from your business logic, and Panache provides ready-to-use implementations of most standard operations.

By extending the PanacheEntity or PanacheBaseEntity class, your entity class inherits one set of these methods. And Panache’s repository provides a 2nd set of these methods. So, by applying both patterns, you end up with 2 sets of methods that do the same.

Having one set of these methods is a great idea and makes implementing your persistence layer much easier. But you don’t want to have 2 of them. It will only make your codebase inconsistent and harder to maintain.

Defining Your Repositories

As mentioned earlier, Panache’s repositories provide you with a set of standard methods to find entities by their primary key. You can also persist, update and remove an entity. The only thing you need to do to get this functionality is to define an entity-specific repository class. It has to implement the PanacheRepository or PanacheRepositoryBase interface.

The only difference between the 2 interfaces is that you can provide the type of your entity and its primary key attribute to the PanacheRepositoryBase interface. The PanacheRepository defaults the primary key to type Long.

@ApplicationScoped
public class ChessGameRepository implements PanacheRepository<ChessGame> {}

Both repository interfaces define multiple versions of persist, update and delete methods and multiple findById, find, findAll, list, listAll, stream, streamAll, and count methods which you can use to read data from the database. You can find a complete list of these methods in the JavaDoc of the PanacheRepositoryBase interface.

Panache provides the required implementations for all standard methods. Due to that, the ChessGameRepository definition in the previous code snippet gets you a fully functional repository, which you can inject and use in your business code.

@QuarkusTest
public class ChessPlayerResourceTest {

    @Inject
    ChessGameRepository chessGameRepository;
	
    @Inject
    ChessPlayerRepository chessPlayerRepository;

    @Test
    @Transactional
    public void testPersistPanacheRepositoryPattern() {
        ChessGame chessGame = new ChessGame();
        chessGame.setRound(1);
        chessGame.setDate(LocalDate.now());

        ChessPlayer chessPlayer1 = chessPlayerRepository.findById(1L);
        ChessPlayer chessPlayer2 = chessPlayerRepository.findById(2L);

        chessGame.setPlayerWhite(chessPlayer1);
        chessGame.setPlayerBlack(chessPlayer2);

        chessGameRepository.persist(chessGame);
	}
}

Adding Custom Queries

In addition to all the standard operations, you can add your own methods to implement custom queries. Panache’s repository interface defines several find, findAll, list, listAll, stream, streamAll, and count methods that you can call with an additional query and ordering criteria. You can provide these criteria as a standard JPQL or HQL statement or using simplified HQL.

You could, of course, use these methods in your business code. But I recommend encapsulating all database operations in your repository definition. This enables you to use methods with a higher abstraction level in your business code. The following code snippets show typical examples of such methods.

In the following paragraphs, I will focus on using the simplified HQL option to define your query. I think it’s the most interesting one, and you’re probably already familiar with standard JPQL. If you read my article on Panache’s support of the active record pattern, most of the following paragraphs will look familiar to you. Panache supports the same feature set for both patterns.

ORDER BY clauses

To adjust the ordering of your query results, you can call the find, findAll, list, listAll, stream, and streamAll methods with an ORDER BY clause, e.g., “order by date DESC”.

@ApplicationScoped
public class ChessGameRepository implements PanacheRepository<ChessGame> {

    public List<ChessGame> getLatestGames() {
        return list("order by date DESC");
    }
}

Panache’s repository implementation extends this to “from ChessGame order by date DESC” and executes this query.

Hibernate: 
    select
        chessgame0_.id as id1_0_,
        chessgame0_.date as date2_0_,
        chessgame0_.playerBlack_id as playerbl5_0_,
        chessgame0_.playerWhite_id as playerwh6_0_,
        chessgame0_.round as round3_0_,
        chessgame0_.version as version4_0_ 
    from
        ChessGame chessgame0_ 
    order by
        chessgame0_.date DESC

WHERE clauses with 1 entity attribute

Let’s create a query with a simple WHERE clause that compares 1 entity attribute with a provided value. You do that by referencing a single entity attribute by its name and providing 1 bind parameter value, e.g., “date” and LocalDate.now().

@ApplicationScoped
public class ChessGameRepository implements PanacheRepository<ChessGame> {

    public List<ChessGame> getTodayGames() {
        return list("date", LocalDate.now());
    }
}

When you call this method, Panache’s repository implementation generates the query “from ChessGame WHERE date = ?” and sets LocalDate.now() as the bind parameter value.

Hibernate: 
    select
        chessgame0_.id as id1_0_,
        chessgame0_.date as date2_0_,
        chessgame0_.playerBlack_id as playerbl5_0_,
        chessgame0_.playerWhite_id as playerwh6_0_,
        chessgame0_.round as round3_0_,
        chessgame0_.version as version4_0_ 
    from
        ChessGame chessgame0_ 
    where
        chessgame0_.date=?

Complex WHERE clauses

If your use case requires a more complex WHERE clause, you can provide it as the query String, together with the necessary bind parameter values. I use that in the following code snippet. The method returns all games played on a specific date and in the defined round of a tournament.

@ApplicationScoped
public class ChessGameRepository implements PanacheRepository<ChessGame> {

    public List<ChessGame> getGamesByDateAndRound(LocalDate date, int round) {
        return list("date = ?1 and round = ?2", date, round);
    }
}

I used positional bind parameters in the previous statement. You can also use named bind parameters and provide a Map with bind parameter values.

@ApplicationScoped
public class ChessGameRepository implements PanacheRepository<ChessGame> {

    public List<ChessGame> getGamesByDateAndRoundUsingMap(LocalDate date, int round) {
        Map<String, Object> params = new HashMap<>();
        params.put("date", date);
        params.put("round", round);

        return list("date = :date and round = :round", params);
    }
}

For these simplified statements, Panache generates the queries “from ChessGame WHERE date = ?1 and round = ?2” and “from ChessGame WHERE date = :date and round = :round” and sets the bind parameter values. SQL doesn’t support named bind parameters. Hibernate, therefore, generates and executes the same SQL statement for both HQL statements.

Hibernate: 
    select
        chessgame0_.id as id1_0_,
        chessgame0_.date as date2_0_,
        chessgame0_.playerBlack_id as playerbl5_0_,
        chessgame0_.playerWhite_id as playerwh6_0_,
        chessgame0_.round as round3_0_,
        chessgame0_.version as version4_0_ 
    from
        ChessGame chessgame0_ 
    where
        chessgame0_.date=? 
        and chessgame0_.round=?

Conclusion

The repository pattern is a very popular pattern used to implementing maintainable and reusable persistence layers. Each repository encapsulates the database operations performed on a single or a group of entities.

Panache provides all of the code required to implement a repository with basic read and write operations. You only need to create a class that implements the PanacheRepository interface, and Panache provides this functionality for you. In addition to that, you can implement your own methods, which encapsulate all kinds of queries or more complex database operations.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.