
Pour des raisons bien connues, le backend ne peut pas renvoyer les données du référentiel telles quelles. Le plus célèbre - les dépendances essentielles ne sont pas prises de la base sous la forme dans laquelle le front peut les comprendre. Ici, vous pouvez ajouter des difficultés avec l'analyse enum (si les champs enum contiennent des paramètres supplémentaires) et de nombreuses autres difficultés résultant de la conversion automatique de type (ou de l'impossibilité de les convertir automatiquement). Cela implique la nécessité d'utiliser un objet de transfert de données - DTO, qui est compréhensible à la fois pour l'arrière et l'avant.
La conversion d'une entité en DTO peut se faire de différentes manières. Vous pouvez utiliser la bibliothèque, vous pouvez (si le projet est petit) assembler quelque chose comme ceci:
@Component public class ItemMapperImpl implements ItemMapper { private final OrderRepository orderRepository; @Autowired public ItemMapperImpl(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @Override public Item toEntity(ItemDto dto) { return new Item( dto.getId(), obtainOrder(dto.getOrderId()), dto.getArticle(), dto.getName(), dto.getDisplayName(), dto.getWeight(), dto.getCost(), dto.getEstimatedCost(), dto.getQuantity(), dto.getBarcode(), dto.getType() ); } @Override public ItemDto toDto(Item item) { return new ItemDto( item.getId(), obtainOrderId(item), item.getArticle(), item.getName(), item.getDisplayName(), item.getWeight(), item.getCost(), item.getEstimatedCost(), item.getQuantity(), item.getBarcode(), item.getType() ); } private Long obtainOrderId(Item item) { return Objects.nonNull(item.getOrder()) ? item.getOrder().getId() : null; } private Order obtainOrder(Long orderId) { return Objects.nonNull(orderId) ? orderRepository.findById(orderId).orElse(null) : null; } }
Ces mappeurs auto-écrits présentent des inconvénients évidents:
- Ne pas mettre à l'échelle.
- Lorsque vous ajoutez / supprimez même le champ le plus insignifiant, vous devrez modifier le mappeur.
Par conséquent, la bonne solution consiste à utiliser une bibliothèque de mappage. Je connais modelmapper et mapstruct. Depuis que je travaille avec modelmapper, je vais en parler, mais si vous, mon lecteur, connaissez bien mapstruct et pouvez parler de toutes les subtilités de son application, écrivez un article à ce sujet, et je serai le premier à me l'écrire (cette bibliothèque est aussi très intéressant, mais il n'y a pas encore le temps d'y entrer).
Alors modelmapper.
Je veux dire tout de suite que si quelque chose n'est pas clair pour vous, vous pouvez télécharger le projet fini avec un test de travail, un lien à la fin de l'article.
La première étape consiste, bien sûr, à ajouter une dépendance. J'utilise gradle, mais il vous est facile d'ajouter une dépendance à votre projet maven.
compile group: 'org.modelmapper', name: 'modelmapper', version: '2.3.2'
Cela suffit pour que le mappeur fonctionne. Ensuite, nous devons créer un bac.
@Bean public ModelMapper modelMapper() { ModelMapper mapper = new ModelMapper(); mapper.getConfiguration() .setMatchingStrategy(MatchingStrategies.STRICT) .setFieldMatchingEnabled(true) .setSkipNullEnabled(true) .setFieldAccessLevel(PRIVATE); return mapper; }
Habituellement, il suffit de renvoyer simplement un nouveau ModelMapper, mais il ne sera pas superflu de configurer le mappeur pour nos besoins. J'ai défini une stratégie de correspondance stricte, activé le mappage des champs, ignoré les champs nuls et défini un niveau d'accès privé aux champs.
Ensuite, créez la structure d'entité suivante. Nous aurons une licorne, qui aura un certain nombre de droïdes subordonnés, et chaque droïde aura un certain nombre de cupcakes.
EntitésParent abstrait:
@MappedSuperclass @Setter @EqualsAndHashCode @NoArgsConstructor @AllArgsConstructor public abstract class AbstractEntity implements Serializable { Long id; LocalDateTime created; LocalDateTime updated; @Id @GeneratedValue public Long getId() { return id; } @Column(name = "created", updatable = false) public LocalDateTime getCreated() { return created; } @Column(name = "updated", insertable = false) public LocalDateTime getUpdated() { return updated; } @PrePersist public void toCreate() { setCreated(LocalDateTime.now()); } @PreUpdate public void toUpdate() { setUpdated(LocalDateTime.now()); } }
Licorne:
@Entity @Table(name = "unicorns") @EqualsAndHashCode(callSuper = false) @Setter @AllArgsConstructor @NoArgsConstructor public class Unicorn extends AbstractEntity { private String name; private List<Droid> droids; private Color color; public Unicorn(String name, Color color) { this.name = name; this.color = color; } @Column(name = "name") public String getName() { return name; } @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "unicorn") public List<Droid> getDroids() { return droids; } @Column(name = "color") public Color getColor() { return color; } }
Droid:
@Setter @EqualsAndHashCode(callSuper = false) @Entity @Table(name = "droids") @AllArgsConstructor @NoArgsConstructor public class Droid extends AbstractEntity { private String name; private Unicorn unicorn; private List<Cupcake> cupcakes; private Boolean alive; public Droid(String name, Unicorn unicorn, Boolean alive) { this.name = name; this.unicorn = unicorn; this.alive = alive; } public Droid(String name, Boolean alive) { this.name = name; this.alive = alive; } @Column(name = "name") public String getName() { return name; } @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "unicorn_id") public Unicorn getUnicorn() { return unicorn; } @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "droid") public List<Cupcake> getCupcakes() { return cupcakes; } @Column(name = "alive") public Boolean getAlive() { return alive; } }
Cupcake:
@Entity @Table(name = "cupcakes") @Setter @EqualsAndHashCode(callSuper = false) @AllArgsConstructor @NoArgsConstructor public class Cupcake extends AbstractEntity { private Filling filling; private Droid droid; @Column(name = "filling") public Filling getFilling() { return filling; } @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "droid_id") public Droid getDroid() { return droid; } public Cupcake(Filling filling) { this.filling = filling; } }
Nous convertirons ces entités en DTO. Il existe au moins deux approches pour convertir les dépendances d'une entité en DTO. L'une implique de ne sauvegarder que l'ID au lieu de l'entité, mais ensuite chaque entité de la dépendance, si nécessaire, nous tirerons en plus l'ID. La deuxième approche consiste à garder le DTO dépendant. Donc, dans la première approche, nous convertirions les droïdes de liste en droïdes de liste (nous stockons uniquement les ID dans la nouvelle liste), et dans la deuxième approche, nous enregistrerons dans les droïdes de liste.
DTOParent abstrait:
@Data public abstract class AbstractDto implements Serializable { private Long id; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") LocalDateTime created; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") LocalDateTime updated; }
UnicornDto:
@EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class UnicornDto extends AbstractDto { private String name; private List<DroidDto> droids; private String color; }
DroidDto:
@EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class DroidDto extends AbstractDto { private String name; private List<CupcakeDto> cupcakes; private UnicornDto unicorn; private Boolean alive; }
CupcakeDto:
@EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class CupcakeDto extends AbstractDto { private String filling; private DroidDto droid; }
Pour affiner le mappeur selon nos besoins, nous devrons créer notre propre classe wrapper et redéfinir la logique de mappage des collections. Pour ce faire, nous créons une classe de composants UnicornMapper, y mappons automatiquement notre mappeur et redéfinissons les méthodes dont nous avons besoin.
La version la plus simple de la classe wrapper ressemble à ceci:
@Component public class UnicornMapper { @Autowired private ModelMapper mapper; @Override public Unicorn toEntity(UnicornDto dto) { return Objects.isNull(dto) ? null : mapper.map(dto, Unicorn.class); } @Override public UnicornDto toDto(Unicorn entity) { return Objects.isNull(entity) ? null : mapper.map(entity, UnicornDto.class); } }
Maintenant, il nous suffit de câbler automatiquement notre mappeur dans un service et de tirer en utilisant les méthodes toDto et toEntity. Le mappeur transformera les entités trouvées dans l'objet en DTO, DTO - en entités.
@Service public class UnicornServiceImpl implements UnicornService { private final UnicornRepository repository; private final UnicornMapper mapper; @Autowired public UnicornServiceImpl(UnicornRepository repository, UnicornMapper mapper) { this.repository = repository; this.mapper = mapper; } @Override public UnicornDto save(UnicornDto dto) { return mapper.toDto(repository.save(mapper.toEntity(dto))); } @Override public UnicornDto get(Long id) { return mapper.toDto(repository.getOne(id)); } }
Mais si nous essayons de convertir quelque chose de cette manière, puis d'appeler, par exemple, toString, nous obtiendrons une StackOverflowException, et voici pourquoi: UnicornDto contient la liste DroidDto, qui contient UnicornDto, qui contient DroidDto, et ainsi de suite jusqu'à ce moment jusqu'à épuisement de la mémoire de la pile. Par conséquent, pour les dépendances inverses, j'utilise généralement non UnicornDto unicorn, mais Long unicornId. De cette façon, nous restons en contact avec Unicorn, mais nous coupons la dépendance cyclique. Corrigeons nos DTO afin qu'au lieu de DTO inversés, ils stockent les ID de leurs dépendances.
@EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class DroidDto extends AbstractDto { ...
et ainsi de suite.
Mais maintenant, si nous appelons DroidMapper, nous obtenons unicornId == null. En effet, ModelMapper ne peut pas déterminer exactement ce qu'est Long. Et ça ne le dérange pas. Et nous devrons affiner les mappeurs nécessaires pour leur apprendre à mapper des entités dans les ID.
Nous rappelons qu'avec chaque bac après son initialisation, vous pouvez travailler manuellement.
@PostConstruct public void setupMapper() { mapper.createTypeMap(Droid.class, DroidDto.class) .addMappings(m -> m.skip(DroidDto::setUnicornId)).setPostConverter(toDtoConverter()); mapper.createTypeMap(DroidDto.class, Droid.class) .addMappings(m -> m.skip(Droid::setUnicorn)).setPostConverter(toEntityConverter()); }
Dans @PostConstruct, nous définirons les règles dans lesquelles nous indiquerons les champs que le mappeur ne doit pas toucher, car pour eux, nous déterminerons la logique par nous-mêmes. Dans notre cas, il s'agit à la fois de la définition de unicornId dans le DTO et de la définition de Unicorn en substance (puisque le mappeur ne sait pas non plus quoi faire avec Long unicornId).
TypeMap - c'est la règle dans laquelle nous spécifions toutes les nuances de la cartographie et définissons également le convertisseur. Nous avons souligné que pour convertir de Droid en DroidDto, nous sautons setUnicornId, et en conversion inverse, nous passons setUnicorn. Nous allons tous convertir dans le convertisseur toDtoConverter () pour UnicornDto et dans toEntityConverter () pour Unicorn. Nous devons décrire ces convertisseurs dans notre composant.
Le post-convertisseur le plus simple ressemble à ceci:
Converter<UnicornDto, Unicorn> toEntityConverter() { return MappingContext::getDestination; }
Nous devons étendre ses fonctionnalités:
public Converter<UnicornDto, Unicorn> toEntityConverter() { return context -> { UnicornDto source = context.getSource(); Unicorn destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; }
On fait de même avec le convertisseur inverse:
public Converter<Unicorn, UnicornDto> toDtoConverter() { return context -> { Unicorn source = context.getSource(); UnicornDto destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; }
En fait, nous insérons simplement une méthode supplémentaire dans chaque post-convertisseur, dans laquelle nous écrivons notre propre logique pour les champs manquants.
public void mapSpecificFields(Droid source, DroidDto destination) { destination.setUnicornId(Objects.isNull(source) || Objects.isNull(source.getId()) ? null : source.getUnicorn().getId()); } void mapSpecificFields(DroidDto source, Droid destination) { destination.setUnicorn(unicornRepository.findById(source.getUnicornId()).orElse(null)); }
Lors du mappage dans DTO, nous définissons l'ID d'entité. Lors du mappage dans DTO, nous obtenons l'entité du référentiel par ID.
Et c'est tout.
J'ai montré le minimum nécessaire pour commencer à travailler avec modelmapper et je n'ai pas particulièrement refactorisé le code. Si vous, lecteur, avez quelque chose à ajouter à mon article, je serai heureux d'entendre des critiques constructives.
Le projet peut être consulté ici:
Projet sur GitHub.Les fans de code propre ont probablement déjà vu l'opportunité de conduire de nombreux composants de code dans une abstraction. Si vous êtes l'un d'entre eux, je suggère sous chat.
Augmenter le niveau d'abstractionPour commencer, nous définissons une interface pour les méthodes de base de la classe wrapper.
public interface Mapper<E extends AbstractEntity, D extends AbstractDto> { E toEntity(D dto); D toDto(E entity); }
Nous en héritons une classe abstraite.
public abstract class AbstractMapper<E extends AbstractEntity, D extends AbstractDto> implements Mapper<E, D> { @Autowired ModelMapper mapper; private Class<E> entityClass; private Class<D> dtoClass; AbstractMapper(Class<E> entityClass, Class<D> dtoClass) { this.entityClass = entityClass; this.dtoClass = dtoClass; } @Override public E toEntity(D dto) { return Objects.isNull(dto) ? null : mapper.map(dto, entityClass); } @Override public D toDto(E entity) { return Objects.isNull(entity) ? null : mapper.map(entity, dtoClass); } Converter<E, D> toDtoConverter() { return context -> { E source = context.getSource(); D destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } Converter<D, E> toEntityConverter() { return context -> { D source = context.getSource(); E destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } void mapSpecificFields(E source, D destination) { } void mapSpecificFields(D source, E destination) { } }
Les post-convertisseurs et les méthodes de remplissage de champs spécifiques peuvent y être envoyés en toute sécurité. Créez également deux objets de type Class et un constructeur pour les initialiser:
private Class<E> entityClass; private Class<D> dtoClass; AbstractMapper(Class<E> entityClass, Class<D> dtoClass) { this.entityClass = entityClass; this.dtoClass = dtoClass; }
Maintenant, la quantité de code dans DroidMapper est réduite à ce qui suit:
@Component public class DroidMapper extends AbstractMapper<Droid, DroidDto> { private final ModelMapper mapper; private final UnicornRepository unicornRepository; @Autowired public DroidMapper(ModelMapper mapper, UnicornRepository unicornRepository) { super(Droid.class, DroidDto.class); this.mapper = mapper; this.unicornRepository = unicornRepository; } @PostConstruct public void setupMapper() { mapper.createTypeMap(Droid.class, DroidDto.class) .addMappings(m -> m.skip(DroidDto::setUnicornId)).setPostConverter(toDtoConverter()); mapper.createTypeMap(DroidDto.class, Droid.class) .addMappings(m -> m.skip(Droid::setUnicorn)).setPostConverter(toEntityConverter()); } @Override public void mapSpecificFields(Droid source, DroidDto destination) { destination.setUnicornId(getId(source)); } private Long getId(Droid source) { return Objects.isNull(source) || Objects.isNull(source.getId()) ? null : source.getUnicorn().getId(); } @Override void mapSpecificFields(DroidDto source, Droid destination) { destination.setUnicorn(unicornRepository.findById(source.getUnicornId()).orElse(null)); } }
Un mappeur sans champs spécifiques semble généralement simple:
@Component public class UnicornMapper extends AbstractMapper<Unicorn, UnicornDto> { @Autowired public UnicornMapper() { super(Unicorn.class, UnicornDto.class); } }