How to add custom methods to Spring Data Rest JPA implementation and leverage HATEOS support?
Asked Answered
E

3

13

I have a Spring Data Rest Repository controller that utilizes JPA for the query implementation, and I need to add some custom query methods that cannot be done using the standard queryByExample method that JPA supports. I have created an Impl class that has the necessary method, but I cannot get it to be recognized. I saw that I can utilize a standard Spring MVC Controller, but I want to have a unified API, and basically all I really want is to implement my own custom /search methods.

Even with the custom controller, the problem is then that the HAL links and other related items are no longer provided.

Can the Spring folks spend some time having someone document how to do some of this more advanced stuff? I'm guessing that having to implement your own search methods at times are fairly common, and it would be time well spent to make it clear how to do this.

Eckel answered 10/4, 2015 at 21:28 Comment(6)
The easiest approach is to define custom JPQL queries using @Query annotations on methods defined by the Repository interface. This article provides the details. Personally I don't like this approach since you are forced to use verbs (e.g. /resource/search/findByCustom) which is not RESTful; so I created my own search controller and used some of the Spring Data Rest Components to properly integrate with the rest of the Resources (ping me you want more details)Riband
@Riband You are not forced to use verbs. But even if you choose to use "findByX" then it's only a search with the name "findByX". It is a resource and it is RESTful.Coverup
@zeroflagL I agree you don't have to use findByX names, but in Java: "By convention, method names should be a verb in lowercase or a multi-word name that begins with a verb". Exposing them directly under the /search sub-resource you end up creating multiple methods on the same Resource which look more like verbs (or actions) than nouns; this is not good since this is what HTTP methods should be used for: actions. Instead, my preference is to use the HTTP query part of the Resource URL to define the filtering criteria: http://..../resource?q=name:somename&page=1&size=10Riband
@Riband The JDK contains plenty of methods that don't follow that pattern. The Java architects themselves even recommend something like users alongside getUsers to additionally support a Stream return type. But even if you name your method findByX it doesn't mean that you have to name the resource findByX too. You can have findByName while exposing it as /users/search/foo. You are arguing that a renowned framework supporting the highest level of REST is not RESTful. Think about it. Btw: There are experts who would discourage ?q=name:somename.Coverup
@zeroflagL I'm not arguing Spring Data REST is not RESTful..I think it's absolutely awesome! I'm arguing with your note that findByX is a Resource..it does not look as such. I also don't like that Data REST recommends using (and exposing) findByX methods in it's guides. Wrt the method names, there can be exceptions yes, but the intuition and good practice is to use verbs or something that defines an action. Good point that you can expose the repository methods under different paths that look more like Resources; I give you that.Riband
@zeroflagL on more note: I recon that for some cases, using the findByX methods can be useful and speed up development. Though, IMPOV, the best approach is to do something in-line with Oliver's answer below. Especially in cases where you want to expose more sophisticated search features, e.g. allowing the client to search using any combination of the Resource properties. Wrt the ?q=name:somename, why do you think is discouraged? I recon that the HTTP query can be used to pass filtering parameters, but I prefer not to mix the filter query string with other params (e.g. paging, etc..).Riband
E
11

A simple implementation could look like this:

@BasePathAwareController
class CustomInvocationsController implements ResourceProcessor<RepositorySearchesResource> {

  private final YourRepository repository;

  public CustomInvocationsController(YourRepository repository) {
    this.repository = repository;
  }

  @RequestMapping(…)
  ResponseEntity<?> handleRequest(PersistentEntityResourceAssembler assembler)

    // invoke repository
    // Use assembler to build a representation
    // return ResponseEntity
  }

  @Override
  public RepositorySearchesResource process(RepositorySearchesResource resource) {
    // add Link to point to the custom handler method
  }
}

