# Ús de DTOs. MapStruct. Controlador API REST
### Patró DTO
El patró DTO (Data Transfer Object) té com a objectiu definir i utilitzar objectes plans (POJOs) que encapsulen un conjunt d'atributs relacionats amb una o més entitats persistents.
La idea principal i el seu principal avantatge és reduir el nombre de crides a les capes de servei (o altres capes), optimitzant així la transferència de dades, tant en la recuperació com en la persistència de la informació.
Prenem com exemple el projecte de llibreria treballat en anteriors activitats. En aquest projecte, el model de domini venia representat per una classe anomenada Llibre, la qual tenia els següents atributs:
```
import java.time.LocalDate;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "books")
public class Llibre {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id_llibre;
@Column(unique = true, nullable = false)
private String titol;
@Column(nullable = false)
private String autor;
@Column(nullable = false)
private String editorial;
@Column(nullable = false)
private LocalDate datapublicacio;
@Column(nullable = false)
private String tematica;
@Column(unique = true, nullable = false)
private String isbn;
}
```
Imaginem que no necessitem que la id_llibre hi sigui traslladada cap al controlador. Una solució passa per crear un DTO, el qual anomenarem LlibreDTO i que tindrà la següent forma:
```
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LlibreDTO {
private String titol;
private String autor;
private String editorial;
private LocalDate datapublicacio;
private String tematica;
private String isbn;
}
```
Des d'un punt de vista de Model Vista Controlador, el Model vindrà representat en endavant per aquesta classe DTO i no per l'entitat.
Suposem ara que el nostre model de domini incorpora una classe Usuari amb aquests atributs:
```
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "users")
public class Usuaris {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id_usuari;
@Column(unique = true, nullable = false)
private String nom_usuari;
@Column(nullable = false)
private String password;
```
Si bé no s'ha fet al projecte d'exemple que teniu a github, podríem aprofitar i modificar l'anterior DTO per tal d'afegir el nom d'usuari (i per "claredat" es pot canviar el nom si es vol):
```
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LlibreDTO {
private String titol;
private String autor;
private String editorial;
private LocalDate datapublicacio;
private String tematica;
private String isbn;
private String nom_usuari;
}
```
Així s'aprecia com aquest DTO agrupa atributs de diferents entitats.
### Mappers. MapStruct
El següent punt és dur a terme la correspondència entre els atributs presents a les entitats (és a dir al model de domini) i als DTOs.
Hi ha diferents tipus d'approach. Un és el proposat des de [Baeldung - DTO pattern](https://www.baeldung.com/java-dto-pattern) i un altre és fer servir [MapStruct](https://mapstruct.org/).
D'acord a la definició oficial: MapStruct és un generador de codi que simplifica (molt) la implementació de mapatges entre tipus de beans de Java, basant-se en un enfocament de **conveni sobre configuració**. Aquest enfocament ens diu que un sistema ha de funcionar amb un mínim de configuració explícita i de fet ja l'hem aplicat als repositoris, quan hem fet un extends de JPARepository (o de qualsevol de les altres) i no hem detallat de forma explícita els mètodes del CRUD.
#### Interfície de mapatge
Fent servir MapStruct tindrem en aquest cas una interfície anomenada **LlibreMapper**, la qual tindrà el següent codi que explicarem tot seguit:
```
import java.util.Set;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
import com.iticbcn.webapp.mywebapp.DTO.LlibreDTO;
import com.iticbcn.webapp.mywebapp.DomainModel.Llibre;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface LlibreMapper {
@Mapping(target="titol", source="llibre.titol")
@Mapping(target="autor", source="llibre.autor")
@Mapping(target="editorial", source="llibre.editorial")
@Mapping(target = "datapublicacio", source = "llibre.datapublicacio")
@Mapping(target = "tematica", source = "llibre.tematica")
@Mapping(target = "isbn", source = "llibre.isbn")
LlibreDTO LlibreToLlibreDTO(Llibre llibre);
@Mapping(target="titol", source="llibreDTO.titol")
@Mapping(target="autor", source="llibreDTO.autor")
@Mapping(target="editorial", source="llibreDTO.editorial")
@Mapping(target = "datapublicacio", source = "llibreDTO.datapublicacio")
@Mapping(target = "tematica", source = "llibreDTO.tematica")
@Mapping(target = "isbn", source = "llibreDTO.isbn")
Llibre LlibreDTOToLlibre(LlibreDTO llibreDTO);
Set<Llibre> LlibresDTOToLlibres(Set<LlibreDTO> llibresDTO);
Set<LlibreDTO> LlibresToLlibresDTO(Set<Llibre> llibres);
}
```
Les anotacions rellevants com veieu son `@Mapper` i `@Mapping`:
* `@Mapper` serveix per indicar que la interfície LlibreMapper és una interfície de mapatge i fer que el processador de MapStruct entri en acció durant la compilació.
* `@Mapping` serveix per establir la correspondència entre els camps d'origen (source) i destí (target), especialment quan aquests tenen noms diferents. En el nostre cas no era necessari fer-les servir ja que els noms dels atributs al DTO i a les entitats és el mateix, però les hem fet servir per il·lustrar com s'utilitza.
Fixeu-vos que els mètodes que finalment s'encarreguen de fer els enllaços són:
* `LlibreDTO LlibreToLlibreDTO(Llibre llibre)`:
* Retorna un DTO de Llibre i rep com a paràmetre un objecte de l'entitat Llibre. Fa doncs la conversió de **Llibre** a **LlibreDTO**.
* Normalment l'emprarem quan des de les entitats s'hagi de retornar alguna informació cap als controladors, sent LlibreDTO el mitjancer (per exemple una consulta feta des del controlador cap a la taula llibres on es retorna un sol llibre).
* `Llibre LlibreDTOToLlibre(LlibreDTO llibreDTO)`:
* Retorna un Llibre i rep com a paràmetre un DTO de Llibre. Fa doncs la conversió de **LlibreDTO** a **Llibre**.
* Normalment l'emprarem quan a partir de la informació continguda al DTO volguem fer alguna acció com persistir una nova entitat o modificar/esborrar una d'existent.
* Per tal de tractar els conjunts de Llibres i LlibreDTO i les seves respectives conversions tenim `Set<Llibre> LlibresDTOToLlibres(Set<LlibreDTO> llibresDTO)` i `Set<LlibreDTO> LlibresToLlibresDTO(Set<Llibre> llibres)`. És perfectament possible utilitzar altres tipus d'estructures iteratives com són llistes (List) o qualsevol altra en comptes del Set.
Per últim:
* La política `unmappedTargetPolicy = ReportingPolicy.IGNORE` serveix per no obligar a mapejar determinats camps entre entitats i DTOs relacionats al mapper.
* Per tal que el mapper pugui ser injectat als serveis hi afegirem `componentModel = "spring"`. MapStruct no és una API exclussiva de Spring o SpringBoot i es pot fer servir per fora d'aquests frameworks.
#### Dependències de MapStruct
Per tal de fer funcionar MapStruct cal afegir el següent al pom.xml. D'una banda afegirem la següent dependència:
```
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.3</version>
</dependency>
```
La qual hem obtingut directament de la pàgina de [maven central repository](https://mvnrepository.com/artifact/org.mapstruct/mapstruct)
Per altra banda afegirem a la secció del build i més específicament a l'annotationProcessorPaths el següent fragment:
```
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.3</version>
</path>
```
Aquest fragment el que farà és que durant la compilació, MapStruct pugui generar automàticament el codi dels mapejos entre entitats i DTOs. Sense aquest processor, no es generaria la classe d’implementació i s'obtindrien errors conforme no s'ha creat implementació pel Mapper de llibre (LlibreMapper).
### Canvis al servei
En endavant els serveis hauran d'utilitzar els DTOs i els mappers en comptes de fer servir les entitats del model de domini. Així, la interfície de servei passarà a ser com segueix:
```
import java.util.Optional;
import java.util.Set;
//reemplaçar per l'import corresponent a la carpeta on tinguem el DTO
import com.iticbcn.webapp.mywebapp.DTO.LlibreDTO;
public interface LlibreService {
Set<LlibreDTO> findAll();
LlibreDTO findByTitol(String titol);
LlibreDTO findByTitolAndEditorial(String titol, String Editorial);
void save(LlibreDTO llibreDTO);
Optional<LlibreDTO> findByIdLlibre(Long idLlibre);
}
```
Fixeu-vos que s'ha canviat la classe Llibre per la classe LlibreDTO.
En conseqüència el servei que implementa aquesta interfície s'ha d'adaptar a aquests canvis d'aquesta forma:
```
import java.util.Optional;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
//reemplaçar pels imports corresponents a les carpetes on tinguem repositoris,
//mappers, model de domini i DTOs
import com.iticbcn.webapp.mywebapp.DTO.LlibreDTO;
import com.iticbcn.webapp.mywebapp.DomainModel.Llibre;
import com.iticbcn.webapp.mywebapp.Repositories.LlibreRepository;
import com.iticbcn.webapp.mywebapp.Mappers.LlibreMapper;
@Service
public class LlibreServiceImpl implements LlibreService{
private final LlibreRepository llibreRepository;
private final LlibreMapper llibreMapper;
//injectem per constructor
@Autowired
public LlibreServiceImpl(LlibreRepository llibreRepository, LlibreMapper llibreMapper){
this.llibreRepository = llibreRepository;
this.llibreMapper = llibreMapper;
}
@Override
public Set<LlibreDTO> findAll() {
Set<Llibre> llibres = llibreRepository.findAll();
return llibreMapper.LlibresToLlibresDTO(llibres);
}
@Override
public LlibreDTO findByTitol(String titol) {
Llibre llibre = llibreRepository.findByTitol(titol);
return llibreMapper.LlibreToLlibreDTO(llibre);
}
@Override
public LlibreDTO findByTitolAndEditorial(String titol, String Editorial) {
Llibre llibre = llibreRepository.findByTitolAndEditorial(titol,Editorial);
return llibreMapper.LlibreToLlibreDTO(llibre);
}
@Override
public void save(LlibreDTO llibreDTO) {
Llibre llibre = llibreMapper.LlibreDTOToLlibre(llibreDTO);
llibreRepository.save(llibre);
}
public Optional<LlibreDTO> convertirLlibre(Optional<Llibre> llibre) {
return llibre.map(llibreMapper::LlibreToLlibreDTO);
}
@Override
public Optional<LlibreDTO> findByIdLlibre(Long idLlibre) {
Optional<Llibre> llibre = llibreRepository.findById(idLlibre);
return convertirLlibre(llibre);
}
}
```
Fixeu-vos en els següents detalls:
* Injectem mapper a més del repositori. En aquest cas fem injecció per constructor.
* Els mètodes **find** com findAll o findByTitol retornaran un LlibreDTO. Sense fer cap canvi al repositori:
* Cridem el mètode corresponent del repositori, el qual ens retornarà un objecte Llibre o un Set de Llibres.
* Farem la conversió de Llibre a LlibreDTO amb el mètode definit a LlibreMapper.
* En el cas del mètode save (i de forma semblant a update o a delete):
* En primer lloc fem la conversió de LlibreDTO cap a Llibre.
* Posterioment cridem al mètode corresponent al repositori (en aquest cas save) per poder persistir l'objecte Llibre.
* En el cas dels Optional com és el cas de findByIdLlibre:
* Emprarem un mètode anomenat **convertirLlibre** el qual fa un map entre llibreMapper i LlibreToLlibreDTO. El seu funcionament és molt simple:
* Si l’Optional conté un Llibre, aplica la conversió a DTO mitjançant `llibreMapper.LlibreToLlibreDTO(llibre)`.
* Si l'Optional està buit (Optional.empty()), simplement retorna un Optional.empty().
* findByIdLlibre segueix utilitzant el mètode findById del repositori i amb seu resultat (un Optional<Llibre>), el que fa és passar-lo a convertirLlibre i fer aquest retorn.
### Adaptacions al controlador web
El fet de fer canvis al servei ens força a fer una sèrie de canvis al controlador.
En el cas de l'opció consulta, fem el canvi de llibres per llibresDTO, ja que és el que ens retornarà el mètode findAll del servei:
```
@GetMapping("/consulta")
public String consulta(@ModelAttribute("users") Usuaris users,Model model) {
Set<LlibreDTO> llibreDTOs = llibreService.findAll();
model.addAttribute("llibreDTOs", llibreDTOs);
return "consulta";
}
```
Pel que fa a l'opció de cerca per id fem algunes adaptacions conseqüència del canvi de Llibre per LlibreDTO:
```
@GetMapping("/cercaid")
public String inputCerca(@ModelAttribute("users") Usuaris users, Model model) {
LlibreDTO llibreDTO = new LlibreDTO();
model.addAttribute("llibreErr", true);
model.addAttribute("message", "");
model.addAttribute("llibreDTO", llibreDTO);
return "cercaid";
}
@PostMapping("/cercaid")
public String cercaId(@ModelAttribute("users") Usuaris users,
@RequestParam(name = "idLlibre", required = false) String idLlibre,
Model model) {
String message = "";
boolean llibreErr = false;
try {
Long idLl = Long.parseLong(idLlibre);
Optional<LlibreDTO> llibreDTO = llibreService.findByIdLlibre(idLl);
if (llibreDTO.isPresent()) {
model.addAttribute("llibreDTO", llibreDTO);
} else {
message = "No hi ha cap llibre amb aquesta id";
llibreErr = true;
}
} catch (Exception e) {
message = "La id de llibre ha de ser un nombre enter";
llibreErr = true;
}
model.addAttribute("message", message);
model.addAttribute("llibreErr",llibreErr);
return "cercaid";
}
```
També cal fer canvis al PostMapping corresponent a quan tornem de la vista d'inserir un nou llibre:
```
@PostMapping("/inserir")
public String inserir(@ModelAttribute("users") Usuaris users, @ModelAttribute LlibreDTO llibreDTO,
Model model) {
llibreService.save(llibreDTO);
Set<LlibreDTO> llibreDTOs = llibreService.findAll();
model.addAttribute("llibreDTOs", llibreDTOs);
return "consulta";
}
```
Fixeu-vos que en aquest cas **save** rep el DTO com a paràmetre, tal i com ja l'havíem adaptat prèviament al servei. Les línies posteriors al save són opcionals i les hem afegit per refrescar la llista de llibres, però perfectament es podria canviar per afegir un missatge de confirmació de la inserció.
A la resta del controlador no cal a priori fer cap canvi.
### Controlador API REST
Al nostre codi hi afegirem un nou controlador per tal que hi puguem disposar d'una sèrie d'endpoints que interaccionin amb la taula de Llibres.
Així el primer que farem és crear un nou component dins la carpeta de controladors a on hi afegirem les anotacions:
```
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
@RequestMapping("/api/llibreria")
public class RESTBookController {
...
}
```
Tot seguit hi injectarem el DTO via setter (es pot fer també per constructor):
```
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.beans.factory.annotation.Autowired;
//Reemplaçar aquests imports pels corresponents a les carpetes
//on hi estiguin els serveis i els DTOs
import com.iticbcn.webapp.mywebapp.DTO.LlibreDTO;
import com.iticbcn.webapp.mywebapp.Services.LlibreService;
@RestController
@RequestMapping("/api/llibreria")
public class RESTBookController {
private LlibreService llibreService;
@Autowired
public void setLlibreService(LlibreService llibreService) {
this.llibreService = llibreService;
}
}
```
Ara implementarem l'endpoint arrel (/) i l'endpoint de consulta (/consulta), el qual hi retorna la llista de llibres:
```
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
//Reemplaçar aquests imports pels corresponents a les carpetes
//on hi estiguin els serveis i els DTOs
import com.iticbcn.webapp.mywebapp.DTO.LlibreDTO;
import com.iticbcn.webapp.mywebapp.Services.LlibreService;
@RestController
@RequestMapping("/api/llibreria")
public class RESTBookController {
private LlibreService llibreService;
@Autowired
public void setLlibreService(LlibreService llibreService) {
this.llibreService = llibreService;
}
@GetMapping("/")
public String iniciar() {
return "API BIBLIOTECA";
}
@GetMapping("/consulta")
public Set<LlibreDTO> consulta() {
Set<LlibreDTO> llibreDTOs = llibreService.findAll();
return llibreDTOs;
}
}
```
Això veurem més endavant que ens retorna un JSON amb la llista de llibres.
Ara anem a afegir l'endpoint corresponent a inserir un nou llibre:
```
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
//Reemplaçar aquests imports pels corresponents a les carpetes
//on hi estiguin els serveis i els DTOs
import com.iticbcn.webapp.mywebapp.DTO.LlibreDTO;
import com.iticbcn.webapp.mywebapp.Services.LlibreService;
@RestController
@RequestMapping("/api/llibreria")
public class RESTBookController {
private LlibreService llibreService;
@Autowired
public void setLlibreService(LlibreService llibreService) {
this.llibreService = llibreService;
}
@GetMapping("/")
public String iniciar() {
return "API BIBLIOTECA";
}
@GetMapping("/consulta")
public Set<LlibreDTO> consulta() {
Set<LlibreDTO> llibreDTOs = llibreService.findAll();
return llibreDTOs;
}
@PostMapping("/inserir")
public String inserir(@RequestBody LlibreDTO llibreDTO) {
llibreService.save(llibreDTO);
return "llibre inserit amb èxit";
}
}
```
Per acabar completarem el controlador amb l'endpoint de cerca per id (aquest ja seria el codi complet del controlador):
```
import java.util.Optional;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
//Reemplaçar aquests imports pels corresponents a les carpetes
//on hi estiguin els serveis i els DTOs
import com.iticbcn.webapp.mywebapp.DTO.LlibreDTO;
import com.iticbcn.webapp.mywebapp.Services.LlibreService;
@RestController
@RequestMapping("/api/llibreria")
public class RESTBookController {
//@Autowired
private LlibreService llibreService;
@Autowired
public void setLlibreService(LlibreService llibreService) {
this.llibreService = llibreService;
}
@GetMapping("/")
public String iniciar() {
return "API BIBLIOTECA";
}
@GetMapping("/consulta")
public Set<LlibreDTO> consulta() {
Set<LlibreDTO> llibreDTOs = llibreService.findAll();
return llibreDTOs;
}
@PostMapping("/inserir")
public String inserir(@RequestBody LlibreDTO llibreDTO) {
llibreService.save(llibreDTO);
return "llibre inserit amb èxit";
}
@GetMapping("/cercaid")
public ResponseEntity<?> cercaId(@RequestParam String idLlibre) {
try {
Long idLl = Long.parseLong(idLlibre);
Optional<LlibreDTO> llibreDTO = llibreService.findByIdLlibre(idLl);
return llibreDTO
.map(llibre -> ResponseEntity.ok(llibre.toString()))
.orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
.body("No hi ha cap llibre amb aquesta id"));
} catch (Exception e) {
return ResponseEntity.badRequest().body("ID no vàlid: ha de ser un número.");
}
}
}
```
En aquest darrer cas com aquest mètode pot retornar un llibre o cap, fem que el retorn es faci amb ResponseEntity. Aquesta és una extensió d'HttpEntity que ens permet afegir l'status code (HttpStatusCode). Fixeu-vos en els tres mètodes que hi porta:
* **.ok** si el servei retorna un llibre.
* **.status** si hem de retornar un codi d'status en concret (en aquest cas NOT_FOUND).
* **.badRequest()** per retornar conforme la petició ha anat malament per un error de validació o per qualsevol altre error.
Un cop arranquem l'API i accedim a l'arrel veurem això:

Si volem interaccionar via swagger amb l'API cal afegir:
1. La dependència corresponent a openapi al pom.xml:
```
<!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.6</version>
</dependency>
```
2. Les següents instruccions al application.properties:
```
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui-custom.html
```
Un cop reiniciem l'API podem accedir al swagger d'aquesta forma: `http://localhost:8080/swagger-ui/index.html`:

La forma de fer servir el swagger és ben senzilla. Cliquem per exemple al GET de consulta:

Ara fem clic al botó "Try it out":

I per últim clic al botó "Execute":

Si tot va bé veurem finalment el retorn:

### Exercicis proposats
1. Quins canvis cal fer al servei i al controlador Rest de l'API per poder disposar d'un endpoint que modifiqui l'editorial d'un llibre?
2. Suposeu que el DTO de Llibre ens ve d'aquesta forma:
```
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LlibreDTO {
private String nomLlibre;
private String autorLlibre;
private String editorialLlibre;
private LocalDate datapublicacioLlib;
private String tematicaLlibre;
private String isbnLlibre;
}
```
Quins canvis cal fer al Mapper?
3. Si en comptes de ser un String, autor fos un objecte de l'entitat Autor (amb relació OneToMany a Llibre), què penseu que cal canviar al Mapper?
NOTA: Suposeu que Autor és una entitat que té una id, el nom i el Set de Llibres escrits (de cada autor).