Entity Views with Blaze Persistence – The better DTO projections?


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.


Blaze Persistence’s Entity Views try to solve some of the most common complaints about DTO projections in JPA and Hibernate. Most developers know that DTOs improve the performance of their read operations. But they often avoid using them because it requires boilerplate code and often creates code that’s not easily maintainable. JPA’s DTO support also doesn’t provide a comfortable way to map lists of associated objects. Blaze Persistence’s Entity Views improve on all this by supporting interface-based DTO definitions and flexible, annotation-based mappings.

To get the most out of this article, you need at least a basic understanding of Blaze Persistence’s Criteria API. If you’re new to Blaze Persistence, you should read my introduction to Blaze Persistence’s Criteria API before you continue with this article.

Maven dependencies

Before using Blaze Persistence’s Entity Views, you need to add the required dependencies.

Entity Views are an addon to Blaze Persistence Core. So, you first need to add the core dependencies and the integration with your JPA implementation. After that, you add the dependencies to Blaze Persistence Entity View.

<!-- Blaze Persistence Core -->
<dependency>
	<groupId>com.blazebit</groupId>
	<artifactId>blaze-persistence-core-api-jakarta</artifactId>
	<version>${version.blaze}</version>
	<scope>compile</scope>
</dependency>
<dependency>
	<groupId>com.blazebit</groupId>
	<artifactId>blaze-persistence-core-impl-jakarta</artifactId>
	<version>${version.blaze}</version>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>com.blazebit</groupId>
	<artifactId>blaze-persistence-integration-hibernate-6.0</artifactId>
	<version>${version.blaze}</version>
	<scope>runtime</scope>
</dependency>

<!-- Blaze Persistence Entity Views -->
<dependency>
	<groupId>com.blazebit</groupId>
	<artifactId>blaze-persistence-entity-view-api-jakarta</artifactId>
	<version>${version.blaze}</version>
	<scope>compile</scope>
</dependency>
<dependency>
	<groupId>com.blazebit</groupId>
	<artifactId>blaze-persistence-entity-view-impl-jakarta</artifactId>
	<version>${version.blaze}</version>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>com.blazebit</groupId>
	<artifactId>blaze-persistence-entity-view-processor</artifactId>
	<version>${version.blaze}</version>
	<scope>provided</scope>
</dependency>

After you add these dependencies to your project, you can define your first entity views.

Defining and using a basic Entity View

Similar to a DTO object that you can use with plain JPA, an entity view is a wrapper for a set of attributes. In contrast to an entity projection, you can define use case-specific entity view projections that only includes the required information. This can provide huge performance benefits compared to a managed entity projection.

The easiest way to define an entity view is to create an interface or abstract class and annotate it with @EntityView. Each entity view is based on an entity class, and you need to set a reference to that entity class as the parameter of the @EntityView annotation. Based on that entity reference and the method’s name, Blaze Persistence creates a mapping between each getter method of the entity view projection and the corresponding entity attribute.

You can define multiple entity view projections for each entity class. That enables you to define use case specific entity views that only model the required attributes.

Let’s take a look at a basic example that only contains the id and name of a player. The ChessPlayerView interface defines an entity view based on the ChessPlayer entity class. Its getter methods get mapped to entity attributes with matching names. E.g., the getFirstName method of the ChessPlayerView interface gets mapped to the firstName attribute of the ChessPlayer entity class.

@EntityView(ChessPlayer.class)
public interface ChessPlayerView {
    
    @IdMapping
    public Long getId();

    public String getFirstName();

    public String getLastName();
}

An entity view can also include attributes from associated entities or the result of a database function. The following sections will show you how to define such a mapping. But for now, let’s keep it simple and concentrate on the ChessPlayerView entity view.

The previous code snippet shows an @IdMapping annotation. It’s an optional annotation that defines which attribute identifies the view object. This enables you to use that entity view to be part of a collection mapping and to map collections themselves.

After you have defined your entity view projection, you need to register your entity views before you can use them with Blaze Persistence’s Criteria API. You do that by instantiating an EntityViewConfiguration object and adding your entity view definitions.

EntityViewConfiguration cfg = EntityViews.createDefaultConfiguration();
cfg.addEntityView(ChessPlayerView.class);
// add all entity view projections
EntityViewManager evm = cfg.createEntityViewManager(cbf);

