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.
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
@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:
@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:
@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:
public interface MovieRepositoryCustom {
List<MovieView> findViewsByIdIn(List<String> ids);
}
Next goes implementation MovieRepositoryCustomImpl
:
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:
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:
@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.
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:
@EntityView(Actor.class)
public interface ActorView {
@IdMapping
String getId();
String getName();
}
Next we need to update MovieView
to include list of ActorViews
.
@EntityView(Movie.class)
public interface MovieView {
@IdMapping
String getId();
String getTitle();
@Mapping("cast.actor")
List<ActorView> getActors();
}
Lets now call findViewsByIdIn
on the repository:
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:
@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
select m1_0.id,m1_0.title from movie m1_0 where m1_0.id in (?,?)
and then for each entry:
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:
select m1_0.id,m1_0.title from movie m1_0 where m1_0.id in (?,?)
Second to find all actors:
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
:
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.