Implementing custom methods of Spring Data repository and exposing them through REST
After two days, I have solved in this way.
Custom Repository Interface:
public interface PersonRepositoryCustom {
Page<Person> customFind(String param1, String param2, Pageable pageable);
}
Custom Repository Implementation
public class PersonRepositoryImpl implements PersonRepositoryCustom{
@Override
public Page<Person> customFind(String param1, String param2, Pageable pageable) {
// custom query by mongo template, entity manager...
}
}
Spring Data Repository:
@RepositoryRestResource(collectionResourceRel = "person", path = "person")
public interface PersonRepository extends MongoRepository<Person, String>, PersonRepositoryCustom {
Page<Person> findByName(@Param("name") String name, Pageable pageable);
}
Bean Resource representation
public class PersonResource extends org.springframework.hateoas.Resource<Person>{
public PersonResource(Person content, Iterable<Link> links) {
super(content, links);
}
}
Resource Assembler
@Component
public class PersonResourceAssembler extends ResourceAssemblerSupport<Person, PersonResource> {
@Autowired
RepositoryEntityLinks repositoryEntityLinks;
public PersonResourceAssembler() {
super(PersonCustomSearchController.class, PersonResource.class);
}
@Override
public PersonResource toResource(Person person) {
Link personLink = repositoryEntityLinks.linkToSingleResource(Person.class, person.getId());
Link selfLink = new Link(personLink.getHref(), Link.REL_SELF);
return new PersonResource(person, Arrays.asList(selfLink, personLink));
}
}
Custom Spring MVC Controller
@BasePathAwareController
@RequestMapping("person/search")
public class PersonCustomSearchController implements ResourceProcessor<RepositorySearchesResource> {
@Autowired
PersonRepository personRepository;
@Autowired
PersonResourceAssembler personResourceAssembler;
@Autowired
private PagedResourcesAssembler<Person> pagedResourcesAssembler;
@RequestMapping(value="customFind", method=RequestMethod.GET)
public ResponseEntity<PagedResources> customFind(@RequestParam String param1, @RequestParam String param2, @PageableDefault Pageable pageable) {
Page personPage = personRepository.customFind(param1, param2, pageable);
PagedResources adminPagedResources = pagedResourcesAssembler.toResource(personPage, personResourceAssembler);
if (personPage.getContent()==null || personPage.getContent().isEmpty()){
EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
EmbeddedWrapper wrapper = wrappers.emptyCollectionOf(Person.class);
List<EmbeddedWrapper> embedded = Collections.singletonList(wrapper);
adminPagedResources = new PagedResources(embedded, adminPagedResources.getMetadata(), adminPagedResources.getLinks());
}
return new ResponseEntity<PagedResources>(adminPagedResources, HttpStatus.OK);
}
@Override
public RepositorySearchesResource process(RepositorySearchesResource repositorySearchesResource) {
final String search = repositorySearchesResource.getId().getHref();
final Link customLink = new Link(search + "/customFind{?param1,param2,page,size,sort}").withRel("customFind");
repositorySearchesResource.add(customLink);
return repositorySearchesResource;
}
}
The reason these methods are not exposed is that you're basically free to implement whatever you want in custom repository methods and thus it's impossible to reason about the correct HTTP method to support for that particular resource.
In your case it might be fine to use a plain GET
, in other cases it might have to be a POST
as the execution of the method has side effects.
The current solution for this is to craft a custom controller to invoke the repository method.
For GET
methods I have used the following approach:
- create a dummy
@Query
method in the Repository (LogRepository.java) - create a custom interface with the same method declared (LogRepositoryCustom.java)
- create an implementation of the custom interface (LogRepositoryImpl.java)
Using this approach I don't have to manage projections and resource assembling.
@RepositoryRestResource(collectionResourceRel = "log", path = "log")
public interface LogRepository extends PagingAndSortingRepository<Log, Long>,
LogRepositoryCustom {
//NOTE: This query is just a dummy query
@Query("select l from Log l where l.id=-1")
Page<Log> findAllFilter(@Param("options") String options,
@Param("eid") Long[] entityIds,
@Param("class") String cls,
Pageable pageable);
}
public interface LogRepositoryCustom {
Page<Log> findAllFilter(@Param("options") String options,
@Param("eid") Long[] entityIds,
@Param("class") String cls,
Pageable pageable);
}
In the implementation you are free to use the repository methods or going directly to the persistence layer:
public class LogRepositoryImpl implements LogRepositoryCustom{
@Autowired
EntityManager entityManager;
@Autowired
LogRepository logRepository;
@Override
public Page<Log> findAllFilter(
@Param("options") String options,
@Param( "eid") Long[] entityIds,
@Param( "class" ) String cls,
Pageable pageable) {
//Transform kendoui json options to java object
DataSourceRequest dataSourceRequest=null;
try {
dataSourceRequest = new ObjectMapper().readValue(options, DataSourceRequest.class);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
Session s = entityManager.unwrap(Session.class);
Junction junction = null;
if (entityIds != null || cls != null) {
junction = Restrictions.conjunction();
if (entityIds != null && entityIds.length > 0) {
junction.add(Restrictions.in("entityId", entityIds));
}
if (cls != null) {
junction.add(Restrictions.eq("cls", cls));
}
}
return dataSourceRequest.toDataSourceResult(s, Log.class, junction);
}