After that’s done, you create an EntityViewManager and use it to apply your entity view projection to Blaze Persistence’s criteria query. If you’re not already familiar with that API, you should check out Blaze Persistence Criteria API guide.

The query in the following code snippet returns the chess players “Fabiano Caruana” and “Magnus Carlsen” as a ChessPlayerView projection. And as you can see in the code, the entity view projection and the query are independent of each other. I first create a CriteriaBuilder for a query that returns ChessPlayer entities and apply the ChessPlayerView entity view to it before executing the query.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

CriteriaBuilder<ChessPlayer> cb = cbf.create(em, ChessPlayer.class)
							         .whereOr()
									      .whereAnd().where("firstName").eq("Fabiano")
										      .where("lastName").eq("Caruana")
									      .endAnd()
									      .whereAnd().where("firstName").eq("Magnus")
										      .where("lastName").eq("Carlsen")
									      .endAnd()
							         .endOr();
CriteriaBuilder<ChessPlayerView> playerViewBuilder = evm.applySetting(EntityViewSetting.create(ChessPlayerView.class), cb);
List<ChessPlayerView> playerViews = playerViewBuilder.getResultList();

playerViews.forEach(p -> log.info(p.getFirstName() + " " + p.getLastName()));

em.getTransaction().commit();
em.close();

When you execute your query, Blaze Persistence adjusts the SELECT clause based on your projection. So, in this example, it generates a query that only selects the 3 attributes mapped by the ChessPlayerView class. These are each player’s id, first name, and last name. This gives you the same performance benefits as JPA’s DTO projection but based on a more comfortable mapping definition.

12:34:35,966 DEBUG [org.hibernate.SQL] - 
    select
        c1_0.id,
        c1_0.firstName,
        c1_0.lastName 
    from
        ChessPlayer c1_0 
    where
        (
            c1_0.firstName=? 
            and c1_0.lastName=?
        ) 
        or (
            c1_0.firstName=? 
            and c1_0.lastName=?
        )
12:34:35,966 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [VARCHAR] - [Fabiano]
12:34:35,967 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [2] as [VARCHAR] - [Caruana]
12:34:35,967 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [3] as [VARCHAR] - [Magnus]
12:34:35,967 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [4] as [VARCHAR] - [Carlsen]
12:34:35,971 INFO  [com.thorben.janssen.TestBlazeEntityView] - Magnus Carlsen
12:34:35,971 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana

The entity view definition used in this example was very basic. It mapped each getter method to an entity attribute with a matching name. That’s good enough for many use cases, but Blaze Persistence’s entity view mapping is much more flexible.

Aggregating data in an EntityView

You can add a @Mapping annotation to the getter method to provide a JPQL snippet that Blaze Persistence will use for the mapping. This provides you with great flexibility. E.g., you can use it to reference an attribute with a different name or reference an attribute of an associated entity class.

Let’s use it in an example. The ChessGame entity class models 2 associations to the ChessPlayer entity. The playerWhite association represents the player who played the white pieces, and the playerBlack association references the player with the black pieces.

@Entity
public class ChessGame {

	@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

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

    ...
}

If you want to show a list of games in your UI, you most likely don’t need the entire ChessGame and the 2 associated ChessPlayer objects. The id of the game and the names of both players should be all the information you need to show in such a list.

Using the @Mapping annotation, you can easily define an entity view projection that only contains that information and doesn’t require your persistence layer to fetch any entity objects.

@EntityView(ChessGame.class)
public interface SimpleChessGameView {
    
    @IdMapping
    public Long getId();

    @Mapping("playerWhite.firstName || ' ' || playerWhite.lastName")
    public String getPlayerWhite();

    @Mapping("playerBlack.firstName || ' ' || playerBlack.lastName")
    public String getPlayerBlack();
}

As you can see in the code snippet, I annotated the getPlayerWhite and getPlayerBlack methods with @Mapping annotations and provided 2 JPQL snippets. Each JPQL snippet traverses the to-one associations to a ChessPlayer entity, references their firstName and lastName attributes, and concatenates them to a String.

This only shows a small subset of what you can do with a @Mapping annotation. If your getter method returns a collection type, you could even traverse a to-many association to another entity class and reference one of its attributes. As a rule of thumb, if your mapping doesn’t match the default behavior, you should first try solving it with a @Mapping annotation.

After you have defined that entity projection, you can use it in the same way as in the previous example. You need a CriteriaBuilder that returns ChessGame entities, use it to define your query, and combine it with the SimpleChessGameView entity projection.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

