There are a few different ways to achieve lightweight projections with Hibernate + Spring and they all work fine for ‘simpler’ cases. It boils down to which flavour one prefer – Class based DTOs, Interfaces or dynamic projections. However, things to watch out is N+1 fetch problem and even Open vs Closed projections when defining the attributes subset. Still, this all works satisfactory almost in all cases when we deal with single entity without children entities.
There is nice support in Spring data for nested projections a.k.a. projections for children relations as well. Like this:
interface PersonSummary { String getFirstname(); String getLastname(); AddressSummary getAddress(); interface AddressSummary { String getCity(); } }
Example above is from Spring JPA docs and we can modify it a bit to reflect (quite common) parent-child relations in some entity:
interface PersonSummary { String getFirstname(); String getLastname(); Collection <AddressSummary> getAddresses(); interface AddressSummary { String getCity(); } }
This also works well, and in your service layer, the repository using this PersonSummary as type will return just that – short version with person’s first and last name along with a collection of projected address summaries. However, this is really not an optimal solution. Under the hood, we get something like an open projection where ALL entity data will be fetched for both User and Address entities involved and then only desired/defined attributes will be mapped to our summary projection types and rest of the data will be discarded. This is obviously not why we project data. We simply want data defined and nothing else to be fetched from database. Imagine heavy media attributes or any large types that we try to avoid. While this helps a bit in the network layer after the data has been fetched it still is not what we think is happening when we simply look at the projection types.
So how do we solve this? There is an open issue for Spring Data, and this might be fixed soon in more optimized fashion. And if we don’t want to wait?
Firstly, we can simply fetch raw data as we would working directly with the db-layer. Something like this:
@Query(value="SELECT NEW com.example.PersonSummaryDTO(p.id, p.firstname, p.lastname, address) FROM Person p LEFT JOIN p.addresses as address") Collection <PersonSummaryDTO> findAllBy();
What we get back from the repo is exactly the expanded rows with ‘duplicate rows’ for each new address than Person eventually have. Then we need to flatten these rows to some other DTO that reflects more the structure of Person type having the attribute of collection of addresses. This means some custom code in, perhaps service layer. IMHO this is rarely elegant code that is easy to understand. While it is not complicated to write it, it usually does not read well. Loops, checks for corner cases, error handling etc. Upside is blazing performance but somewhat ugly code.
Second option is to mirror normal repository with a ‘Read-Only’ restriction to be abundantly clear with the purpose of it and then defining entities with only wanted attributes. There is, again, extra code involved but this time it’s trivial code that both is fast to write but even fast to read. Standard repo + entity bundle with some specific names for distinction such as ReadOnlyPerson or something like that. Also, hibernate optimizes (almost completely) this alternative so the performance is blazingly fast. However one last thing to do here is to instruct Hibernate to fetch our collection items with as few SQL queries as possible (preferably only two total for our person/address example). This is done like this:
@Fetch(FetchMode.SUBSELECT) @ManyToMany(cascade=CascadeType.MERGE, fetch = FetchType.EAGER) @JoinTable(name="...", joinColumns={@JoinColumn(name="...", referencedColumnName="id")}, inverseJoinColumns={@JoinColumn(name="...", referencedColumnName="id")}) private Set<ReadOnlyChildType> someType;
Important part is FetchMode.SUBSELECT – why (from docs):
“Available for collections only. When accessing a non-initialized collection, this fetch mode will trigger loading all elements of all collections of the same role for all owners associated with the persistence context using a single secondary select.”
Also, note that this works fine for OneToMany, ManyToMany etc relations. This code scales nicely as well as no flattening is done manually it is a matter of adding few lines in entity for any other needed collections.
Simple example of a read-only repo might be something like:
@NoRepositoryBean public interface ReadOnlyRepository<T, ID> extends Repository<T, ID> { Optional<T> findById(ID id); List<T> findAll(); }
Then you can simply extend this repo with your type defined and (often) no additional methods are needed. Short and concise.