Spring Data REST automates exposing only domain object. But most often we have to deal with Data Transfer Objects. So how to do this in SDR way?
1 Answers
An approach of how to work with DTO in Spring Data REST projects
The working example is here
Entities
Entities must implement the Identifiable interface. For example:
@Entity
public class Category implements Identifiable<Integer> {
@Id
@GeneratedValue
private final Integer id;
private final String name;
@OneToMany
private final Set<Product> products = new HashSet<>();
// skipped
}
@Entity
public class Product implements Identifiable<Integer> {
@Id
@GeneratedValue
private final Integer id;
private final String name;
// skipped
}
Projections
Make a projection interface that repository query methods will return:
public interface CategoryProjection {
Category getCategory();
Long getQuantity();
}
It will be a basement for DTO. In this example DTO will represent a Category
and the number of Product
s are belong to it.
Repository methods
Create methods return the projection: a single one, a list of DTO and a paged list of DTO.
@RepositoryRestResource
public interface CategoryRepo extends JpaRepository<Category, Integer> {
@RestResource(exported = false)
@Query("select c as category, count(p) as quantity from Category c join c.products p where c.id = ?1 group by c")
CategoryProjection getDto(Integer categoryId);
@RestResource(exported = false)
@Query("select c as category, count(p) as quantity from Category c join c.products p group by c")
List<CategoryProjection> getDtos();
@RestResource(exported = false)
@Query("select c as category, count(p) as quantity from Category c join c.products p group by c")
Page<CategoryProjection> getDtos(Pageable pageable);
}
DTO
Implement DTO from its interface:
@Relation(value = "category", collectionRelation = "categories")
public class CategoryDto implements CategoryProjection {
private final Category category;
private final Long quantity;
// skipped
}
Annotation Relation
is used when Spring Data REST is rendering the object.
Controller
Add custom methods to RepositoryRestController
that will serve requests of DTO:
@RepositoryRestController
@RequestMapping("/categories")
public class CategoryController {
@Autowired private CategoryRepo repo;
@Autowired private RepositoryEntityLinks links;
@Autowired private PagedResourcesAssembler<CategoryProjection> assembler;
/**
* Single DTO
*/
@GetMapping("/{id}/dto")
public ResponseEntity<?> getDto(@PathVariable("id") Integer categoryId) {
CategoryProjection dto = repo.getDto(categoryId);
return ResponseEntity.ok(toResource(dto));
}
/**
* List of DTO
*/
@GetMapping("/dto")
public ResponseEntity<?> getDtos() {
List<CategoryProjection> dtos = repo.getDtos();
Link listSelfLink = links.linkFor(Category.class).slash("/dto").withSelfRel();
List<?> resources = dtos.stream().map(this::toResource).collect(toList());
return ResponseEntity.ok(new Resources<>(resources, listSelfLink));
}
/**
* Paged list of DTO
*/
@GetMapping("/dtoPaged")
public ResponseEntity<?> getDtosPaged(Pageable pageable) {
Page<CategoryProjection> dtos = repo.getDtos(pageable);
Link pageSelfLink = links.linkFor(Category.class).slash("/dtoPaged").withSelfRel();
PagedResources<?> resources = assembler.toResource(dtos, this::toResource, pageSelfLink);
return ResponseEntity.ok(resources);
}
private ResourceSupport toResource(CategoryProjection projection) {
CategoryDto dto = new CategoryDto(projection.getCategory(), projection.getQuantity());
Link categoryLink = links.linkForSingleResource(projection.getCategory()).withRel("category");
Link selfLink = links.linkForSingleResource(projection.getCategory()).slash("/dto").withSelfRel();
return new Resource<>(dto, categoryLink, selfLink);
}
}
When Projections are received from repository we must make the final transformation from a Projection to DTO
and 'wrap' it to ResourceSupport object before sending to the client.
To do this we use helper method toResource
: we create a new DTO, create necessary links for this object,
and then create a new Resource
with the object and its links.
Result
See the API docs on the Postman site
Singe DTO
GET http://localhost:8080/api/categories/6/dto
{
"category": {
"name": "category1"
},
"quantity": 3,
"_links": {
"category": {
"href": "http://localhost:8080/api/categories/6"
},
"self": {
"href": "http://localhost:8080/api/categories/6/dto"
}
}
}
List of DTO
GET http://localhost:8080/api/categories/dto
{
"_embedded": {
"categories": [
{
"category": {
"name": "category1"
},
"quantity": 3,
"_links": {
"category": {
"href": "http://localhost:8080/api/categories/6"
},
"self": {
"href": "http://localhost:8080/api/categories/6/dto"
}
}
},
{
"category": {
"name": "category2"
},
"quantity": 2,
"_links": {
"category": {
"href": "http://localhost:8080/api/categories/7"
},
"self": {
"href": "http://localhost:8080/api/categories/7/dto"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/categories/dto"
}
}
}
Paged list of DTO
GET http://localhost:8080/api/categories/dtoPaged
{
"_embedded": {
"categories": [
{
"category": {
"name": "category1"
},
"quantity": 3,
"_links": {
"category": {
"href": "http://localhost:8080/api/categories/6"
},
"self": {
"href": "http://localhost:8080/api/categories/6/dto"
}
}
},
{
"category": {
"name": "category2"
},
"quantity": 2,
"_links": {
"category": {
"href": "http://localhost:8080/api/categories/7"
},
"self": {
"href": "http://localhost:8080/api/categories/7/dto"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/categories/dtoPaged"
}
},
"page": {
"size": 20,
"totalElements": 2,
"totalPages": 1,
"number": 0
}
}

- 28,144
- 8
- 75
- 101
-
why do you use final fields in `CategoryDto`? It's not compilable. – Oleksandr H Nov 01 '17 at 08:52
-
and what code is //skipped ? do you skip getters and setters? I can't compile your example – Oleksandr H Nov 01 '17 at 09:04
-
а есть ли какой-то способ программно к проекции добавить линки на другие сущности? у меня в коде динамически определяется в какой проекции отправлять page с сущностями, происходит трансформация, но я не могу добавить _links на другие объекты внутри элементов страницы – Oleksandr H Nov 01 '17 at 09:27
-
@CatH Используй [ResourceProcessor](https://docs.spring.io/spring-data/rest/docs/current/reference/html/#_the_resourceprocessor_interface) и [RepositoryEntityLinks](https://docs.spring.io/spring-data/rest/docs/current/reference/html/#_programmatic_links). [Пример](https://github.com/Cepr0/restdemo/blob/master/src/main/java/restsdemo/ResourceProcessors.java) – Cepr0 Nov 01 '17 at 09:41
-
Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/157978/discussion-between-cat-h-and-cepr0). – Oleksandr H Nov 01 '17 at 09:50
-
Where is the source that states Entities must implement Identifiable? – Edwin Diaz May 11 '18 at 23:20
-
When using Spring Data Rest, by default it creates an endpoint that returns my entity with links to itself and its related entities. Isn't there a way to use this capability in a custom controller to avoid manual converting the entity to a Resource? – Jefferson Lima Dec 05 '18 at 19:20
-
3This is a pretty nice example. Thank you for it. However, I was wondering what is the benefit of using Spring-Data-Rest if we disable all methods in the repository, i.e. "exported = false". And we also set the HATEOAS links manually in the controller. What do we get from SDR? – egelev Oct 23 '19 at 20:50
-
2@egelev We don't disable 'all' repo methods, only our custom ones. – Cepr0 Oct 23 '19 at 20:57
-
1To convert incoming DTO classes to entities, this approach seems fine: https://auth0.com/blog/automatically-mapping-dto-to-entity-on-spring-boot-apis/ It is using custom annotation, ModelMapper and `RequestResponseBodyMethodProcessor`. – Lubo Mar 06 '20 at 15:09