Spring Data REST - Notes

Gist

Posted by Tomy Jaya on February 28, 2021

I just realized that people found the notes I wrote about Spring Data REST about 3 years ago useful. For SEO’s sake, I’ll make a copy of it here:

How to change baseUri:

in application.properties add:

spring.data.rest.basePath=/api
  • or -

in application.yml add:

spring:
  data:
    rest: 
      basePath: /api

WARNING:

  1. Some SOs answer will suggest the deprecated baseUri. But, as of spring boot 1.5.1.RELEASE, it seems that baseUri is removed.

  2. in YAML, make sure you nest the properties otherwise it’ll complain duplicate key:

Exception in thread "main" while parsing MappingNode
 in 'reader', line 2, column 1:
    server:
    ^
Duplicate key: spring
 in 'reader', line 29, column 23:
    #      security: DEBUG

nesting example:

# JACKSON
spring:
  jackson:
    serialization:
      INDENT_OUTPUT: true
  # TOMY: below added by me    
  data:
    rest: 
      basePath: /api

IMPORTANT PSA ABOUT YAML: Beware that YAML doesn’t allow tabs for indentation. It only accepts 2 or 4 spaces.

How to prevent spring data repository from being exported as REST?

import java.util.List;

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(exported=false)
public interface PersonRepository extends MongoRepository<Person, String> {
}

How to change path instead of the default pluralized name

import java.util.List;

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends MongoRepository<Person, String> {
}

How to Make REST Resource Repository read-only (i.e. only support GET, HEAD, OPTIONS method)

Create and extend the below class. It uses annotation to suppress save and delete from being exposed as REST APIs (effectively removing POST and DELETE methods support).

import java.io.Serializable;

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.rest.core.annotation.RestResource;

@NoRepositoryBean
public interface GetOnlyMongoRestRepository<T, ID extends Serializable> extends MongoRepository<T, ID> {
    @Override
    @RestResource(exported = false)
    void delete(ID id);

    @Override
    @RestResource(exported = false)
    void delete(T entity);

    @Override
    @RestResource(exported = false)
    <S extends T> S save(S entity);
}

How to project nested object (e.g. DBRef)

For instance, you have a Order object with a nested orderer reference which is of type Person.

public class Order {
    @Id
    private String id;

    private int quantity;
    private String productName;

    @DBRef
    private Person orderer;
    
    // usual getter and setter
}

You can create a projection such as the below:

import org.springframework.data.rest.core.config.Projection;

@Projection(name = "inlineOrderer", types = { Order.class })
interface InlineOrderer {
    String getProductName();
    int getQuantity();
    Person getOrderer();
}

Now you can query your REST API using: http://localhost:8080/api/orders/some-order-id?projection=inlineOrderer

Additionally, if you want this projection to be default when listing the items in the collection, use the below:

@RepositoryRestResource(excerptProjection = InlineOrderer.class)
public interface OrderRepository extends GetOnlyMongoRestRepository<Order, String> {

}

How to query by a certain attributes (search/ filter function)

In the CrudRepository interface, add the query method:

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends MongoRepository<Person, String> {

    List<Person> findByLastNameContainingIgnoreCase(@Param("name") String name);
    
}

then, you can access it using: http://localhost:8080/api/people/search/findByLastNameContainingIgnoreCase?name=ang

You can alias it:

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends MongoRepository<Person, String> {

    @RestResource(path="searchByLastName", rel="searchByLastName")
    List<Person> findByLastNameContainingIgnoreCase(@Param("name") String name);
    
}

then, you can access it using alias: http://localhost:8080/api/people/search/searchByLastName?name=ang

Note:

  • Containing does *SEARCH_STRING*
  • IgnoreCase does case-insensitive search

Refer to the below link for all query methods: http://docs.spring.io/spring-data/jpa/docs/1.5.1.RELEASE/reference/html/jpa.repositories.html#jpa.query-methods.query-creation

How to query multiple fields

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends MongoRepository<Person, String> {

    // Find multiple fields
    List<Person> findByLastNameOrFirstNameContainingAllIgnoreCase(@Param("name") String firstName,
            @Param("name") String lastName);

}

http://localhost:8080/api/people/search/findByLastNameOrFirstNameContainingAllIgnoreCase?name=test

Note:

  • All makes both FirstName and LastName ignore case *\*
  • Or joins the 2 criteria

How to query multiple TextIndexed fields

Set up the index in the POJO:

