JPA Projection
JPA projection is a projection that gets constructed as a result of executing JPQL query (as an opposite to native SQL query).
Projection can be defined either as Java record,a regular class - the class must have a constructor that matches all the fields fetched from the database, or an interface.
Record projection
record MovieSummary(String id, String title) {}
Class projection
class MovieSummary {
private final String id;
private final String title;
public MovieSummary(String id, String title) {
this.id = id;
this.title = title;
}
// getters
}
Interface projection
Projections can be defined also as an interface:
interface MovieSummary {
String getId();
String getTitle();
}
When each getter method matches the name of one of the entity field, such projection is called closed projection - which means that Spring Data JPA fetches only columns from the database that are included in the projection.
Spring Data JPA offers also more complex mappings with interface based projections - including mapping one-to-many relation into a projection - you can read about it in Spring Data JPA Reference - but beware - they turn a projection into an open projection and effectively cause fetching all columns from the database - they are not helpful when it comes to optimizing performance so I won't get into more details about it.
Using JPA projections
JPA projections are constructed inside the JPQL query in a @Query
annotations:
interface MovieRepository extends Repository<Movie, String> {
@Query("select new com.example.projections.MovieSummary(id, title) from Movie where id = :id")
MovieSummary findById(String id);
}
The great benefit of JPA projections is that we have a full control over the JPQL query and are free to add any conditions to where clause as needed.
ClassImportIntegrator
If using full package name in query (select new com.example.projections.MovieSummary(..)
) is too verbose for you, you may want to use Vlad Mihalcea's ClassImportIntegrator
from Hypersistence Utils library.
public class ClassImportIntegratorIntegratorProvider implements IntegratorProvider {
@Override
public List<Integrator> getIntegrators() {
return List.of(
new ClassImportIntegrator(
// list all DTOs for which you don't want to use full pacakge in JPQL query
List.of(
MovieSummary.class
)
)
);
}
}
Set the property in application.properties
:
spring.jpa.properties.hibernate.integrator_provider=com.example.projections.ClassImportIntegratorIntegratorProvider
Or alternatively define a bean of type org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer
:
@Configuration
public class HibernateConfiguration {
@Bean
HibernatePropertiesCustomizer hibernatePropertiesCustomizer() {
return props -> props.put("integrator_provider", ClassImportIntegratorIntegratorProvider.class.getName());
}
}
You can get more creative and scan classpath for DTOs to register them automatically, but I won't cover it in this guide.
With this in place, our query changes to:
interface MovieRepository extends Repository<Movie, String> {
@Query("select new MovieSummary(id, title) from Movie where id = :id")
MovieSummary findById(String id);
}
Projections vs Derived Queries
While having full control over JPQL query is very powerful, writing full query for simple use cases may feel redundant - especially if you're used to using Spring Data derived queries.
Spring Data JPA supports fetching projections from derived queries with both classes and records:
interface MovieRepository extends Repository<Movie, String> {
MovieSummary findById(String id);
MovieSummary findByTitle(String title);
}
The only requirement is that projection class/record field names must match JPA entity field names.
Derived queries work with all types of projections: interface, record and class.
Dynamic Projections
Once you start using projections you'll quickly find out that different use cases need different data and you may end up with quite many different projections corresponding to a single entity. For example:
record MovieBasicData(String title, int year) {}
record MovieWithDescription(String id, String title, String description) {}
Adding find
methods for each projection means that we will likely end up with a repository with so many methods that it becomes unreadable or at least hard to maintain. This is where dynamic projections can help.
We can define a generic method on a repository, where method name defines a query (derived query), but the return type is set in runtime:
interface MovieRepository {
<T> T findMovieById(String id, Class<T> clazz);
}
The drawback of dynamic projections is that you cannot use it with custom JPQL or native SQL query - only queries derived from the method name are supported.
Not having to define explicit method on repository interface for each projection helps us to isolate projections for specific use case, keeping them private in another package. For example, the repository may be defined in com.imdb.domain
package and the MovieSummary
as package private projection in com.imdb.usecase.homepage
- in such setup, no other part of the system is able to use MovieSummary
projection.
Dynamic Projections make it possible to fetch data into local records. For example:
void someMethod(String id) {
// Local record class
record MovieSummary(String id, String title) {}
MovieSummary movies = movieRepository.findMovieById(id, MovieSummary.class);
// do something with the result
}
OneToMany & ManyToMany projection
So far we've covered relatively simple projections - limiting columns from a single table. What if we have more complex scenario and want to fetch projections from one-to-many or one-to-many relation? Short answer - with JPA projections - you can't.