A few things to note:

  • using @BasePathAwareController instead of a plain @Controller makes sure whatever you're mapping the handler method to, it will consider the base path you've configured on Spring Data REST, too.
  • within the request mapping, use everything you already know from Spring MVC, choose an appropriate HTTP method.
  • PersistentEntityResourceAssembler basically abstracts setting up a representation model within a PersistentEntityResource so that the Spring Data REST specific treatment of associations etc. kicks in (associations becoming links etc.
  • implement ResourceProcessor to post-process RepositorySearchesResource which is returned for the resource rendering all searches. Currently, there's no way to determine which domain type that resource was rendered. I filed and fixed DATAREST-515 to improve that.
Elector answered 11/4, 2015 at 13:40 Comment(4)
Oliver, thanks for the info. I understand a bit of what you are saying, but I'm still struggling with your example. I am fairly new to Spring in general, so I think I'm getting lost in some of the implied assumptions. I think having an explicit, working example is the best way to go. As I figure this out, I hope to be able to post an explicit example that resolves all of that. In the mean time, I have a few questions. * When using @BasePathAwareController, do I still need to add a @RequestMapping to the class, or use the @RequestMapping on the method, and use rely on it being absolute?Eckel
In your example, do you mean to have an explicit method, ResponseEntity<?> handleRequest(PersistentEntityResourceAssembler assembler), or is that an example of a method that I should implement? Then the question is, how do I gain access to the input parameters? Do I just add @RequestParam attributes to the method? For the PersistentEntityResourceAssembler, I had started down the road if implementing my own Assembler class, and using that to generate a Resource which is then in turn used to generate the RequestEntity. With your example, am I correct that I don't need that any more?Eckel
@olivergierke RepositorySearchesResource works well but I have observed it doesn't render search links if there are no standard search methods exposed ?Cassock
What's the difference between @RepositoryRestController and @BasePathAwareController, other than the former ignores the base path?Gearalt
E
4

Ok, based upon the information provided so far, I have something working that I think makes sense. I'm definitely looking for someone to validate my theories so far.

The goal was to be able to implement additional custom methods to the methods that SDR already provides. This is because I need to implement additional logic that cannot be expressed as simple Spring Data Repository query methods (the findByXXX methods). In this case, I'm looking to query other data sources, like Solr, as well as provide custom manipulation of the data before its returned.

I have implemented a Controller based upon what @oliver suggested, as follows:

@BasePathAwareController
@RequestMapping(value = "/persons/search")
public class PersonController implements ResourceProcessor<RepositorySearchesResource>, ResourceAssembler<Person, Resource<Person>> {

    @Autowired
    private PersonRepository repository;

    @Autowired
    private EntityLinks entityLinks;

    @RequestMapping(value = "/lookup", method = RequestMethod.GET)
    public ResponseEntity<Resource<Person>> lookup(@RequestParam("name") String name)
    {
        try
        {
          Resource<Person> resource = toResource(repository.lookup(name));
          return new ResponseEntity<Resource<Person>>(resource, HttpStatus.OK);
        }
        catch (PersonNotFoundException nfe)
        {
            return new ResponseEntity<Resource<Person>>(HttpStatus.OK);
        }
    }

    @Override
    public RepositorySearchesResource process(RepositorySearchesResource resource) {

        LinkBuilder lb = entityLinks.linkFor(Person.class, "name");
        resource.add(new Link(lb.toString() + "/search/lookup{?name}", "lookup"));
        return resource;
    }

    @Override
    public Resource<Person> toResource(Person person) {
        Resource<IpMaster> resource = new Resource<Person>(person);

        return resource;
    }

This produces a "lookup" method that is considered a template and is listed when you do a query on /persons/search, along with the other search methods defined in the Repository. It doesn't use the PersistentEntityResourceAssembler that was suggested. I think I would rather use that, but then I'm a bit confused as to how to get it injected, and what the comment about the filed bug means.

Eckel answered 13/4, 2015 at 15:27 Comment(6)
I've run into an issue with this implementation. It appears that the RepositorySearchResource process() method is being called for all /search related methods, even in the case of a query of /other/search for a search of a different Repository then this Controller is meant to be related to. It appears that they code is scanning for all such process() methods when constructing the /search links. Is there any way to influence that?Eckel
I'm running into the same problem. I filed #31575759, which Oliver also answered. I came across this trying to make sense of his response, which, as you mentioned assumed a degree of spring knowledge. Have you resolved the above mapping issue?Sundaysundberg
I sort of have this working. I ended up moving away from some of the custom methods for unrelated reasons, so I haven't fully nailed this yet. The key is to inject the ```PersistentEntityResourceAssembler```` as a method into your customer Controller method, and then use that to generate the Resource or Resources objects. Basically, you use Spring Data Repository to get your data, and then iterate over that. The key is to have your method return a Resources<Resource<Object>> and not your Entity type. Otherwise you get class cast issues. Spend some time in the SDR and HATEOAS source too.Eckel
This worked for me, making it part of the data rest. I did not implement exactly the same, but it was a good base to get it going.Nebula
@MarcZampetti, what is IpMaster? Like I said, I have it working, but I did not include the ResourceAssembler and was trying to get an example like yours working.Nebula
IpMaster is just one of my internal classes.Eckel
E
0

See the latest docs, as the most recent version of SDR has more details on this, and makes it a bit easier. The key is to use the @RepositoryRestController annotation to be able to add additional methods under the /search endpoint, or the @BasePathAwareController annotation if you want to add endpoints to other parts of the url namespace. Then include the PersistentEntityResourceAssembler as a parameter to your controller method, and use that to generate the HAL output if you are returning your entity objects.

Eckel answered 5/11, 2015 at 17:50 Comment(1)
Will you provide a link to the section in the docs you referred to in this answer?Hofmann

© 2022 - 2024 — McMap. All rights reserved.