Как использовать отдельную сущность из Spring HATEAOS PresentationModel / Dto - PullRequest
0 голосов
/ 09 мая 2020

Я использую Spring Boot 2.2.7, Spring HATEAOS 1.0, Spring JPA, Spring Data REST, Hibernate. Я создал серверный REST со сложной моделью.

Пока что я развернул свой API, открывающий миру сущности, как это позволяет Data REST. Итак, мой DTO равен моей сущности (даже если у меня несколько проекций Spring). Однако после долгого чтения я понял, что то, что делаю, не является лучшей практикой, и я хотел бы отделить Entity от Dto, сохраняя HATEOAS.

Я искал несколько сложных примеров, но не нашел не нашел ничего действительно полезного для реального приложения. Я знаю, что HATEOAS не так часто используется, но я приложил много усилий, чтобы воспользоваться им на клиенте.

Я ищу изящный способ выполнять запросы к моей БД, получать данные и преобразовать их в Dto HATEOAS.

Чтобы скопировать данные из Entity в Dto Я использую Mapstructs: я хочу максимально сократить шаблонный код.

Скажем, У меня есть эта сущность:

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

     //other fields
}

и эта сущность

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

Я создал репозитории Entity как:

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

}

Мой контроллер:

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

У меня есть некоторые недостающие части в головоломке: как определить Dto и как создать ассемблер, который не требует репликации кода, уже определенного в Dto.

Я пытался создать Dto как:

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

, а насчет EntityB я все еще не уверен:

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

Я создал общий c ассемблер :

/**
 * 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() + "/";
    }
}

и я хотел бы создать простой спецификационный c ассемблер для каждой модели, например:

@Component
public
class EntityAModelAssembler extends SimpleIdentifiableRepresentationModelAssembler<EntityAModel> {

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

Я пропустил сопоставитель mapStructs, потому что я все еще не уверен, кто должен нести ответственность за преобразование Entity в Dto, и особенно, как это сделать, когда я хочу создать, например, RepresentationModel, который несет комбинацию двух разных Entity s (т.е. чтобы избежать множественных HTTP-запросов).

...