How to Enrich DTOs With Virtual Properties Via Spring Projections
Learn more about how to enrich DTOs with Spring projections.
Join the DZone community and get the full member experience.
Join For FreeIn JPA, as a rule of thumb, our queries (SQLs) must extract from the database only the needed data, meaning only the data that it is prone to be modified. When the fetched data is read-only (we don't plan to modify it), then we must strive to fetch it as DTOs instead of JPA entities.
Starting from this statement, let's consider the following JPA trivial entity:
@Entity
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String surname;
private String city;
private String country;
private long ssn;
private int age;
// getters, setters, ...
}
Extracting a DTO
Now, let's assume that we want to fetch from the database a DTO containing only the username
andcity
for displaying them in a GUI. So, we don't plan to modify this data and we want to ignore the usersurname
, country
, ssn,
andage
.
In Spring Data, we can accomplish this task via Spring Data Projections. Spring Data Repositories are usually returning the domain model (entities), but when we need DTOs, we can rely on projections.
A projection starts with an interface definition that contains the signatures of getters corresponding to the entity fields (database columns) that should be part of the DTO. For example, an interface for fetching only the username
andcity
can be written as follows:
public interface UserNameAndCity {
String getName();
String getCity();
}
Having this interface in place, we can continue by writing read-only queries that extract DTOs. For example, we can add in ourUserRepository,
a query that fetches the first two users bysurname
:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Transactional(readOnly = true)
List<UserNameAndCity> findFirst2BySurname(String surname);
}
Further, using Spring Data style, we can call this method and display the data:
List<UserNameAndCity> users = userRepository.findFirst2BySurname("Francisco");
logger.info(() -> "Number of users:" + users.size());
for (UserNameAndCity user : users) {
logger.info(() -> "User:" + user.getName() + ", " + user.getCity());
}
The executed query reveals that only name and city columns have been fetched from the database:
SELECT user0_.name AS col_0_0_, user0_.city AS col_1_0_
FROM user user0_
WHERE user0_.surname = ? LIMIT ?
The code of this application can be found here.
Adding Virtual Properties to DTOs
By adding virtual properties to DTOs, we can remodel data. The virtual properties can:
enrich the final DTO with properties that point to backing bean properties from the domain model (check the
livingin
property fromUserDetail
DTO below)enrich the final DTO with properties that don't have a match in the domain model (check the
sessionid
andstatus
properties fromUserDetail
DTO below)
Let's consider the below DTO, UserDetail
:
public interface UserDetail {
String getName();
@Value("#{target.city}")
String livingin();
@Value("#{ T(java.lang.Math).random() * 10000 }")
int sessionid();
@Value("online")
String status();
}
We fetch from the database only the user
name
andcity
In the projection interface,
UserDetail
, use the@Value
and Spring SpEL to point to a backing property from the domain model (in this case, the domain model propertycity
is exposed via the virtual propertylivingin
)In the projection interface,
UserDetail
, use the@Value
and Spring SpEL to enrich the result with two virtual properties that don't have a match in the domain model (in this case,sessionid
andstatus
)
Now, let's write a query for extracting a List<UserDetail>
:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Transactional(readOnly = true)
@Query("SELECT u.name as name, u.city as city FROM User u WHERE u.surname = ?1")
List<UserDetail> fetchBySurname(String surname);
}
Finally, let's display the output:
List<UserDetail> users = userRepository.fetchBySurname("Francisco");
logger.info(() -> "Number of users:" + users.size());
for (UserDetail user : users) {
logger.info(() -> "User:" + user.getName() + ", "
+ user.livingin() + ", " + user.status() + ", " + user.sessionid());
}
The complete code can be found here.
Opinions expressed by DZone contributors are their own.
Comments