CriteriaBuilder<ChessGame> cb = cbf.create(em, ChessGame.class);
CriteriaBuilder<SimpleChessGameView> gameViewBuilder = evm.applySetting(EntityViewSetting.create(SimpleChessGameView.class), cb);
List<SimpleChessGameView> gameViews = gameViewBuilder.getResultList();

gameViews.forEach(g -> log.info(g.getPlayerWhite() + " - " + g.getPlayerBlack()));

em.getTransaction().commit();
em.close();

When you execute that query, you can see in the log output that Blaze Persistence generated a query that uses the 2 associations to join the ChessGame and ChessPlayer tables. It selects the id of the ChessGame and concatenates the firstName and lastName of both players.

12:34:17,440 DEBUG [org.hibernate.SQL] - 
    select
        c1_0.id,
        p1_0.firstName||' '||p1_0.lastName,
        p2_0.firstName||' '||p2_0.lastName 
    from
        ChessGame c1_0 
    left join
        ChessPlayer p1_0 
            on p1_0.id=c1_0.playerBlack_id 
    left join
        ChessPlayer p2_0 
            on p2_0.id=c1_0.playerWhite_id
12:34:17,447 INFO  [com.thorben.janssen.TestBlazeEntityView] - Jorden van Foreest - Magnus Carlsen
12:34:17,447 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Magnus Carlsen
12:34:17,447 INFO  [com.thorben.janssen.TestBlazeEntityView] - Magnus Carlsen - Anish Giri
12:34:17,447 INFO  [com.thorben.janssen.TestBlazeEntityView] - Jorden van Foreest - Anish Giri
12:34:17,447 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Jorden van Foreest
12:34:17,447 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Anish Giri

Mapping complex types

One of the most common complaints about JPA’s DTO mapping is that it only supports flat data structures. You can’t easily include a singular attribute of the type of another DTO or entity class. And the mapping of collection types is not supported at all.

Blaze Persistence’s entity view mapping provides a huge improvement on that. You can use entities and entity views as attribute types, and you can even map to-many associations to a collection of entities or entity views. When doing that, please be aware that including managed entities in your entity view mappings can cause LazyInitializationExceptions, if any of its attributes are fetched lazily.

Let’s take a closer look at an example of a to-one and a to-many association to another entity view projection.

Mapping to-one associations to an entity view projection

In the previous example, we mapped each player’s first and last name to 1 attribute of the entity view. You could also map each player to the ChessPlayerView projection that I showed you in the first example. You do that by creating a ChessGameView entity view projection with the getter methods ChessPlayerView getPlayerWhite() and ChessPlayerView getPlayerBlack(). The ChessGame entity class defines the attributes playerWhite and playerBlack, which map many-to-one associations to the ChessPlayer entity. Blaze Persistence will map the getter methods to those attributes and apply the entity view mapping to each of them.

@EntityView(ChessGame.class)
public interface ChessGameView {
    
    @IdMapping
    public Long getId();

    public ChessPlayerView getPlayerWhite();

    public ChessPlayerView getPlayerBlack();
}

When you then create a query and assign the ChessGameView entity view projection to it, Blaze Persistence will generate a query that only fetches the required information from the database. As you can see in the following log output, that also includes the required JOIN clauses to connect the record in the ChessGame table with the 2 corresponding records in the ChessPlayer table.

12:33:44,669 DEBUG [org.hibernate.SQL] - 
    select
        c1_0.id,
        c1_0.playerBlack_id,
        p1_0.firstName,
        p1_0.lastName,
        c1_0.playerWhite_id,
        p2_0.firstName,
        p2_0.lastName 
    from
        ChessGame c1_0 
    left join
        ChessPlayer p1_0 
            on p1_0.id=c1_0.playerBlack_id 
    left join
        ChessPlayer p2_0 
            on p2_0.id=c1_0.playerWhite_id
12:33:44,674 INFO  [com.thorben.janssen.TestBlazeEntityView] - Jorden van Foreest - Magnus Carlsen
12:33:44,674 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Magnus Carlsen
12:33:44,674 INFO  [com.thorben.janssen.TestBlazeEntityView] - Magnus Carlsen - Anish Giri
12:33:44,674 INFO  [com.thorben.janssen.TestBlazeEntityView] - Jorden van Foreest - Anish Giri
12:33:44,674 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Jorden van Foreest
12:33:44,675 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Anish Giri