import org.springframework.data.mongodb.core.index.TextIndexed;
import org.springframework.data.mongodb.core.mapping.Document;

@Document
public class Person {

    @TextIndexed
    private String aboutMe;
    @TextIndexed
    private String address;
    
    // ...
}

NOTES:

  • Don’t forget to annotate @Document. Without it, somehow the text index is not created.
  • There can only be one text index per collection in MongoDB. So all the fields annotated with @TextIndexed will be combined into one.
  • You can assign weight to the text index: http://docs.spring.io/spring-data/mongodb/docs/current/api/org/springframework/data/mongodb/core/index/TextIndexed.html

Modify the MongoRepository to add the method to findAllBy:

import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.repository.query.Param;

public interface PersonRepository extends MongoRepository<Person, String> {

    // Find in indexed text
    List<Person> findAllBy(@Param("criteria") TextCriteria criteria);
   
}

Since Spring data rest doesn’t have the default converter from String to TextCriteria, create a RestController wrapper to invoke this:

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.repository.query.Param;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/people")
public class PersonController {

    private static final Logger LOG = LoggerFactory.getLogger(PersonController.class);

    @Autowired
    private PersonRepository personRepository;

    @RequestMapping(value = "search", method = RequestMethod.GET)
    public List<Person> search(@Param("criteria") String criteria) {
        LOG.debug("Search invoked for criteria {}", criteria);
        return personRepository.findAllBy(TextCriteria.forDefaultLanguage().matchingAny(criteria));
    }
}

You can now access it here:

http://localhost:8080/people/search?criteria=happy

How to page the results

E.g. page every 2 results. Access it here:

http://localhost:8080/api/orders?size=2

In addition to the normal results, there will be _links property to navigate to the next pages. HETEOAS FTW!

How to sort

E.g. sort by quantity descending. Access it here:

http://localhost:8080/api/orders?sort=quantity,desc

NOTE:

  • Use comma to separate field name is sort direction

How to serialize java 8 LocalDate nicely in spring data rest APIs

Add the below maven dependency:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

Add the below line in application.properties:

spring.jackson.serialization.write_dates_as_timestamps=false

LocalDate will be serialized as yyyy-MM-dd.

Resource: http://stackoverflow.com/questions/29956175/json-java-8-localdatetime-format-in-spring-boot

Two-way @DBRef reference

Basically, if you have a User which has an Address and you want the Address to have a back pointer to the User, Do NOT use two-way @DBRef! When spring-data-rest serializes the object, you’ll run into a infinite recursion and possibly get the below exception: java.lang.IllegalStateException: getOutputStream() has already been called for this response

How to generate sequence ID for Spring Data Mongodb

Follow the instructions here: http://stackoverflow.com/questions/36448921/how-we-create-autogenerated-field-for-mongodb-using-springboot

How to index a field

Just annotate it with @Indexed. But remember to annotate the POJO with @Document for MongoDB to scan the metadata mapping. E.g.

@Document
public class Person {

  @Id
  private ObjectId id;
  @Indexed
  private Integer ssn;

How to handle LocalDate in Controller

Annotate the request parameter with @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)

DateTimeFormat.ISO.DATE is yyyy-MM-dd format.

@RequestMapping(value = "/protected/getPastUserOrders", method = RequestMethod.GET)
public List<Order> getPastUserOrders(@RequestParam("userId") String userId, @RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date)  throws IOException {
    return orderRepository.findByUser_FacebookIdAndDateBefore(userId, date);
}

How to pass deserialize JSON object as a java Map?

Just declare the type as Map<K,V>:

private Map<String, Boolean> daysOpen;

How to read file in ClassPath to String (One-liner)

  1. First, don’t use ClassPathResource::getFile. This doesn’t work when your file is bundled in a jar. (i.e. only works in an IDE).
  2. Hence, use ClassPathResource::getInputStream
  3. How to easily convert InputStream to String? Use IOUtils

Sample:

String templateString = IOUtils.toString(new ClassPathResource("templates/order-receipt.html").getInputStream(), "UTF-8");

References:

  • http://stackoverflow.com/questions/25869428/classpath-resource-not-found-when-running-as-jar
  • http://stackoverflow.com/questions/309424/read-convert-an-inputstream-to-a-string

Epilogue

Original Gist hosted on Github can be found here: https://gist.github.com/TomyJaya/31e48d81af0ca236496582d00192ef80

Note that any future updates will be made in this blog post instead. However, since I don’t work with this technology actively anymore, you shouldn’t expect much of such.