Skip to content

Entity Views

Projections types supported by Spring Data JPA cover basic and most common use cases, but are not helpful when we need to deal with one-to-many relations. For that we need to either switch to jOOQ or use Blaze Persistence Entity Views. This section explains the latter.

Blaze Persistence

Blaze Persistence is a 3rd party library maintained by a Hibernate team member - Christian Beikov. Keep in mind that this is not an official library provided by Red Hat - so it comes with certain risks as using any 3rd party dependency not backed by a multi billion dollar company.

Blaze Persistence's main purpose is providing rich Criteria API, but it also comes with the Entity View module.

You can roughly imagine an entity view is to an entity, what a RDBMS view is to a table.

Entity Views Setup

Dependencies

To use Entity Views in a Spring Boot project, you must add a bunch of dependencies.

groovy
ext {
    blazePersistenceVersion = '1.6.10'
}

// ..

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation "com.blazebit:blaze-persistence-core-impl-jakarta:$blazePersistenceVersion"
    implementation "com.blazebit:blaze-persistence-core-api-jakarta:$blazePersistenceVersion"
    implementation "com.blazebit:blaze-persistence-entity-view-api-jakarta:$blazePersistenceVersion"
    implementation "com.blazebit:blaze-persistence-entity-view-impl-jakarta:$blazePersistenceVersion"
    implementation "com.blazebit:blaze-persistence-integration-entity-view-spring-6.0:$blazePersistenceVersion"
    implementation "com.blazebit:blaze-persistence-integration-hibernate-6.2:$blazePersistenceVersion"

    // other dependencies
}

At the time of writing this guide, there is no dedicated support for Spring Framework 6.1 and Spring Data JPA 3.1. While blaze-persistence-integration-entity-view-spring-6.0 works well with Spring Framework 6.1, the module for Spring Data blaze-persistence-integration-spring-data-2.7 does not. This should not stop you from using Entity Views, as these integration modules are needed only for convenience. You can use this library in Spring Boot application without any dedicated Spring integration module in place, as the integration is realtively simple.

Spring Configuration

java
@Configuration
@EnableEntityViews
public class BlazeConfiguration {

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;

    @Bean
    public CriteriaBuilderFactory createCriteriaBuilderFactory() {
        CriteriaBuilderConfiguration config = Criteria.getDefault();
        return config.createCriteriaBuilderFactory(entityManagerFactory);
    }

    @Bean
    public EntityViewManager createEntityViewManager(CriteriaBuilderFactory cbf, EntityViewConfiguration entityViewConfiguration) {
        // here you can configure entity views
        // for example: entityViewConfiguration.setProperty(ConfigurationProperties.MANAGED_TYPE_VALIDATION_DISABLED, "true");
        return entityViewConfiguration.createEntityViewManager(cbf);
    }
}

If you decide to not use Spring integration, you must register entity views manually. This means that @EnableEntityViews has to be removed and EntityViewConfiguration bean has to be created manually:

java
@Configuration
public class BlazeConfiguration {

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;

    @Bean
    EntityViewConfiguration entityViewConfiguration() {
        EntityViewConfiguration cfg = EntityViews.createDefaultConfiguration();
        cfg.addEntityView(MovieView.class);
        return cfg;
    }

    @Bean
    public CriteriaBuilderFactory createCriteriaBuilderFactory() {
        CriteriaBuilderConfiguration config = Criteria.getDefault();
        return config.createCriteriaBuilderFactory(entityManagerFactory);
    }

    @Bean
    public EntityViewManager createEntityViewManager(CriteriaBuilderFactory cbf, EntityViewConfiguration entityViewConfiguration) {
        // here you can configure entity views
        // for example: entityViewConfiguration.setProperty(ConfigurationProperties.MANAGED_TYPE_VALIDATION_DISABLED, "true");
        return entityViewConfiguration.createEntityViewManager(cbf);
    }
}

Define @EntityView

Similar to JPA's @Entity, views must be defined with @EntityView annotation. View can be either an interface or a Java record. At this stage we will define a simple view that is an equivalent of projections we've already discussed:

java
@EntityView(Movie.class)
public interface MovieView {
    @IdMapping
    String getId();
    String getTitle();
}

