Implementing custom methods of Spring Data repository and exposing them through REST

JavaSpringRestJpaSpring Data

Java Problem Overview


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?

Java Solutions


Solution 1 - Java

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

}

Solution 2 - Java

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.

Solution 3 - Java

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

Solution 4 - Java

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

Solution 5 - Java

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.

Solution 6 - Java

Try using

class PersonRepositoryCustomImpl implements PersonRepositoryCustom, InitializingBean {
    ...
}

Solution 7 - Java

The implementing class name should be PersonRepositoryCustomImpl instead of PersonRepositoryImpl.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionbachrView Question on Stackoverflow
Solution 1 - JavaleoView Answer on Stackoverflow
Solution 2 - JavaOliver DrotbohmView Answer on Stackoverflow
Solution 3 - JavaErik MellegårdView Answer on Stackoverflow
Solution 4 - JavaTommy BView Answer on Stackoverflow
Solution 5 - JavaauxView Answer on Stackoverflow
Solution 6 - JavaBinulal NarayananView Answer on Stackoverflow
Solution 7 - JavaBinulal NarayananView Answer on Stackoverflow