52

I'm trying to add custom methods to my Spring Data repository PersonRepository as described in 1.3 Custom implementations for Spring Data repositories and exposing these method through REST. The initial code is from Accessing JPA Data with REST sample, here is the code for added/modified classes:

interface PersonRepositoryCustom {
  List<Person> findByFistName(String name);
}

class PersonRepositoryImpl implements PersonRepositoryCustom, InitializingBean {
  @Override
  public void afterPropertiesSet() throws Exception {
    // initialization here
  }
  @Override
  public List<Person> findByFistName(String name) {
    // find the list of persons with the given firstname
  }
}

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
  List<Person> findByLastName(@Param("name") String name);  
}

When I run the application and visit http://localhost:8080/portfolio/search/, I get the following response body:

{
  "_links" : {
    "findByLastName" : {
      "href" : "http://localhost:8080/people/search/findByLastName{?name}",
      "templated" : true
     }
  }
}

Why findByFirstName is not exposed even if it is available in the PersonRepository interface?

Also, is there a way to dynamically/programmatically add respositories to be exposed via REST?

George
  • 769
  • 4
  • 11
  • 31
bachr
  • 5,780
  • 12
  • 57
  • 92
  • What do you mean by not exposed? It shows up in when you hit `search` – geoand Aug 08 '14 at 13:23
  • I'm expecting to see `http://localhost:8080/people/search/findByFirstName{?name}` that exposes my custom implementation of `PersonRepository.findByFirstName(name)` – bachr Aug 08 '14 at 15:03
  • In your example `PersonRepository` has no `findByFirstName` method. I guess you want it to extend the `PersonRepositoryCustom` interface. – a better oliver Aug 22 '14 at 11:31
  • `PersonRepositoryCustom` is not a repository, but as spring looks for implementations of `PersonRepository` (that have Impl as suffix) before implementing it itself I thought it could exposes the other method `.findByFirstName(name)` which is not true as provided in the answer below. – bachr Aug 22 '14 at 13:44
  • 1
    possible duplicate of [Custom jpa repository method published by spring-data-rest](http://stackoverflow.com/questions/21116539/custom-jpa-repository-method-published-by-spring-data-rest) – JBCP Oct 23 '14 at 18:30

7 Answers7

26

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;
    }

}
leo
  • 1,243
  • 1
  • 18
  • 20
  • 11
    Seems to defeat the purpose of Spring Data REST. If I have to do all that anyway, I'll just stick with Spring MVC + HATEAOS + Data and skip Data REST. – SingleShot Aug 07 '16 at 21:46
  • 1
    This helped me a lot thank you. What is the purpose of the custom resource assembler? Does PersistentEntityResourceAssembler not suffice? I wish AbstractRepositoryRestController was public I ended up copying the class and using it as a utility within my controller for easy pagination. – Miguel Pereira Jan 13 '18 at 20:01
25

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.

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • 3
    Is there a way to assign to my custom method the corresponding HTTP method (e.g. through annotation)? – bachr Aug 10 '14 at 10:46
  • 3
    It would be better if custom methods are exposed by ``GET`` by default, because most of the custom methods are used for searching. And if the concern is backward compatibility, then an annotation to mark the method as a searching method would be preferred – James Oct 19 '14 at 01:45
  • 7
    Controllers by default don't use the Spring REST and HATEOAS serialization system. Is there a way to get them to use them so we don't end up with a disjointed API? – JBCP Oct 23 '14 at 18:32
  • 1
    Specifically, if you craft a custom controller, how do you refer to the original object? – JBCP Nov 25 '14 at 19:39
  • @JBCP, did you ever find an answer to this? I haven't. It's huge in terms of adding custom controller methods. – gyoder Jan 02 '15 at 13:03
  • 2
    @gyoder - as you found, I think http://stackoverflow.com/questions/26538156 is your best bet for now. – JBCP Jan 02 '15 at 19:03
  • I second @James, and would argue that it is possible to reason about the correct HTTP method if you follow the naming conventions of a spring data repository. I would expect that findByX() method is mappable whether the implementation is generated by spring data or specified by custom code. – Jason May 05 '15 at 10:02
  • I third @James. This is a missing functionality. How is the http://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#repositories.single-repository-behaviour supposed to help if you can't map it to a URL? – Bruce Edge Jun 18 '15 at 19:16
  • I came across this question trying to define a custom distance-sorted pageable query. Currently `order by distance(...)` clause in `@Query` is erased by `Pageable`. Also, `Sort` doesn't support expressions. Can't easily use Hibernate Spatial now. – Tair Jan 23 '17 at 09:28
  • maybe this answer should make it into the documentation of spring-data-rest, at the very beginning.... – helt Jul 11 '18 at 18:00
  • The whole problem seems to be in the spring-data-common module, and in how narrow DefaultRepositoryInformation.isQueryMethodCandidate is about what is a query. Maybe there should be marker annotation that would tag a custom method as a query without providing the implementation? If that's not enough, it could be checked that the tag exists without the @Modifier tag associated with the same method. – Davi Cavalcanti May 07 '20 at 08:00
10

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);
}
Vasily Kabunov
  • 6,511
  • 13
  • 49
  • 53
Erik Mellegård
  • 101
  • 1
  • 2
  • 1
    Hi Erik, well done finding that work-around. The danger is that the original authors see this as an issue and change the algorithm to double-check you're not doing this. However being optimistic, maybe there will be a future release of SDR that allows custom methods for GETs. – Adam Jan 13 '17 at 14:03
  • That's a great solution, I used on a project while using Spring Boot 1.5. It doesn't work on Spring Boot 2.x, though :-( I'm currently looking for a way to not have to write the whole Rest API stack for my custom queries. The sad part is it seems like a trivial problem to solve, with a marker annotation on the Spring Data module, doing just what we were doing with the dummy Query annotation for earlier versions of SDR. – Davi Cavalcanti May 07 '20 at 08:10
3

The answer is that you haven't followed instructions. Your PersonRepository has to extend both PagingAndSortingRepository<Person, Long> AND PersonRepositoryCustomin order to achieve what you're after. See https://docs.spring.io/spring-data/data-jpa/docs/current/reference/html/#repositories.custom-implementations

Tommy B
  • 187
  • 2
  • 8
1

Another option we used as well is to implement a custom repository factory for your specific storage type.

You can extend from RepositoryFactoryBeanSupport, build your own PersistentEntityInformation and take care of CRUD ops in a default repo impl for your custom data storage type. See JpaRepositoryFactoryBean for example. You maybe need to implement about 10 classes in total but then it gets reusable.

aux
  • 1,589
  • 12
  • 20
0

Try using

class PersonRepositoryCustomImpl implements PersonRepositoryCustom, InitializingBean {
    ...
}
Selaron
  • 6,105
  • 4
  • 31
  • 39
-1

The implementing class name should be PersonRepositoryCustomImpl instead of PersonRepositoryImpl.

Selaron
  • 6,105
  • 4
  • 31
  • 39
  • Suffixing with Impl is enough, according to their documentation (https://docs.spring.io/spring-data/jpa/docs/1.5.0.RC1/reference/html/repositories.html) , under 1.3.1 Adding custom behavior to single repositories -> Configuration. The same applies to more current versions: (https://docs.spring.io/spring-data/jpa/docs/2.2.4.RELEASE/reference/html/#reference) – Davi Cavalcanti May 07 '20 at 07:55