Mapping to-many associations to entity view projections

You can map to-many associations in almost the same way. You only need to change the return type of your getter methods to a collection type.

So, an entity view projection for a player with all games they played might look like this:

@EntityView(ChessPlayer.class)
public interface ChessPlayerWithGamesView {
    
    @IdMapping
    Long getId();

    @Mapping("firstName || ' ' || lastName")
    String getName();

    List<SimpleChessGameView> getGamesWhite();

    List<SimpleChessGameView> getGamesBlack();
}

As you can see, mapping the gamesWhite and gamesBlack associations to a List of SimpleChessGameView entity view projections doesn’t require any annotations. Based on the method’s names, Blaze Persistence finds the corresponding entity attributes and maps each object to a SimpleChessGameView object. And if your projection doesn’t follow the default mapping conventions, you can add a @Mapping annotation to customize it.

Let’s execute the following test case to give this mapping a try.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

CriteriaBuilder<ChessPlayer> cb = cbf.create(em, ChessPlayer.class);
CriteriaBuilder<ChessPlayerWithGamesView> playerViewBuilder = evm.applySetting(EntityViewSetting.create(ChessPlayerWithGamesView.class), cb);
List<ChessPlayerWithGamesView> playerViews = playerViewBuilder.getResultList();

playerViews.forEach(p ->  {
							log.info(p.getName());
							p.getGamesWhite().forEach(g -> log.info(g.getPlayerWhite() + " - " + g.getPlayerBlack()));
							p.getGamesBlack().forEach(g -> log.info(g.getPlayerWhite() + " - " + g.getPlayerBlack()));
						  }
					);

em.getTransaction().commit();
em.close();

As you can see in the following log output, Blaze Persistence doesn’t fetch any entities. It instead generates a query that only fetches the information required by the entity view projection. Even though that query might require several LEFT JOIN clauses, fetching only the required information with 1 statement is usually much faster than executing multiple queries to fetch a graph of entities with all their attributes.

12:42:25,412 DEBUG [org.hibernate.SQL] - 
    select
        c1_0.id,
        g1_0.id,
        p1_0.firstName||' '||p1_0.lastName,
        p2_0.firstName||' '||p2_0.lastName,
        g2_0.id,
        p3_0.firstName||' '||p3_0.lastName,
        p4_0.firstName||' '||p4_0.lastName,
        c1_0.firstName||' '||c1_0.lastName 
    from
        ChessPlayer c1_0 
    left join
        ChessGame g1_0 
            on c1_0.id=g1_0.playerBlack_id 
    left join
        ChessPlayer p1_0 
            on p1_0.id=g1_0.playerBlack_id 
    left join
        ChessPlayer p2_0 
            on p2_0.id=g1_0.playerWhite_id 
    left join
        ChessGame g2_0 
            on c1_0.id=g2_0.playerWhite_id 
    left join
        ChessPlayer p3_0 
            on p3_0.id=g2_0.playerBlack_id 
    left join
        ChessPlayer p4_0 
            on p4_0.id=g2_0.playerWhite_id
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Magnus Carlsen
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Magnus Carlsen - Anish Giri
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Jorden van Foreest - Magnus Carlsen
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Magnus Carlsen
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Anish Giri
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Magnus Carlsen - Anish Giri
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Jorden van Foreest - Anish Giri
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Anish Giri
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Jorden van Foreest
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Jorden van Foreest - Anish Giri
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Jorden van Foreest - Magnus Carlsen
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Jorden van Foreest
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Anish Giri
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Jorden van Foreest
12:42:25,431 INFO  [com.thorben.janssen.TestBlazeEntityView] - Fabiano Caruana - Magnus Carlsen

Summary

When working with JPA implementations like Hibernate, DTO projections provide much better performance for read-only operations than entities. That’s because they avoid the management overhead of entity objects, and you can design them to only include the information required by your use case.

Blaze Persistence’s entity view projection improves that in 2 ways:

  1. It offers an annotation-based mapping definition for your DTO classes. Yes, that’s right. As you saw in this article, an entity view is a DTO, not an entity. So, you avoid any management overhead, but you also can’t use it to implement write operations.
  2. An entity view projection doesn’t have to be a flat data structure. It can map to-one and to-many associations to one or more other entity view projections.

Based on these 2 improvements, Blaze Persistence’s entity view projections provide the same benefits as JPA’s DTO projections but are much more flexible and easier to use.