How to apply Spring Data projections in a Spring MVC controllers?
Is it possible to specify projection
when calling data repository method directly? Here's repository code - note I would not like to expose it via REST, instead I would like to be able to call it from a service or controller:
@RepositoryRestResource(exported = false)
public interface UsersRepository extends PagingAndSortingRepository<User, Long> {
@Query(value = "SELECT u FROM User u WHERE ....")
public Page<User> findEmployeeUsers(Pageable p);
}
Then in a controller I do this:
@PreAuthorize(value = "hasRole('ROLE_ADMIN')")
@RequestMapping(value = "/users/employee")
public Page<User> listEmployees(Pageable pageable) {
return usersRepository.findEmployeeUsers(pageable);
}
Is there any way to specify projection
for findEmployeeUsers
method when it is called directly like above?
I realise that the code above might look odd for someone... it would be possible to expose the repository via REST and put the @PreAuthorize
thing in the repository. Thought controller is the more right place to do security checks - it is more natural as well as simpler to test.
So, can projection
thing be somehow passed into a repository method called directly?
Solution 1:
No it's not, especially as projections are usually applied to the result of a query execution on a case by case basis. Thus they're currently designed to be selectively applied to domain types.
As of the latest Spring Data Fowler release train GA release the projection infrastructure can be used programmatically in Spring MVC controllers. Simply declare a Spring bean for SpelAwareProxyProjectionFactory
:
@Configuration
class SomeConfig {
@Bean
public SpelAwareProxyProjectionFactory projectionFactory() {
return new SpelAwareProxyProjectionFactory();
}
}
Then inject it into your controller and use it:
@Controller
class SampleController {
private final ProjectionFactory projectionFactory;
@Autowired
public SampleController(ProjectionFactory projectionFactory) {
this.projectionFactory = projectionFactory;
}
@PreAuthorize(value = "hasRole('ROLE_ADMIN')")
@RequestMapping(value = "/users/employee")
public Page<?> listEmployees(Pageable pageable) {
return usersRepository.findEmployeeUsers(pageable).//
map(user -> projectionFactory.createProjection(Projection.class, user);
}
}
See how as of the latest release Page
has a map(…)
method that can be used to transform the page content on the fly. We use a JDK 8 lambda to provide a conversion step using the ProjectionFactory
.
Solution 2:
It can be easily done in the lates Spring Data Rest releases!
All you need to do is to:
-
pass projection name as request param
`/api/users/search/findEmployeeUsers?projection=userView`
-
return
PagedModel<PersistentEntityResource>
instead ofPage<User>
from your service method;
Done!
and I assume you want to call this service method from your custom controller, in this case you need to return ResponseEntity<PagedModel<PersistentEntityResource>>
from your controller method.
Don't want it pageable? Simply return ResponseEntity<CollectionModel<PersistentEntityResource>>
instead.
Also check out example for single resoure projection.
Spring Data Rest takes care of applying @Projection
s to PersistentEntityResource
s on api requests, it's just like you keep exposing your @RestResource
from @RepositoryRestResource
; same behaviour for projections, keeping same naming convention, basically same URI (for current example).
Your service method with a bit of bussiness logic might look like:
@Override
@Transactional(readOnly = true)
public PagedModel<PersistentEntityResource> listEmployees(Pageable pageable, PersistentEntityResourceAssembler resourceAssembler) {
Page<User> users = userRepository.findEmployeeUsers(pageable);
List<User> entities = users.getContent();
entities.forEach(user -> user.setOnVacation(isUserOnVacationNow(user)));
CollectionModel<PersistentEntityResource> collectionModel = resourceAssembler.toCollectionModel(entities);
return PagedModel.of(collectionModel.getContent(), new PagedModel.PageMetadata(
users.getSize(),
users.getNumber(),
users.getTotalElements(),
users.getTotalPages()));
}
and your controller method might look like this:
@BasePathAwareController
public class UsersController {
@GetMapping(value = "/users/search/findEmployeeUsers")
ResponseEntity<PagedModel<PersistentEntityResource>> findEmployeeUsers(Pageable pageable,
PersistentEntityResourceAssembler resourceAssembler) {
return ResponseEntity.status(HttpStatus.OK)
.body(userService.listEmployees(pageable, resourceAssembler));
}
}
I'm using spring-boot-starter-data-rest:2.3.4.RELEASE with spring-data-rest-webmvc:3.3.4.RELEASE and spring-data-rest-webmvc:3.3.4.RELEASE as dependencies, configuring it as parent of my pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
Solution 3:
Additionally to the @Oliver's answer, if you want to lookup the Projections by name as SpringDataRest does (instead of hardwired them in your controller), this is what you have to do:
- Inject
RepositoryRestConfiguration
into your controller. This bean gives you access to a class calledProjectionDefinitions
(see,getProjectionConfiguration()
) which acts a projection metadata directory. - Using
ProjectionDefinitions
you can retrieve Projection Classes given their names and their associated bound classes. - Later, you can use the method detailed by @Oliver to create the projections instances ...
This is a small Controller that implements what I describe:
@RestController
@RequestMapping("students")
public class StudentController {
/**
* {@link StudentController} logger.
*/
private static final Logger logger =
LoggerFactory.getLogger(StudentController.class);
/**
* Projections Factory.
*/
private ProjectionFactory p8nFactory;
/**
* Projections Directory.
*/
private ProjectionDefinitions p8nDefs;
/**
* {@link Student} repository.
*/
private StudentRepository repo;
/**
* Class Constructor.
*
* @param repoConfig
* {@code RepositoryRestConfiguration} bean
* @param p8nFactory
* Factory used to create projections
* @param repo
* {@link StudentRepository} instance
*/
@Autowired
public StudentController(
RepositoryRestConfiguration repoConfig,
ProjectionFactory p8nFactory,
StudentRepository repo
) {
super();
this.p8nFactory = p8nFactory;
this.p8nDefs = repoConfig.getProjectionConfiguration();
this.repo = repo;
}
...
/**
* Retrieves all persisted students.
*
* @param projection
* (Optional) Name of the projection to be applied to
* students retrieved from the persistence layer
* @return
* {@code ResponseEntity} whose content can be a list of Students
* or a projected view of them
*/
@GetMapping(path = "", produces = APPLICATION_JSON_VALUE)
public ResponseEntity<Object> retrieveAll(
@RequestParam(required = false) String projection
) {
Class<?> type; // Kind of Projection to be applied
List<?> rawData; // Raw Entity Students
List<?> pjData; // Projected students (if applies)
rawData = this.repo.findAll();
pjData = rawData;
if (projection != null) {
type = this.p8nDefs.getProjectionType(Student.class, projection);
pjData = rawData
.stream()
.map(s -> this.p8nFactory.createProjection(type, s))
.collect(Collectors.toList());
}
return new ResponseEntity<>(pjData, HttpStatus.OK);
}
}