Spring Boot 3 - Desarrollo API Rest: Aula 4

This commit is contained in:
devfzn 2023-09-10 23:01:50 -03:00
parent fb7ea652e4
commit c391edc321
Signed by: devfzn
GPG Key ID: E070ECF4A754FDB1
14 changed files with 396 additions and 10 deletions

View File

@ -1,18 +1,24 @@
package med.voll.api.controller;
import jakarta.validation.Valid;
import med.voll.api.medico.DatosListadoMedicos;
import med.voll.api.medico.DatosRegistroMedico;
import med.voll.api.medico.Medico;
import med.voll.api.medico.MedicoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/medicos")
public class MedicoController {
// No es recomendable usar @Autowired a nivel de declaración pues genera
// problemas al realizar pruebas unitarias
/* No es recomendable usar @Autowired a nivel de declaración pues genera
problemas al realizar pruebas unitarias */
@Autowired
private MedicoRepository medicoRepository;
@PostMapping
@ -20,4 +26,8 @@ public class MedicoController {
medicoRepository.save(new Medico(datosRegistroMedico));
}
@GetMapping
public Page<DatosListadoMedicos> listadoMedicos(@PageableDefault(size = 5) Pageable paginacion) {
return medicoRepository.findAll(paginacion).map(DatosListadoMedicos::new);
}
}

View File

@ -0,0 +1,31 @@
package med.voll.api.controller;
import jakarta.validation.Valid;
import med.voll.api.paciente.DatosListadoPacientes;
import med.voll.api.paciente.DatosRegistroPaciente;
import med.voll.api.paciente.Paciente;
import med.voll.api.paciente.PacienteRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/pacientes")
public class PacienteController {
/* No es recomendable usar @Autowired a nivel de declaración pues genera
problemas al realizar pruebas unitarias */
@Autowired
private PacienteRepository pacienteRepository;
@PostMapping
public void registrarPaciente(@RequestBody @Valid DatosRegistroPaciente datosRegistroPaciente) {
pacienteRepository.save(new Paciente(datosRegistroPaciente));
}
@GetMapping
public Page<DatosListadoPacientes> listadoPacientes(@PageableDefault(size = 5) Pageable paginacion) {
return pacienteRepository.findAll(paginacion).map(DatosListadoPacientes::new);
}
}

View File

@ -0,0 +1,12 @@
package med.voll.api.medico;
public record DatosListadoMedicos(String nombre, String especialidad, String documento, String email) {
public DatosListadoMedicos (Medico medico) {
this(medico.getNombre(),
medico.getEspecialidad().toString(),
medico.getDocumento(),
medico.getEmail());
}
}

View File

