19

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?

Cepr0
  • 28,144
  • 8
  • 75
  • 101

1 Answers1

35

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 Products 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
    }
}
Cepr0
  • 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
  • 3
    This 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
  • 1
    To 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