How to use separate Entity from Spring HATEAOS RepresentationModel/Dto

Viewed 824

I'm using Spring Boot 2.2.7, Spring HATEAOS 1.0, Spring JPA, Spring Data REST, Hibernate. I created a server REST with a complex model.

So far I deployed my API exposing entities to the world as Data REST permits to do. So my DTO where equals to my entity (even if I've several Spring projections). However, after many reading, I got what I'm doing is not the best practice and I'd like to separate Entity from Dto preserving HATEOAS.

I looked for some complex examples but I didn't find anything really useful for a real application. I know that HATEOAS is not used so much, but I made a lot of effort to take advantage of it on the client.

I'm looking for a neat way to perform queries on my DB, get data and convert them to a Dto HATEOAS capable.

To copy data from Entity to Dto I'm using Mapstructs: I want to reduce as much as possible boilerplate code.

Let's say I've this entity:

@Entity
@Data
@Builder
public class EntityA implements Persistable<Long>, Serializable{
     private long id;
     private String 

     //other fields
}

and this entity

@Entity

@Data
@Builder
public class EntityB implements Persistable<Long>, Serializable{
    @ToString.Exclude    
    @NotNull
    @OnDelete(action = OnDeleteAction.CASCADE)
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Document document;

    //other fields    
}

I created Entity repositories as:

@Transactional
@PreAuthorize("isAuthenticated()")
public interface ContactRepository extends JpaRepository<EntityA, Long>, ContactCustomRepository<EntityA, Long>, JpaSpecificationExecutor<EntityA> {

}

My controller is:

@RepositoryRestController
@PreAuthorize("isAuthenticated()")
@Log4j2
public class EntityAController {

 @GetMapping(path = "/entityA/{id:[0-9]+}")
    public ResponseEntity<?> get(@PathVariable(value = "id") long id) {
        return ResponseEntity.ok(/*assembler??*/.toModel(myservice.get(id)));
    }

I've some missing pieces to the puzzle: how to define the Dto and how to create an assembler that doesn't require to replicate the code already defined in the Dto.

I was trying to create the Dto as:

@Data
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@JsonRootName(value = "entityA")
@Relation(collectionRelation = "entitiesA")
@Builder
public class EntityAModel extends RepresentationModel<EntityA> implements Serializable {
     private long id;
     private String 
     //other fields
}

and about EntityB I'm still more unsure:

@Data
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@JsonRootName(value = "entityB")
@Relation(collectionRelation = "entitiesB")
@Builder
public class EntityBModel extends RepresentationModel<EntityB> implements Serializable {
     private EntityA entityA;
     //other fields
}

I created a generic assembler:

/**
 * A {@link SimpleRepresentationModelAssembler} that mixes together a Spring web controller and a
 * {@link LinkRelationProvider} to build links upon a certain strategy.
 *
 * @author Greg Turnquist
 */
public class SimpleIdentifiableRepresentationModelAssembler<T> implements SimpleRepresentationModelAssembler<T> {

    /**
     * The Spring MVC class for the object from which links will be built.
     */
    private final Class<?> controllerClass;

    /**
     * A {@link LinkRelationProvider} to look up names of links as options for resource paths.
     */
    @Getter
    private final LinkRelationProvider relProvider;

    /**
     * A {@link Class} depicting the object's type.
     */
    @Getter
    private final Class<?> resourceType;

    /**
     * Default base path as empty.
     */
    @Getter
    @Setter
    private String basePath = "";

    /**
     * Default a assembler based on Spring MVC controller, resource type, and {@link LinkRelationProvider}. With this
     * combination of information, resources can be defined.
     *
     * @param controllerClass - Spring MVC controller to base links off of
     * @param relProvider
     * @see #setBasePath(String) to adjust base path to something like "/api"/
     */
    public SimpleIdentifiableRepresentationModelAssembler(Class<?> controllerClass, LinkRelationProvider relProvider) {

        this.controllerClass = controllerClass;
        this.relProvider = relProvider;

        // Find the "T" type contained in "T extends Identifiable<?>", e.g.
        // SimpleIdentifiableRepresentationModelAssembler<User> -> User
        this.resourceType = GenericTypeResolver.resolveTypeArgument(this.getClass(),
                SimpleIdentifiableRepresentationModelAssembler.class);
    }

    /**
     * Alternate constructor that falls back to {@link EvoInflectorLinkRelationProvider}.
     *
     * @param controllerClass
     */
    public SimpleIdentifiableRepresentationModelAssembler(Class<?> controllerClass) {
        this(controllerClass, new EvoInflectorLinkRelationProvider());
    }

    /**
     * Add single item self link based on the object and link back to aggregate root of the {@literal T} domain type using
     * {@link LinkRelationProvider#getCollectionResourceRelFor(Class)}}.
     *
     * @param resource
     */
    @Override
    public void addLinks(EntityModel<T> resource) {

        resource.add(getCollectionLinkBuilder().slash(getId(resource)).withSelfRel());
        resource.add(getCollectionLinkBuilder().withRel(this.relProvider.getCollectionResourceRelFor(this.resourceType)));
    }

    private Object getId(EntityModel<T> resource) {

        Field id = ReflectionUtils.findField(this.resourceType, "id");
        ReflectionUtils.makeAccessible(id);

        return ReflectionUtils.getField(id, resource.getContent());
    }

    /**
     * Add a self link to the aggregate root.
     *
     * @param resources
     */
    @Override
    public void addLinks(CollectionModel<EntityModel<T>> resources) {
        resources.add(getCollectionLinkBuilder().withSelfRel());
    }

    /**
     * Build up a URI for the collection using the Spring web controller followed by the resource type transformed by the
     * {@link LinkRelationProvider}. Assumption is that an {@literal EmployeeController} serving up {@literal Employee}
     * objects will be serving resources at {@code /employees} and {@code /employees/1}. If this is not the case, simply
     * override this method in your concrete instance, or resort to overriding {@link #addLinks(EntityModel)} and
     * {@link #addLinks(CollectionModel)} where you have full control over exactly what links are put in the individual
     * and collection resources.
     *
     * @return
     */
    protected LinkBuilder getCollectionLinkBuilder() {

        WebMvcLinkBuilder linkBuilder = linkTo(this.controllerClass);

        for (String pathComponent : (getPrefix() + this.relProvider.getCollectionResourceRelFor(this.resourceType))
                .split("/")) {
            if (!pathComponent.isEmpty()) {
                linkBuilder = linkBuilder.slash(pathComponent);
            }
        }

        return linkBuilder;
    }

    /**
     * Provide opportunity to override the base path for the URI.
     */
    private String getPrefix() {
        return getBasePath().isEmpty() ? "" : getBasePath() + "/";
    }
}

and I would hope to create a simple specific assembler for each model like this:

@Component
public
class EntityAModelAssembler extends SimpleIdentifiableRepresentationModelAssembler<EntityAModel> {

    DocumentFullModelAssembler() {
        super(EntityAController.class);
    }
}

I omitted the mapStructs mapper because I'm still not sure who should be responsible to convert Entity to the Dto and expecially how to do that when I want to create a RepresentationModel, for example, that carries a combination of two different Entitys (i.e. to avoid multiple HTTP requests).

0 Answers
Related