Custom Repository Method

Since we are not using Entity Views' Spring Data JPA integration, we must define repository method ourselves. We are going to do it using Custom Repository Implementations.

Note that in following steps, naming matters and you must stick to conventions described in reference docs linked above.

Lets define MovieRepositoryCustom interface:

java
public interface MovieRepositoryCustom {
    List<MovieView> findViewsByIdIn(List<String> ids);
}

Next goes implementation MovieRepositoryCustomImpl:

java
public class MovieRepositoryCustomImpl implements MovieRepositoryCustom {
    private final CriteriaBuilderFactory cbf;
    private final EntityManager entityManager;
    private final EntityViewManager evm;

    public MovieRepositoryCustomImpl(CriteriaBuilderFactory criteriaBuilderFactory, 
                                     EntityManager entityManager, 
                                    EntityViewManager entityViewManager) {
        this.cbf = criteriaBuilderFactory;
        this.entityManager = entityManager;
        this.evm = entityViewManager;
    }

    @Override
    public List<MovieView> findViewsByIdIn(List<String> ids) {
        var cb = cbf.create(entityManager, Movie.class)
                .where("id")
                .in(ids);
        return evm.applySetting(EntityViewSetting.create(MovieView.class), cb)
                .getResultList();
    }
}

And make MovieRepository extend from MovieRepositoryCustom, making new method available on MovieRepository interface:

java
public interface MovieRepository extends Repository<Movie, String>, MovieRepositoryCustom {
    // ..
}

Dynamic Projections

Similar to dynamic projections in Spring Data, we can turn the custom repository to support dynamic projections, and it is actually very simple:

java
@Override
public <T> List<T> findViewsByIdIn(List<String> ids, Class<T> clazz) {
    var cb = cbf.create(entityManager, clazz)
            .where("id")
            .in(ids);
    return evm.applySetting(EntityViewSetting.create(clazz), cb)
            .getResultList();
}

Since @EntityView annotated classes or interfaces must be registered on application context startup, unfortunately there is no option to use local records with Entity Views.

Mapping One-To-Many relations

All we've seen so far with Entity Views is already supported natively by Spring Data JPA and as long as you need just basic projections - most likely you should stick to what Spring Data JPA offers.

Entity Views shine with complex projections on many to many, one to many, or even nested relations.

Setting the stage

Lets extend the Movie example a bit and add movie roles and actors.

java
package com.example.projections;

@Entity
public class Movie {
    @Id
    private String id;
    private String title;
    private int year;
    private String description;
    
    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "movie")
    private List<Role> cast = new ArrayList<>();

    // ..
}

@Entity
public class Role {
    @Id
    private String id;
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "movie_id")
    private Movie movie;
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "actor_id")
    private Actor actor;
    
    // ..
}

@Entity
public class Actor {
    @Id
    private String id;
    private String name;

    // ..
}

For our use case, we need to fetch all movies with fields id, title and fields id and name for each actor that play in each movie.

We need a view for the Actor entity, containing only fields we are interested in:

java
@EntityView(Actor.class)
public interface ActorView {
    @IdMapping
    String getId();
    String getName();
}

Next we need to update MovieView to include list of ActorViews.

java
@EntityView(Movie.class)
public interface MovieView {
    @IdMapping
    String getId();
    String getTitle();

    @Mapping("cast.actor")
    List<ActorView> getActors();
}

Lets now call findViewsByIdIn on the repository:

sql
SELECT m1_0.id,
       c1_0.actor_id,
       a1_0.name,
       m1_0.title
FROM movie m1_0
LEFT JOIN ROLE c1_0 ON m1_0.id=c1_0.movie_id
LEFT JOIN actor a1_0 ON a1_0.id=c1_0.actor_id
WHERE m1_0.id in (?,?)

Entity Views were fetched in a single query and mapped to one to many relation automatically! That's already very handy and for many use cases it can be enough. But it comes with a drawback: select .. join on one-to-many relations produce cartesian product of two results. Effectively, we end up with duplicated data in resultset and lots of redundant data sent over the network between the database and application:

