Panache – Active Record 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.
The main idea of the active record pattern is to let the entity object encapsulate the data and the database operations you can perform on it. That makes it an excellent fit for the persistence layer of a Java application. Panache, a Quarkus extension based on Hibernate, provides great support for this pattern and makes it easy to apply it to your JPA entity classes.
Martin Fowler describes the pattern as follows:
An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.
Active Record definition by Martin Fowler
You probably already recognized the difference between the active record pattern and the usual approach to design and work with your entity classes.
When working with plain JPA, Hibernate, or Spring Data JPA, we usually use the repository pattern or the DAO pattern to implement our persistence layer. Both of them separate the representation of the database record from the database operations. Frameworks like Spring Data JPA and Apache Delta Spike support this by providing standardized repositories at runtime.
Using the active record pattern avoids this separation and focuses on the entity class and its objects. They represent records in the database, and their methods also encapsulate the database operations. That follows the main ideas of the object-oriented programming paradigm. And as I will show you in this article, Panache handles most of the work so that you can concentrate on your business logic.
Defining Your Entity Mapping
When using the active record pattern, your entity classes need to provide methods for all supported database operations. This, of course, includes standard operations, like find by ID, persist a new record, and update or remove an existing one. But it also includes all custom queries that return one or more objects of this class. Panache helps you with all of this. It provides ready-to-use implementations of all standard operations and also helps you create custom queries.
Using the Defaults by Extending PanacheEntity
The easiest way to get all this is to extend the PanacheEntity class and define a public attribute for each database column you want to map. You don’t need to implement any getter or setter methods, and you don’t need to provide an identifier. Panache handles all of that for you. But you might need to add a few mapping annotations to define associations or activate optimistic locking. You can apply these annotations directly to the public entity attributes.
Here you can see an example of an entity class that maps the records in the ChessPlayer table and supports standard database operations on that table.
@Entity public class ChessPlayer extends PanacheEntity { public String firstName; public String lastName; public LocalDate birthDate; @Version public int version; ... }
The PanacheEntity class provides multiple versions of findById, find, findAll, list, listAll, stream, streamAll, and count methods which you can use to read data from the database. We will take a closer look at some of these methods in a few paragraphs.
And the PanacheEntity class also provides multiple persist, update and delete methods. Please keep in mind that changes on managed entity objects are detected automatically and that you don’t need to call any method to trigger a database update.
Here you can see an example of a simple test case that creates a new ChessPlayer object, sets its attributes, and calls its persist method to store a new record in the database.
@Test @Transactional public void testPersist() { log.info("==== Test Persist - Hibernate ORM with Panache - Active Record Pattern ===="); ChessPlayer chessPlayer = new ChessPlayer(); chessPlayer.firstName = "Thorben"; chessPlayer.lastName = "Janssen"; chessPlayer.persist(); assertThat(chessPlayer.isPersistent(), is(true)); }
When you execute this test case, you can see in the log output that Panache called the persist method on the EntityManager. Hibernate then used the database sequence hibernate_sequence to get a new primary key value and executed an SQL INSERT statement.
Aug. 05, 2021 4:39:40 PM com.thorben.janssen.sample.ChessPlayerResourceTest testPersist INFO: ==== Test Persist - Hibernate ORM with Panache - Active Record Pattern ==== Hibernate: select nextval ('hibernate_sequence') Hibernate: insert into ChessPlayer (birthDate, firstName, lastName, version, id) values (?, ?, ?, ?, ?)
Accessing Entity Fields
I mentioned earlier that you don’t need to implement getter or setter methods for your entity attributes. It internally rewrites read operations, e.g., chessPlayer.firstName, to calls of the corresponding getter method and write operations to calls of the corresponding setter method. This ensures proper encapsulation and enables you to provide your own getter and setter methods if needed.
I use that in the following example to implement a setLastName method that converts the provided lastName to upper case and prints out a message.
@Entity public class ChessPlayer extends PanacheEntity { public String firstName; public String lastName; public LocalDate birthDate; @Version public int version; public void setLastName(String lastName) { System.out.println("Change last name to upper case."); this.lastName = lastName.toUpperCase(); } }
When I rerun the testPersist test case we used in the previous example, you can see that message in the log output.
Aug. 05, 2021 5:06:33 PM com.thorben.janssen.sample.ChessPlayerResourceTest testPersist INFO: ==== Test Persist - Hibernate ORM with Panache - Active Record Pattern ==== Change last name to upper case. Hibernate: select nextval ('hibernate_sequence') Hibernate: insert into ChessPlayer (birthDate, firstName, lastName, version, id) values (?, ?, ?, ?, ?)
Provide a Custom Primary Key Mapping by Extending PanacheEntityBase
As you saw in the previous chapter, the PanacheEntity class defines an id attribute and uses the database-specific default strategy to generate unique primary key values. If you want to adjust that, e.g., using a different database sequence, your entity class needs to extend the PanacheEntityBase class. This class doesn’t define an identifying attribute. That’s the only difference to the previously used PanacheEntity. When you extend the PanacheEntityBase class, you need to include an identifier in your entity mappings, and you can choose a strategy and provide your own generator.
@Entity public class ChessGame extends PanacheEntityBase { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "game_seq") @SequenceGenerator(name = "game_seq", sequenceName = "game_sequence", initialValue = 100) public Long id; public LocalDate date; public int round; @ManyToOne(fetch = FetchType.LAZY) public ChessPlayer playerWhite; @ManyToOne(fetch = FetchType.LAZY) public ChessPlayer playerBlack; @Version public int version;
Define Custom Queries
The PanacheEntity and PanacheEntityBase classes provide a set of methods to get an entity by its primary key, get all entities, and get one or more entities that fulfill specific criteria. You can use them to define your own queries comfortably. Let’s take a closer look at the last group of methods before we end this article.
You can call the find, list, and stream method with a query String and one or more bind parameter values. Panache will include your query String in the generated statement and set the provided bind parameter values. You can use this to define custom queries that return the data required by your use case.
These methods are public. You could, of course, use them directly in your business code. But I recommend adding static methods to your entity class to separate your database operations from your business code.
Within these methods, you can call the find, list, and stream method provided by the PanacheEntityBase class. The interesting part of that method call is the provided query String. You can either provide a JPQL or HQL statement or a simplified HQL String. The simplified HQL String is only a small part of a query statement, and Panache generates the remaining parts.
Simplified HQL – ORDER BY clauses
You can call these methods with an ORDER BY clause as simplified HQL, e.g., “order by date DESC”.
@Entity public class ChessGame extends PanacheEntityBase { ... public static List<ChessGame> getLatestGames() { return list("order by date DESC"); } }
Panache 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
Simplified HQL – 1 entity attribute
You can reference a single entity attribute by its name and provide 1 bind parameter value, e.g., “date” and LocalDate.now().
@Entity public class ChessGame extends PanacheEntityBase { ... public static List<ChessGame> getTodaysGames() { return list("date", LocalDate.now()); } }
Panache extends this to “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=?
Simplified HQL – WHERE clause
You can provide an entire WHERE clause as the query String and the required bind parameters, e.g., “date = ?1 and round = ?2”.
@Entity public class ChessGame extends PanacheEntityBase { ... public static List<ChessGame> getGamesByDateAndRound(LocalDate date, int round) { return list("date = ?1 and round = ?2", date, round); } }
Or, if you prefer using named bind parameters, as I do, you can provide a Map with bind parameter values.
@Entity public class ChessGame extends PanacheEntityBase { ... public static 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); } }
Panache extends these simplified statements to “from ChessGame WHERE date = ?1 and round = ?2” or “from ChessGame WHERE date = :date and round = :round” and sets the bind parameter values. Because SQL only supports positional bind parameters, Hibernate 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
When implementing the active record pattern, an entity class maps a database table and encapsulates the operations that you can perform on that table. This is an interesting alternative to the more popular repository and DAO patterns. The active record pattern aligns much better with the general ideas and concepts of object-oriented programming. I hope developers will use it more often in the future.
As I showed you in this article, Panache helps you implement this pattern. It provides methods to find all records, find one record by its primary key, and persist, update, or remove a database record. In addition to that, Panache also generates getter and setter methods for your entity attributes, and you can use simplified HQL statements to define custom queries.
There is text duplication on this page: "Using the active record pattern avoids this separation and focuses on the entity class and its objects. They represent records in the database, and their methods also encapsulate the database Using the active record pattern avoids this separation and focuses on the entity class and its objects. They represent records in the database, and their methods also encapsulate the database operations."
Fixed it. Thanks!