@ -22,7 +22,7 @@ public class Medico {
private String email;
private String telefono;
private String documento;
@Enumerated
@Enumerated(EnumType.STRING)
private Especialidad especialidad;
@Embedded
private Direccion direccion;
@ -30,8 +30,8 @@ public class Medico {
public Medico(DatosRegistroMedico datosRegistroMedico) {
this.nombre = datosRegistroMedico.nombre();
this.email = datosRegistroMedico.email();
this.telefono = datosRegistroMedico.telefono();
this.documento = datosRegistroMedico.documento();
this.telefono = datosRegistroMedico.telefono();
this.especialidad = datosRegistroMedico.especialidad();
this.direccion = new Direccion(datosRegistroMedico.direccion());
}

View File

@ -2,4 +2,5 @@ package med.voll.api.medico;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MedicoRepository extends JpaRepository<Medico, Long> {}
public interface MedicoRepository extends JpaRepository<Medico, Long> {
}

View File

@ -0,0 +1,11 @@
package med.voll.api.paciente;
public record DatosListadoPacientes(String nombre, String documento, String email) {
public DatosListadoPacientes(Paciente medico) {
this(medico.getNombre(),
medico.getDocumento(),
medico.getEmail());
}
}

View File

@ -0,0 +1,16 @@
package med.voll.api.paciente;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import med.voll.api.direccion.DatosDireccion;
public record DatosRegistroPaciente(
@NotBlank String nombre,
@NotBlank @Email String email,
@NotBlank String telefono,
@NotBlank @Pattern(regexp = "\\d{4,6}") String documento,
@NotNull @Valid DatosDireccion direccion
) {}

View File

@ -0,0 +1,35 @@
package med.voll.api.paciente;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import med.voll.api.direccion.Direccion;
@Table(name="pacientes")
@Entity(name="Paciente")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Paciente {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nombre;
private String email;
private String telefono;
private String documento;
@Embedded
private Direccion direccion;
public Paciente(DatosRegistroPaciente datosRegistroPaciente) {
this.nombre = datosRegistroPaciente.nombre();
this.email = datosRegistroPaciente.email();
this.documento = datosRegistroPaciente.documento();
this.telefono = datosRegistroPaciente.telefono();
this.direccion = new Direccion(datosRegistroPaciente.direccion());
}
}

View File

@ -0,0 +1,6 @@
package med.voll.api.paciente;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PacienteRepository extends JpaRepository<Paciente, Long> {
}

View File

@ -1,3 +1,6 @@
spring.datasource.url=jdbc:mysql://192.168.0.8/vollmed_api
spring.datasource.username=alura
spring.datasource.password=alura
spring.datasource.password=alura
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

View File

@ -0,0 +1,16 @@
create table pacientes(
id bigint not null auto_increment,
nombre varchar(100) not null,
email varchar(100) not null unique,
documento varchar(6) not null unique,
telefono varchar(20) not null,
calle varchar(100) not null,
distrito varchar(100) not null,
complemento varchar(100),
numero varchar(20),
ciudad varchar(100) not null,
primary key(id)
);

View File

@ -0,0 +1,20 @@
USE vollmed_api;
INSERT INTO vollmed_api.medicos(nombre, email, telefono, documento, especialidad, calle, distrito, complemento, ciudad, numero)
VALUES
('Cuarto Medico', 'dr_4@voll.med', '44444', '444444', 'CARDIOLOGIA', 'calle 4', 'distrito 4', 'prueba', 'Santiago', '4'),
('Quinto Medico', 'dr_5@voll.med', '55555', '555555', 'PEDIATRIA', 'calle 5', 'distrito 5', 'prueba', 'Santiago', '5'),
('Sexto Medico', 'dr_6@voll.med', '66666', '666666', 'ORTOPEDIA', 'calle 6', 'distrito 6', 'prueba', 'Santiago', '6'),
('Septimo Medico', 'dr_7@voll.med', '77777', '777777', 'GINECOLOGIA', 'calle 7', 'distrito 7', 'prueba', 'Santiago', '7'),
('Octavo Medico', 'dr_8@voll.med', '88888', '888888', 'CARDIOLOGIA', 'calle 8', 'distrito 8', 'prueba', 'Santiago', '8'),
('Noveno Medico', 'dr_9@voll.med', '99999', '999999', 'PEDIATRIA', 'calle 9', 'distrito 9', 'prueba', 'Santiago', '9'),
('Decimo Medico', 'dr_10@voll.med', '10101', '101010', 'ORTOPEDIA', 'calle 10', 'distrito 10', 'prueba', 'Santiago', '10'),
('Onceavo Medico', 'dr_11@voll.med', '11011', '110111', 'CARDIOLOGIA', 'calle 11', 'distrito 11', 'prueba', 'Santiago', '11'),
('Doceavo Medico', 'dr_12@voll.med', '12121', '121212', 'PEDIATRIA', 'calle 12', 'distrito 12', 'prueba', 'Santiago', '12'),
('Treceavo Medico', 'dr_13@voll.med', '13131', '131313', 'ORTOPEDIA', 'calle 13', 'distrito 13', 'prueba', 'Santiago', '13'),
('Catorceavo Medico', 'dr_14@voll.med', '14141', '141414', 'GINECOLOGIA', 'calle 14', 'distrito 14', 'prueba', 'Santiago', '14'),
('Quinceavo Medico', 'dr_15@voll.med', '15151', '151515', 'CARDIOLOGIA', 'calle 15', 'distrito 15', 'prueba', 'Santiago', '15'),
('Diesiceisavo Medico', 'dr_16@voll.med', '16161', '161616', 'PEDIATRIA', 'calle 16', 'distrito 16', 'prueba', 'Santiago', '16'),
('Diesicieteavo Medico', 'dr_17@voll.med', '17171', '171717', 'ORTOPEDIA', 'calle 17', 'distrito 17', 'prueba', 'Santiago', '17'),
('Diesiochoavo Medico', 'dr_18@voll.med', '18181', '181818', 'GINECOLOGIA', 'calle 18', 'distrito 18', 'prueba', 'Santiago', '18'),
('Diecinueveavo Medico', 'dr_19@voll.med', '19191', '191919', 'CARDIOLOGIA', 'calle 19', 'distrito 19', 'prueba', 'Santiago', '19'),
('Veinteavo Medico', 'dr_20@voll.med', '20202', '202020', 'PEDIATRIA', 'calle 20', 'distrito 20', 'prueba', 'Santiago', '20');

View File

@ -0,0 +1,20 @@
USE vollmed_api;
INSERT INTO vollmed_api.pacientes(nombre, email, telefono, documento, calle, distrito, complemento, ciudad, numero)
VALUES
('Cuarto Paciente', 'paciente_4@private.cl', '44444', '444444', 'calle 4', 'distrito 4', 'prueba', 'Santiago', '4'),
('Quinto Paciente', 'paciente_5@private.cl', '55555', '555555', 'calle 5', 'distrito 5', 'prueba', 'Santiago', '5'),
('Sexto Paciente', 'paciente_6@private.cl', '66666', '666666', 'calle 6', 'distrito 6', 'prueba', 'Santiago', '6'),
('Septimo Paciente', 'paciente_7@private.cl', '77777', '777777', 'calle 7', 'distrito 7', 'prueba', 'Santiago', '7'),
('Octavo Paciente', 'paciente_8@private.cl', '88888', '888888', 'calle 8', 'distrito 8', 'prueba', 'Santiago', '8'),
('Noveno Paciente', 'paciente_9@private.cl', '99999', '999999', 'calle 9', 'distrito 9', 'prueba', 'Santiago', '9'),
('Decimo Paciente', 'paciente_10@private.cl', '10101', '101010', 'calle 10', 'distrito 10', 'prueba', 'Santiago', '10'),
('Onceavo Paciente', 'paciente_11@private.cl', '11011', '110111', 'calle 11', 'distrito 11', 'prueba', 'Santiago', '11'),
('Doceavo Paciente', 'paciente_12@private.cl', '12121', '121212', 'calle 12', 'distrito 12', 'prueba', 'Santiago', '12'),
('Treceavo Paciente', 'paciente_13@private.cl', '13131', '131313', 'calle 13', 'distrito 13', 'prueba', 'Santiago', '13'),
('Catorceavo Paciente', 'paciente_14@private.cl', '14141', '141414', 'calle 14', 'distrito 14', 'prueba', 'Santiago', '14'),
('Quinceavo Paciente', 'paciente_15@private.cl', '15151', '151515', 'calle 15', 'distrito 15', 'prueba', 'Santiago', '15'),
('Diesiceisavo Paciente', 'paciente_16@private.cl', '16161', '161616', 'calle 16', 'distrito 16', 'prueba', 'Santiago', '16'),
('Diesicieteavo Paciente', 'paciente_17@private.cl', '17171', '171717', 'calle 17', 'distrito 17', 'prueba', 'Santiago', '17'),
('Diesiochoavo Paciente', 'paciente_18@private.cl', '18181', '181818', 'calle 18', 'distrito 18', 'prueba', 'Santiago', '18'),
('Diecinueveavo Paciente', 'paciente_19@private.cl', '19191', '191919', 'calle 19', 'distrito 19', 'prueba', 'Santiago', '19'),
('Veinteavo Paciente', 'paciente_20@private.cl', '20202', '202020', 'calle 20', 'distrito 20', 'prueba', 'Santiago', '20');

View File

@ -330,7 +330,8 @@ Documentación Java
## Agregando dependencias
Copiar `xml` de Spring [initializr](https://start.spring.io/), y pegar en `pom.xml`
Copiar sección del `xml` de Spring [initializr](https://start.spring.io/), y
pegar en `pom.xml`
Modificar `resources/application.properties`
@ -413,10 +414,11 @@ public class ProductoDao {
En el ejemplo anterior, se utilizó JPA como tecnología de persistencia de datos
de la aplicación.
**Patrón Repository**. Según el famoso libro Domain-Driven Design de Eric Evans:
**Patrón Repository** según el famoso libro *Domain-Driven Design* de *Eric
Evans*:
El repositorio es un mecanismo para encapsular el almacenamiento, recuperación y
comportamiento de búsqueda, que emula una colección de objetos.
> *El repositorio es un mecanismo para encapsular el almacenamiento, recuperación y
comportamiento de búsqueda, que emula una colección de objetos.*
En pocas palabras, un repositorio también maneja datos y oculta consultas
similares a DAO. Sin embargo, se encuentra en un nivel más alto, más cerca de
@ -516,3 +518,206 @@ estableciendo una colaboración entre componentes.
Para más información sobre la anotación, ver la
[documentación oficial](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/annotation/Autowired.html)
## Get
### Consideraciones
Información requerida del médico
- Nombre
- Especialidad
- Documento
- Email
Reglas del negocio
- Orden ascendente
- Paginado, máximo 10 registros por página
<br>
En
[MedicoController](./api_rest/api/src/main/java/med/voll/api/controller/MedicoController.java)
se utilizo DTO para representar los datos recibi4os y devueltos a través de la
API, *¿Por qué, en lugar de crear un DTO, no devolvemos directamente la entidad
JPA en el Controller?*. Para esto, basta con cambiar método `listadoMedicos()`
en el Controller a:
```java
@GetMapping
public List<Medico> listarMedicos() {
return repository.findAll();
}
```
De esa forma, el código sería más ligero y no necesitaríamos crear el DTO
en el proyecto. Pero, **¿es esto realmente una buena idea?**
#### Problemas de recepción/devolución de la entidad JPA
De hecho, es mucho más simple y cómodo no usar DTO, sino tratar directamente
con entidades JPA en los Controllers. Sin embargo, este enfoque tiene algunas
desventajas, incluida la vulnerabilidad de la aplicación a los ataques de
*Mass Assignment*.
Uno de los problemas, es que al devolver una entidad JPA en un método del
Controller, Spring generará el JSON que contiene todos sus atributos, y este no
siempre es el comportamiento deseado.
Eventualmente pueden tener atributos que no se requiere que sean devueltos en el
JSON, ya sea por razones de seguridad, en el caso de datos sensibles, o incluso
porque no son utilizados por clientes API.
#### Uso de la anotación `@JsonIgnore`
En esta situación, se puede usar la anotación `@JsonIgnore`, que nos ayuda a
ignorar ciertas propiedades de una clase Java cuando se serializa en un objeto
JSON.
Su uso consiste en agregar la anotación a los atributos que se quieren ignorar
cuando se genera el JSON. Por ejemplo, supongamos que tenemos una entidad JPA
`Empleado`, en la que se quiere ignorar el atributo `salario`:
```java
@Getter
@NoArgsConstructor
@EqualsAndHashCode(of = "id")
@Entity(name = "Empleado")
@Table(name = "empleados")
public class Empleado {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nombre;
private String email;
@JsonIgnore
private BigDecimal salario;
...
}
```
En el ejemplo anterior, el atributo `salario` de la clase `Empleado` no será
mostrado en las respuestas JSON y el problema estaría resuelto.
Sin embargo, puede haber algún otro endpoint de la API en el que necesitemos
enviar el salario de los empleados en el JSON, en cuyo caso se tendrían problemas,
ya que con la anotación `@JsonIgnore` tal atributo nunca se enviará en el JSON,
y al eliminar la anotación se enviará el atributo siempre. Por lo tanto,
se pierde la flexibilidad de controlar cuándo se deben enviar ciertos atributos
en el JSON y cuándo no.
#### DTO
El patrón **DTO** (***Data Transfer Object***) es un patrón arquitectónico que
se usó ampliamente en aplicaciones Java distribuidas (arquitectura
cliente/servidor) para representar los datos que eran enviados y recibidos entre
aplicaciones cliente y servidor.
El patrón **DTO** puede (y debe) usarse cuando no queremos exponer todos los
atributos de alguna entidad en nuestro proyecto, una situación similar a los
salarios de los empleados que discutimos anteriormente. Además, con la
flexibilidad y la opción de filtrar qué datos se transmiten, podemos ahorrar
tiempo de procesamiento.
#### Bucle infinito que causa StackOverflowError
Otro problema muy recurrente cuando se trabaja directamente con entidades JPA
ocurre cuando una entidad tiene alguna *auto-relación* o *relación
bidireccional*. Por ejemplo, considere las siguientes entidades JPA:
```java
@Getter
@NoArgsConstructor
@EqualsAndHashCode(of = "id")
@Entity(name = "Producto")
@Table(name = "productos")
public class Producto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nombre;
private String descripcion;
private BigDecimal precio;
@ManyToOne
@JoinColumn(name = “id_categoria”)
private Categoria categoria;
...
}
```
```java
@Getter
@NoArgsConstructor
@EqualsAndHashCode(of = "id")
@Entity(name = "Categoria")
@Table(name = "categorias")
public class Categoria {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nombre;
@OneToMany(mappedBy = “categoria”)
private List<Producto> productos = new ArrayList<>();
...
}
```
Al devolver un objeto de tipo `Producto` en el Controller, Spring tendría
problemas para generar el JSON de este objeto, lo que provocaría una excepción
de tipo `StackOverflowError`. Este problema ocurre porque el objeto producto
tiene un atributo de tipo `Categoria`, que a su vez tiene un atributo de tipo
`Lista<Producto>` lo que provoca un bucle infinito en el proceso de serialización
a JSON.
Este problema se puede resolver usando la anotación `@JsonIgnore` o usando las
anotaciones `@JsonBackReference` y `@JsonManagedReference`, pero también se puede
evitar usando un DTO que represente solo los datos que se deben devolver en el
JSON.
### Paginación y Orden
Para la paginación se utiliza la interfase `Pageable`
```java
...
@GetMapping
public Page<DatosListadoMedicos> listadoMedicos(
@PageableDefault(size = 5) Pageable paginacion
) {
return medicoRepository.findAll(paginacion).map(DatosListadoMedicos::new);
}
}
```
Estos pueden pre-establecerse con la anotación `@PageableDefault`, o en el archivo
[application.properties](./api_rest/api/src/main/resources/application.properties)
```ini
spring.data.web.pageable.page-parameter=0
spring.data.web.pageable.size-parameter=5
spring.data.web.sort.sort-parameter=nombre
```
Estos además pueden ser sobrescritos al hacer el request:
```http
http://127.0.0.1:8080/medicos?size=3&page=2&sort=documento,desc
```
#### Ver y formatear querys en el IDE
En archivo [application.properties](./api_rest/api/src/main/resources/application.properties)
```ini
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
```