Я использую 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-запросов).