Dopo la creazione di una entity sono necessari i seguenti passaggi:
- Creazione delle classi per la gestione degli eventi di createUpdate e delete
- Creazione layer di business tra resource e service
- Creazione resource aggiuntive per la ripubblicazione di alcuni eventi
- Creazione delle entity aggregate
- Creazione scodatori per popolare gli aggregate
- Modifica del puntamento delle resource di lettura dalle entity alle aggregate
1. Creazione delle classi per la gestione degli eventi di createUpdate e delete
ApiGest trasmette un evento ad un server rabitMQ per ogni azione svolta sulle entity, come la createUpdate o delete. Per poter creare tale trasmissione vanno create le classi nel pacchetto event.publisher come dal seguente esempio:
import org.springframework.cloud.stream.annotation.Output; import org.springframework.messaging.MessageChannel; public interface BuyerPublishChannel { String CREATE_UPDATE_BUYER_OUTPUT = "createUpdateBuyer-Output"; @Output(BuyerPublishChannel.CREATE_UPDATE_BUYER_OUTPUT) MessageChannel createUpdateBuyerChannel(); String DELETE_BUYER_OUTPUT = "deleteBuyer-Output"; @Output(BuyerPublishChannel.DELETE_BUYER_OUTPUT) MessageChannel deleteBuyerChannel(); }
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.handler.annotation.SendTo; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.eneasys.apigest.ucp_jwt.AigUcpHolder; import com.eneasys.apigest.contexts_apps.commerce.service.dto.BuyerDTO; @EnableBinding(BuyerPublishChannel.class) public class BuyerPublishSender { private final Logger log = LoggerFactory.getLogger(BuyerPublishSender.class); @Autowired private BuyerPublishChannel buyerPublishChannel; @SendTo(BuyerPublishChannel.CREATE_UPDATE_BUYER_OUTPUT) public void publishCreateUpdate(final BuyerDTO buyerDTO) { if(buyerDTO != null) { ObjectMapper objectMapper = new ObjectMapper(); try { objectMapper.registerModule(new JavaTimeModule()); String message = objectMapper.writeValueAsString(buyerDTO); String context = objectMapper.writeValueAsString(AigUcpHolder.getAigUcp().getContext()); buyerPublishChannel.createUpdateBuyerChannel() .send(MessageBuilder.withPayload(message) .setHeader("aigContext", context) .build()); } catch (Exception e) { log.debug("Send createUpdateBuyer on queue error: {}", e.getMessage()); e.printStackTrace(); } } } @SendTo(BuyerPublishChannel.DELETE_BUYER_OUTPUT) public void publishDelete(final Long buyerId) { if(buyerId != null) { ObjectMapper objectMapper = new ObjectMapper(); try { objectMapper.registerModule(new JavaTimeModule()); String message = objectMapper.writeValueAsString(buyerId); String context = objectMapper.writeValueAsString(AigUcpHolder.getAigUcp().getContext()); buyerPublishChannel.deleteBuyerChannel() .send(MessageBuilder.withPayload(message) .setHeader("aigContext", context) .build()); } catch (Exception e) { log.debug("Send deleteBuyerChannel on queue error: {}", e.getMessage()); e.printStackTrace(); } } } }
Inoltre va configurato nell’application.yml l’exchange nel quale scrivere
cloud: stream: bindings: ... createUpdateBuyer-Output: destination: createUpdateBuyerExchange contentType: application/json deleteBuyer-Output: destination: deleteBuyerExchange contentType: application/json
2. Creazione layer di Business
Per ogni entity va creata una classe con annotazione @Service che fungerà come punto unico di accesso al entity service layer (quello generato da jhipster sotto il pacchetto service).
La classe buisness dovrà essere creata sotto il pacchetto business e sarà un interfaccia con metodi uguali al seguente esempio:
public interface BuyerBusinessService { List<BuyerDTO> search(BuyerCriteria criteria, Pageable pageable); List<BuyerDTO> findAll(Pageable pageable); Optional<BuyerDTO> findOne(Long id); BuyerDTO save(BuyerDTO buyerDTO); int republishCreateUpdate(BuyerCriteria criteria, Pageable pageable); void delete(Long id); }
dopo aver creato l’interfaccia creare l’implementazione di default nel pacchetto business.impl.def come nel seguente esempio:
@Service public class BuyerBusinessServiceImpl implements BuyerBusinessService { private final Logger log = LoggerFactory.getLogger(BuyerBusinessServiceImpl.class); private final BuyerService buyerService; private final BuyerQueryService buyerQueryService; private final BuyerPublishSender buyerPublishSender; private final EopooDecorator eopooDecorator; public BuyerBusinessServiceImpl(BuyerService buyerService, BuyerPublishSender buyerPublishSender, EopooDecorator eopooDecorator, BuyerQueryService buyerQueryService) { this.buyerService = buyerService; this.buyerPublishSender = buyerPublishSender; this.eopooDecorator = eopooDecorator; this.buyerQueryService = buyerQueryService; } @Override public List<BuyerDTO> search(BuyerCriteria criteria, Pageable pageable) { log.debug("Request to search buyer"); // Entity Roule List<BuyerDTO> buyerDTOs = buyerQueryService.findByCriteria(criteria, pageable).getContent(); // Decoration Roule buyerDTOs = eopooDecorator.decore(buyerDTOs, BuyerDTO.class); return buyerDTOs; } @Override public List<BuyerDTO> findAll(Pageable pageable) { log.debug("Request to get all buyer"); // Entity Roule List<BuyerDTO> buyerDTOs = buyerService.findAll(pageable); // Decoration Roule buyerDTOs = eopooDecorator.decore(buyerDTOs, BuyerDTO.class); return buyerDTOs; } @Override public Optional<BuyerDTO> findOne(Long id) { log.debug("Request to get buyer: {}", id); // Entity Roule Optional<BuyerDTO> optionalBuyerDTO = buyerService.findOne(id); // Decoration Roule if(optionalBuyerDTO.isPresent()) { BuyerDTO eopooDTO = eopooDecorator.decore(Arrays.asList(optionalBuyerDTO.get()), BuyerDTO.class).get(0); optionalBuyerDTO = Optional.of(eopooDTO); } return optionalBuyerDTO; } private void publishCreateUpdate(BuyerDTO buyerDTO) { // Publish on queue log.debug("Pulish Event of createUpdateBuyer: {}", buyerDTO); buyerPublishSender.publishCreateUpdate(buyerDTO); } @Override public BuyerDTO save(BuyerDTO buyerDTO) { log.debug("Request to save Buyer : {}", buyerDTO); // Entity Roule buyerDTO = buyerService.save(buyerDTO); // Decoration Roule Optional<BuyerDTO> optionalBuyerDTO = findOne(buyerDTO.getId()); // Publish on queue publishCreateUpdate(optionalBuyerDTO.get()); return optionalBuyerDTO.get(); } @Override public int republishCreateUpdate(BuyerCriteria criteria, Pageable pageable) { log.debug("Republish by criteria: {} and pageable: {}", criteria, pageable); int republishedQuantity = 0; List<BuyerDTO> listBuyerDTOs = search(criteria, pageable); if((listBuyerDTOs != null) && (!listBuyerDTOs.isEmpty())) { for(BuyerDTO buyerDTO : listBuyerDTOs) { this.publishCreateUpdate(buyerDTO); } republishedQuantity = listBuyerDTOs.size(); } return republishedQuantity; } @Override public void delete(Long id) { log.debug("Request to delete buyer: {}", id); // Entity Roule buyerService.delete(id); // Publish on queue log.debug("Pulish Event of deleteBuyer: {}", id); buyerPublishSender.publishDelete(id); } }
3. Creazione resource aggiuntive per la ripubblicazione di alcuni eventi
Potrebbe essere necessaro ripubblicare gli eventi di createUpdate in modo che eventuali listner possano ri-aggiornare i propri valori, per far questo si prevede una risorsa REST per fare questo, accessibile solo da admin.
Nella resource della entity aggiungere il seguente metodo:
@GetMapping("/buyers/publish") public ResponseEntity<String> publish(BuyerCriteria criteria, Pageable pageable) { log.debug("REST request to republish by criteria: {} and page: {}", criteria, pageable); // Access Roule UcpPermission.verifyPermission("c6e.admin"); // Execution int republishedQuantity = buyerBusinessService.republishCreateUpdate(criteria, pageable); return new ResponseEntity<String>("" + republishedQuantity, HttpStatus.OK); }
4. Creazione delle entity aggregate
Le entity aggregate sono uno strumento pensato per vari fattori: velocizzare la lettura, ricerche approfondite, orientare le letture verso no-sql.
Per creare gli aggregate è necessario iniziare duplicando ogni entity esistente ma anteponendo il termine Aggregate al nome della entity, ed anteponendo agg_ al nome della tabella. ad esempio:
entity Buyer (a_buyer) { } entity AggregateBuyer (agg_a_buyer) { }
Successivamente per ogni campo dell’entity va anteposto il nome dell’entity (facendo attenzione a rispettare sempre il formato camelBack) ad esempio:
entity Buyer (a_buyer) { eopooCode String required, confirmation Boolean required, statusNote String, } entity AggregateBuyer (agg_a_buyer) { buyerEopooCode String, buyerConfirmation Boolean, buyerStatusNote String, }
Nell’aggregate va inoltre aggiunto un campo iniziale nel quale verrà inserito l’id dell’entity originale, che si chiamerà con nome entity ed in aggiunta ID come ad esempio: buyerID Integer required
Il campo dell’id dell’entity principale sarà l’unico ad essere required!
A questo punto vanno aggiunti tutti i campi delle entity parent sempre anteponendo al nome del campo il nome dell’entity di riferimento, come di seguito:
entity AggregateBuyer (agg_a_buyer) { buyerID Integer required, buyerEopooCode String, buyerConfirmation Boolean, buyerStatusNote String, sellerID Integer, sellerEopooCode String, sellerName String, }
Una vota aggiunte le entity aggregate nel JDL princuipale nel branch entity_creation generare il codice come indicato nell’apposita guida.
Come ultimo passaggio di preparazione degli aggregate vanno creati i mapper, tra entity ed aggregate, queste classi mapper useranno MapStruct e dovranno essere create nel pacchetto aggregate.mapper come nel seguente esempio:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import com.eneasys.apigest.contexts_apps.commerce.service.dto.AggregateBuyerDTO; import com.eneasys.apigest.contexts_apps.commerce.service.dto.BuyerDTO; @Mapper(componentModel = "spring", uses = {}) public interface BuyerDTOMapper { @Mapping(source = "buyerID", target = "id") @Mapping(source = "buyerConfirmation", target = "confirmation") @Mapping(source = "buyerStatusNote", target = "statusNote") @Mapping(source = "buyerEopooCode", target = "eopooCode") @Mapping(source = "sellerID", target = "sellerId") @Mapping(source = "sellerID", target = "seller.id") @Mapping(source = "sellerName", target = "seller.name") @Mapping(source = "sellerEopooCode", target = "seller.eopooCode") BuyerDTO toDto(AggregateBuyerDTO aggregateBuyerDTO); @Mapping(source = "id", target = "buyerID") @Mapping(source = "confirmation", target = "buyerConfirmation") @Mapping(source = "statusNote", target = "buyerStatusNote") @Mapping(source = "eopooCode", target = "buyerEopooCode") @Mapping(source = "sellerId", target = "sellerID") @Mapping(source = "seller.name", target = "sellerName") @Mapping(source = "seller.eopooCode", target = "sellerEopooCode") AggregateBuyerDTO toAggregate(BuyerDTO buyerDTO); }
5. Creazione scodatori per popolare gli aggregate
Una volta creati gli aggregate e creati gli eventi di createUpdate e delete è possible unire le due cose, in modo da popolare e tenere aggiornati i dati degli aggregate.
Come primo passo vanno creati degli scodatori, per far questo vanno create delle classi nel pacchetto event.aggregate_consumer come nel seguente esempio:
import org.springframework.cloud.stream.annotation.Input; import org.springframework.messaging.SubscribableChannel; public interface BuyerAggregateChannel { String CREATE_UPDATE_BUYER = "createUpdateBuyer-createUpdateBuyerAggregate-Input"; @Input(BuyerAggregateChannel.CREATE_UPDATE_BUYER) SubscribableChannel createUpdateBuyerUpdateBuyerAggregateChannel(); String DELETE_BUYER = "deleteBuyer-deleteBuyerAggregate-Input"; @Input(BuyerAggregateChannel.DELETE_BUYER) SubscribableChannel deleteBuyerDeleteBuyerAggregateChannel(); String CREATE_UPDATE_SELLER = "createUpdateSeller-createUpdateBuyerAggregate-Input"; @Input(BuyerAggregateChannel.CREATE_UPDATE_SELLER) SubscribableChannel createUpdateSellerUpdateBuyerAggregateChannel(); }
Va inoltre configurato nel file application.yml a quali Exchange collegare i listner e come chiamare le code
cloud: stream: bindings: ... createUpdateBuyer-createUpdateBuyerAggregate-Input: destination: createUpdateBuyerExchange group: createUpdateBuyerAggregateQueue contentType: application/json createUpdateSeller-createUpdateBuyerAggregate-Input: destination: createUpdateSellerExchange group: createUpdateBuyerAggregateQueue contentType: application/json deleteBuyer-deleteBuyerAggregate-Input: destination: deleteBuyerExchange group: deleteBuyerAggregateQueue contentType: application/json
Da notare che nel presente esempio buyer essendo figlio di seller e quindi contenedo informazioni del seller nell’aggreagate si sottoscrive al createUpdare di tale entity per poter mantenere aggiornate le informazioni relative al seller nell’aggreagte del buyer. Questo necessiterà di gestire tale evento come vedremo di seguito.
La seconda classe da creare per la gestione dell’evento è la seguente:
import java.util.List; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.annotation.StreamListener; import org.springframework.messaging.Message; import com.eneasys.apigest.contexts_apps.commerce.service.AggregateBuyerService; import com.eneasys.apigest.contexts_apps.commerce.service.dto.AggregateBuyerDTO; import com.eneasys.apigest.contexts_apps.commerce.service.dto.BuyerDTO; import com.eneasys.apigest.contexts_apps.commerce.service.dto.SellerDTO; import com.eneasys.apigest.contexts_apps.commerce.service.mapper.BuyerDTOMapper; import com.eneasys.apigest.contexts_apps.commerce.utils.ParseMessageUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; @EnableBinding(BuyerAggregateChannel.class) public class BuyerAggregateListner { private final Logger log = LoggerFactory.getLogger(BuyerAggregateListner.class); @Autowired private ParseMessageUtils parseMessageUtils; @Autowired private AggregateBuyerService aggregateBuyerService; @Autowired private BuyerDTOMapper buyerDTOMapper; @StreamListener(BuyerAggregateChannel.CREATE_UPDATE_BUYER) public void createUpdateBuyer(final Message<?> message) throws JsonMappingException, JsonProcessingException { try { log.debug("----->>> BuyerAggregateListner - Aspetta buyer create update: " +message.getPayload()); BuyerDTO buyerDTO = parseMessageUtils.parseMessage(BuyerDTO.class, message); AggregateBuyerDTO aggregateBuyerDTO = buyerDTOMapper.toAggregate(buyerDTO); Optional<AggregateBuyerDTO> aggregateBuyerOptional = aggregateBuyerService.findByBuyerId(buyerDTO.getId().intValue()); if(aggregateBuyerOptional.isPresent()) { aggregateBuyerService.updateBuyer(aggregateBuyerDTO); } else { aggregateBuyerService.save(aggregateBuyerDTO); } } catch (Exception e) { log.debug(e.getMessage()); } } @StreamListener(BuyerAggregateChannel.CREATE_UPDATE_SELLER) public void createUpdateSeller(final Message<?> message) { try { log.debug("----->>> BuyerAggregateListner - Aspetta seller create update: " +message.getPayload()); SellerDTO sellerDTO = parseMessageUtils.parseMessage(SellerDTO.class, message); BuyerDTO buyerDTO = new BuyerDTO(); buyerDTO.setSeller(sellerDTO); AggregateBuyerDTO aggregateBuyerDTO = buyerDTOMapper.toAggregate(buyerDTO); Optional<List<AggregateBuyerDTO>> listAggregateBuyerOptional = aggregateBuyerService.findBySellerId(sellerDTO.getId().intValue()); if(listAggregateBuyerOptional.isPresent()) { aggregateBuyerService.updateSeller(aggregateBuyerDTO); } } catch (Exception e) { log.debug(e.getMessage()); } } @StreamListener(BuyerAggregateChannel.DELETE_BUYER) public void deleteBuyerDeleteBuyerAggregateChannel(final Message<?> message) { try { log.debug("----->>> BuyerAggregateListner - Aspetta Buyer Delete: " +message.getPayload()); Long buyerId = parseMessageUtils.parseMessage(Long.class, message); if(buyerId != null) { Optional<AggregateBuyerDTO> aggregateBuyerOptional = aggregateBuyerService.findByBuyerId(buyerId.intValue()); if(aggregateBuyerOptional.isPresent()) { AggregateBuyerDTO aggregateBuyerDTO = aggregateBuyerOptional.get(); aggregateBuyerService.delete(aggregateBuyerDTO.getId()); } } } catch (Exception e) { log.debug(e.getMessage()); } } }
Questa classe necessita di vari metodi nel layer di entity degli aggregate da implementare nel service come nel seguente esempio:
public interface AggregateBuyerService { ... public Optional<AggregateBuyerDTO> findByBuyerId(Integer buyerId); public void updateBuyer(AggregateBuyerDTO aggregateBuyerDTO); public Optional<List<AggregateBuyerDTO>> findBySellerId(Integer sellerId); public void updateSeller(AggregateBuyerDTO aggregateBuyerDTO); }
public class AggregateBuyerServiceImpl implements AggregateBuyerService { ... @Override @Transactional(readOnly = true) public Optional<AggregateBuyerDTO> findByBuyerId(Integer buyerId) { log.debug("Request to get all AggregateBuyers by buyer: {}", buyerId); return aggregateBuyerRepository.findByBuyerID(buyerId) .map(aggregateBuyerMapper::toDto); } @Override public void updateBuyer(AggregateBuyerDTO aggregateBuyerDTO) { log.debug("Request to updateBuyer in AggregateBuyer : {}", aggregateBuyerDTO); aggregateBuyerRepository.updateBuyer(aggregateBuyerDTO.getBuyerID(), aggregateBuyerDTO.getBuyerEopooCode(), aggregateBuyerDTO.getBuyerStatusNote(), aggregateBuyerDTO.isBuyerConfirmation(), aggregateBuyerDTO.getSellerID(), aggregateBuyerDTO.getSellerEopooCode(), aggregateBuyerDTO.getSellerName()); } @Override @Transactional(readOnly = true) public Optional<List<AggregateBuyerDTO>> findBySellerId(Integer sellerId) { log.debug("Request to get all AggregateBuyers by seller: {}", sellerId); return aggregateBuyerRepository.findBySellerID(sellerId) .map(aggregateBuyerMapper::toDto); } @Override public void updateSeller(AggregateBuyerDTO aggregateBuyerDTO) { log.debug("Request to updateSeller in AggregateBuyer: {}", aggregateBuyerDTO); aggregateBuyerRepository.updateSeller(aggregateBuyerDTO.getSellerID(), aggregateBuyerDTO.getSellerEopooCode(), aggregateBuyerDTO.getSellerName()); } }
A sua volta il layer di entity necessita modifiche nel layer di repository come di seguito:
public interface AggregateBuyerRepository extends JpaRepository<AggregateBuyer, Long>, JpaSpecificationExecutor<AggregateBuyer> { public Optional<AggregateBuyer> findByBuyerID(Integer buyerId); @Modifying @Query("update AggregateBuyer u " + " set u.buyerEopooCode = :buyerEopooCode, " + " u.buyerStatusNote = :buyerStatusNote, " + " u.buyerConfirmation = :buyerConfirmation," + " u.sellerID = :sellerID," + " u.sellerEopooCode = :sellerEopooCode," + " u.sellerName = :sellerName" + " where u.buyerID = :buyerID") public void updateBuyer(@Param(value = "buyerID") Integer buyerID, @Param(value = "buyerEopooCode") String buyerEopooCode, @Param(value = "buyerStatusNote") String buyerStatusNote, @Param(value = "buyerConfirmation") Boolean buyerConfirmation, @Param(value = "sellerID") Integer sellerID, @Param(value = "sellerEopooCode") String sellerEopooCode, @Param(value = "sellerName") String sellerName); public Optional<List<AggregateBuyer>> findBySellerID(Integer sellerId); @Modifying @Query("update AggregateBuyer u " + " set u.sellerEopooCode = :sellerEopooCode, " + " u.sellerName = :sellerName " + " where u.sellerID = :sellerID") public void updateSeller(@Param(value = "sellerID") Integer sellerID, @Param(value = "sellerEopooCode") String sellerEopooCode, @Param(value = "sellerName") String sellerName); }
Una volta fatto questo quando l’appicativo riceve un messaggio di createUpdate o di delete di un entità tale azione verrà eseguita anche sull’aggregate corrispondente.
6. Modifica del puntamento delle resource di lettura dalle entity alle aggregate
Quando i dati delle entity saranno anche inserite sulle ripettive aggregate si potrà far leggere le informazioni dall’aggregate in modo da velocizzare la lettura ed estendere le funzionalità di filtro a molti più campi.
Per poter fare questo bisognerà cambiare il puntamento nel layer di resource REST in modo da far leggere dagli aggregate e non dalle entity, lo si fa come di seguito in esempio:
public class BuyerResource { ... @GetMapping("/buyers") public ResponseEntity<List<BuyerDTO>> getAllBuyers(AggregateBuyerCriteria criteria, Pageable pageable) { log.debug("REST request to get Buyers by criteria: {}", criteria); // Access Roule UcpPermission.verifyPermission("c6e.admin"); // Execution Page<AggregateBuyerDTO> pageAggregateBuyerDTO = aggregateBuyerQueryService.findByCriteria(criteria, pageable); // Trasformation Roule Page<BuyerDTO> pageBuyerDTO = pageAggregateBuyerDTO.map(buyerDTOMapper::toDto); // Return HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), pageBuyerDTO); return ResponseEntity.ok().headers(headers).body(pageBuyerDTO.getContent()); } ... @GetMapping("/buyers/count") public ResponseEntity<Long> countBuyers(AggregateBuyerCriteria criteria) { log.debug("REST request to count Buyers by criteria: {}", criteria); // Access Roule UcpPermission.verifyPermission("c6e.admin"); // Execution return ResponseEntity.ok().body(aggregateBuyerQueryService.countByCriteria(criteria)); } ... @GetMapping("/buyers/{id}") public ResponseEntity<BuyerDTO> getBuyer(@PathVariable Long id) { log.debug("REST request to get Buyer: {}", id); // Access Roule UcpPermission.verifyPermission("c6e.admin"); // Execution Optional<BuyerDTO> optionalBuyerDTO = aggregateBuyerService.findByBuyerId(new Integer(id.intValue())).map(buyerDTOMapper::toDto); // Return return ResponseUtil.wrapOrNotFound(optionalBuyerDTO); } ... }