┌──────────────────────────────────────┬──────────────────────────────────────┬───────────────────┬────────────────────┐
│                                   id │                             actor_id │              name │              title │
├──────────────────────────────────────┼──────────────────────────────────────┼───────────────────┼────────────────────┤
│ aef282f3-ce23-486e-8a0b-013c6aaf4af7 │ 77cfa704-279b-48fc-adec-136a0d622312 │      Keanu Reeves │             Matrix │
│ aef282f3-ce23-486e-8a0b-013c6aaf4af7 │ 4fa70b1a-b049-410e-b6ed-7c15e5b76a80 │  Carrie-Anne Moss │             Matrix │
│ 58103eb6-232e-41e6-897c-ff9477a4d9b6 │ c5b1e598-2aa5-4109-9776-b039e3063269 │    Michael J. Fox │ Back to the Future │
│ 58103eb6-232e-41e6-897c-ff9477a4d9b6 │ 18cf5bde-4801-4f10-b771-deda0ecc03a2 │ Christopher Lloyd │ Back to the Future │
└──────────────────────────────────────┴──────────────────────────────────────┴───────────────────┴────────────────────┘

Fortunately, Entity Views can handle relations other ways too. Lets get back to MovieView#actors mapping:

java
@Mapping("cast.actor")
List<ActorView> getActors();

@Mapping annotation takes optional fetch parameter where we can chose the fetch strategy.

FetchStrategy.JOIN

FetchStrategy.JOIN - the default one, associations are fetched together with main entity with single query with SQL's JOIN - this is likely good option for @ManyToOne and @OneToOne relations, but for @OneToMany and @ManyToMany creates cartesian product

FetchStrategy.SELECT

sql
select m1_0.id,m1_0.title from movie m1_0 where m1_0.id in (?,?)

and then for each entry:

sql
select r1_0.actor_id,a1_0.name from role r1_0 join actor a1_0 on a1_0.id=r1_0.actor_id where r1_0.movie_id=?

FetchStrategy.SUBSELECT

Executes 2 queries, no matter how many movies are returned in the first one.

First to find movies:

sql
select m1_0.id,m1_0.title from movie m1_0 where m1_0.id in (?,?)

Second to find all actors:

sql
select r1_0.actor_id,a1_0.name,null,m1_0.id from movie m1_0 join role r1_0 on (r1_0.movie_id=m1_0.id) join actor a1_0 on a1_0.id=r1_0.actor_id where m1_0.id in (?,?)

FetchStrategy.MULTISET

MULTISET is definitely the most interesting and the most unusual approach. With a subquery, it aggregates results from actor table into a single value using json_agg:

sql
SELECT m1_0.id,
  (SELECT json_agg(json_build_object('f0', '' || c1_0.actor_id, 'f1', '' || a1_0.name))
   FROM ROLE c1_0
   JOIN actor a1_0 ON a1_0.id=c1_0.actor_id
   WHERE m1_0.id=c1_0.movie_id),m1_0.title
FROM movie m1_0
WHERE m1_0.id in (?,?)
┌──────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────────────┐
│                                   id │                                                                                                                                                json_agg │              title │
├──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────┤
│ e20089e3-5a15-4ddf-b483-429444e85ba3 │    [{"f0" : "6705efca-dc82-49ca-b2c1-2e5fcd5b264e", "f1" : "Keanu Reeves"}, {"f0" : "8c744422-aabf-498e-8e71-a2ff31a5b5fa", "f1" : "Carrie-Anne Moss"}] │             Matrix │
│ 147ce355-213f-4526-a29d-bedec691e783 │ [{"f0" : "dbb09a3b-5011-4db8-ae41-1d3236ddd8d9", "f1" : "Michael J. Fox"}, {"f0" : "8427b152-4a4a-43fe-aad9-29d1872312b0", "f1" : "Christopher Lloyd"}] │ Back to the Future │
└──────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────┘

Fetch Strategies - Summary

For one-to-many and many-to-many relationships, FetchStrategy.SUBSELECT and FetchStrategy.MULTISET are two reasonable choices in terms of saving the network bandwidth as they do not produce cartesian product.

Which one is faster? Difficult to say as it depends on many factors. Ideally, test it with your production workload.

Let's stay in touch and follow me on Twitter: @maciejwalkowiak

Subscribe to RSS feed