En esta etapa, y, después de la creación de todo el CRUD para la tabla Cine, pasamos a definir el CRUD para la tabla Entrada. En este caso vamos a generar alguna validación, definiendo una anotación, y para ello, debemos añadir a nuestro pom.xml una anotacion mas:
<!-- Para las validaciones -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- fin validaciones -->
Toda las definiciones comunes, como application.properties o secret.properties, los encontrareis en mi github, y esta explicado en el post que os he mencionado anteriormente
Tablas a mantener
Las anotaciones de filtro necesarias
Primero, tenemos que considerar los filtros que necesitamos. Vemos que tenemos que validar el DNI, necesitamos comprobar el formato de una fecha, y que esa fecha no este en el pasado, y que el valor de «entCine» exista en la tabla de cines.
Aunque ya tenemos una clase que se encarga de realizar todos estos filtros, hemos decidido que en lugar de ubicarlos en el controlador o en el servicio, los podríamos hacer por anotaciones, como hacemos con los @NotNull, o @Size…
Para ello, tenemos que crear tres anotaciones, y, para poder comunicar los errores de una forma sencilla, necesitaremos tambien un @ControllerAdvice, y, todo eso lo hemos ido explicando en articulos anteriores
- Para filtrar el DNI hemos creado @DniConstraint en este artículo (Creacion de anotaciones para filtros)
- Para la fecha hemos creado @CheckFechaFuturaValidation en este artículo(Creando un filtro personalizado de fecha con mensaje segun error)
- Y para la comprobación de que el codigo de cine existe, hemos creado @CheckCineValidation en este artículo (Añadiendo un filtro de comprobación de existencia en tabla por anotaciones, en Spring)
- Por ultimo, para conseguir visualizar correctamente los mensajes de error, en un entorno APIRest, hemos explicado como añadir un ControllerAdvice en Añadiendo un @ControllerAdvice. Aclarando mensajes de error
- Ya, de paso, podemos crear una anotación @EntradaValidator que nos garantizara que le entrada que indiquemos, existe en la tabla, y eso lo hacemos en el articulo Añadiendo un filtro de existencia en tabla Entrada, por Anotacion, y que describimos en Añadiendo un filtro de comprobacion de existencia en tabla de Entrada @CheckEntradaValidation
Os remito a todos estos articulos para ver y entender como hemos creado las anotaciones
Para realizar los filtros, en otras ocasiones, hemos utilizado la misma Entity que usábamos para grabar, pero, en este caso, no lo vamos a hacer así, ya que el manejo de fecha nos lo haria un poco retorcido, así que hemos creado la Entity, pero también hemos creado una clase EntradaDTO con la que realizaremos la entrada
EntradaDTO.java
package com.recursosformacion.lcs.model_dto;
import org.springframework.stereotype.Component;
import com.recursosformacion.lcs.util.constraint.interfaces.CheckCineValidation;
import com.recursosformacion.lcs.util.constraint.interfaces.CheckFechaFuturaValidation;
import com.recursosformacion.lcs.util.constraint.interfaces.DniConstraint;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Component
public class EntradaDTO {
private Long id_entrada;
@NotNull
@CheckFechaFuturaValidation
private String ent_fecha;
@NotNull(message = "Es necesario indicar el cine")
@CheckCineValidation
private Long entCine;
@NotNull(message = "Es necesario indicar la fila")
@Min(value = 1, message = "Se ha de indicar una fila mayor que 0")
@Max(value = 100, message = "Se ha de indicar una fila inferior a 100")
private int ent_fila;
@NotNull(message = "Es necesario indicar el asiento")
@Min(value = 1, message = "Se ha de indicar un asiento mayor que 0")
@Max(value = 100, message = "Se ha de indicar un asiento inferior a 100")
private int ent_numero;
@NotBlank
@Size(message = "Error en el identificador del cliente ¿DNI? '${validatedValue}' .Su longitud debe ser {min}", max = 12, min = 12)
@DniConstraint
private String idCliente;
public EntradaDTO() {
super();
}
public EntradaDTO(Long id_entrada, String ent_fecha, Long id_cine, int ent_numero, String idCliente) {
super();
this.id_entrada = id_entrada;
this.ent_fecha = ent_fecha;
this.entCine = id_cine;
this.ent_numero = ent_numero;
this.idCliente = idCliente;
}
public Long getId_entrada() {
return id_entrada;
}
public void setId_entrada(Long id_entrada) {
this.id_entrada = id_entrada;
}
public String getEnt_fecha() {
return ent_fecha;
}
public void setEnt_fecha(String ent_fecha) {
this.ent_fecha = ent_fecha;
}
public Long getId_cine() {
return entCine;
}
public void setId_cine(Long id_cine) {
this.entCine = id_cine;
}
public int getEnt_numero() {
return ent_numero;
}
public void setEnt_numero(int ent_numero) {
this.ent_numero = ent_numero;
}
public String getIdCliente() {
return idCliente;
}
public void setIdCliente(String idCliente) {
this.idCliente = idCliente;
}
public Long getEntCine() {
return entCine;
}
public void setEntCine(Long entCine) {
this.entCine = entCine;
}
public int getEnt_fila() {
return ent_fila;
}
public void setEnt_fila(int ent_fila) {
this.ent_fila = ent_fila;
}
@Override
public String toString() {
return "EntradaDTO [id_entrada=" + id_entrada + ", ent_fecha=" + ent_fecha + ", id_cine=" + entCine
+ ", ent_numero=" + ent_numero + ", idCliente=" + idCliente + "]";
}
}
Como siempre, he tenido que escribir los atributos y sus filtros, y luego he dejado que Eclipse me genere constructores, getters, setter y toString…
Y con este objeto, recibiremos la información de la entrada, y además ya filtrada, por lo que solo necesitaremos moverla a la Entity y salvarla o actualizarla
La creación de nuestro segundo modelo
Para crear nuestros modelos (Entities), vamos a crear la clase Entrada.java, implementando la interface (Modelo) que preparamos anteriormente
Entrada.java
package com.recursosformacion.lcs.model;
import java.time.LocalDate;
import com.recursosformacion.lcs.model.interfaces.Modelo;
import com.recursosformacion.lcs.util.Constantes;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "Entrada")
public class Entrada implements Modelo {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id_entrada;
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy")
@Column(nullable=false,columnDefinition = "DATE")
private LocalDate ent_fecha;
@Column(nullable=false)
private int ent_fila;
@Column(nullable=false)
private int ent_numero;
@Column(nullable=false)
private String idCliente;
private Long entCine;
public Long getEntCine() {
return entCine;
}
public void setEntCine(Long entCine) {
this.entCine = entCine;
}
public Entrada() {
super();
}
public Entrada(long id_entrada, LocalDate ent_fecha, Long entCine, int ent_fila, int ent_numero) {
super();
setId_entrada(id_entrada);
setEnt_fecha(ent_fecha);
setEntCine(entCine);
setEnt_numero(ent_numero);
setEnt_fila(ent_fila);
}
public Entrada(long id_entrada, String ent_fecha_str, Long entCine, int ent_fila, int ent_numero) {
super();
setId_entrada(id_entrada);
setEnt_fecha_str(ent_fecha_str);
setEntCine(entCine);
setEnt_numero(ent_numero);
setEnt_fila(ent_fila);
}
public long getId_entrada() {
return id_entrada;
}
public void setId_entrada(long id_entrada) {
this.id_entrada = id_entrada;
}
public LocalDate getEnt_fecha() {
return ent_fecha;
}
public void setEnt_fecha(LocalDate ent_fecha) {
this.ent_fecha = ent_fecha;
}
public int getEnt_numero() {
return ent_numero;
}
public void setEnt_numero(int ent_numero) {
this.ent_numero = ent_numero;
}
public String getIdCliente() {
return idCliente;
}
public void setIdCliente(String idCliente) {
this.idCliente = idCliente;
}
public void setEnt_fecha_str(String ent_fecha_str) {
setEnt_fecha(LocalDate.parse(ent_fecha_str,Constantes.FORMATO_FECHA_EU));
}
public int getEnt_fila() {
return ent_fila;
}
public void setEnt_fila(int i) {
this.ent_fila = i;
}
@Override
public String toString() {
return "Entrada [id_entrada=" + id_entrada + ", ent_fecha=" + ent_fecha + ", Cine=" + entCine + ", ent_numero="
+ ent_numero + ", idCliente=" + idCliente + "]";
}
@Override
public boolean isValidInsert() {
return true;
}
@Override
public boolean isValidUpdate() {
return true;
}
}
La única novedad aquí, es que utilizo dos constructores, para poder recibir fecha de cualquier manera, asi como un setter mas, también para soportar el formato de fecha en string.
Tambien señalar que no he añadido mas filtros que los necesarios para crear la tabla, ya que la entrada la realizamos por medio de otro objeto (EntradaDTO)
El repositorio
Como siempre, el repositorio lo haremos implementando una interfaz con JpaRepository, interfaz genérica, que completaremos con la indicación de la Entity a mantener (Entrada), y el tipo de campo del Id, en nuestro caso, Long.
IEntrada.java
package com.recursosformacion.lcs.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.recursosformacion.lcs.model.Entrada;
public interface IEntrada extends JpaRepository<Entrada, Long>{
List<Entrada> findByIdCliente(String idCliente);
List<Entrada> findByEntCine(Long entCine);
}
Al preparar este repositorio, me encuentro que necesito buscar por cine, y los generadores de Spring no soportan los campos con guion bajo, asi que el campo que antes definia como «ent_cine», pasa a llamarse «entCine. Realmente, solo necesito hacer eso en la Entity, pero para aclarar las cosas, cambio el nombre en todas partes.
De esta forma, en este repositorio, preparo las lecturas para
- Todas las entradas de un IdCliente
- Todas las entradas de un entCine
El Servicio
Para esta clase, debo utilizar la interface creada anteriormente para los servicios, y que llamamos IServicio.java, descrita en Desarrollo de un CRUD con SpringBoot. (Cine) y solucionar los metodos de acceso a la BBDD; algo como esto:
EntradaService.java
package com.recursosformacion.lcs.service;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.recursosformacion.lcs.exception.DAOException;
import com.recursosformacion.lcs.exception.DomainException;
import com.recursosformacion.lcs.model.Entrada;
import com.recursosformacion.lcs.repository.IEntrada;
import com.recursosformacion.lcs.service.interfaces.IServicio;
import com.recursosformacion.lcs.util.Rutinas;
@Service
public class EntradaService implements IServicio<Entrada, Long> {
private final IEntrada entradaRepository;
EntradaService(IEntrada entradaRepository){
this.entradaRepository = entradaRepository;
}
@Override
public Entrada insert(Entrada entrada) {
return entradaRepository.save(entrada);
}
@Override
public List<Entrada> listAll() {
return entradaRepository.findAll();
}
public List<Entrada> entradaPorIdCliente(String id){
return entradaRepository.findByIdCliente(id);
}
@Override
public boolean update(Entrada entrada) throws DomainException,DAOException {
Optional<Entrada> entradaDBO = entradaRepository.findById(entrada.getId_entrada());
if (entradaDBO.isEmpty()) {
throw new DAOException("El registro:" + entrada.getId_entrada() + ", ya no existe");
}
Entrada entradaDB = entradaDBO.get();
entradaDB.setIdCliente(Rutinas.nuevoSiNoVacio(entradaDB.getIdCliente(), entrada.getIdCliente()));
entradaDB.setEnt_fecha(Rutinas.nuevoSiNoVacio(entradaDB.getEnt_fecha(), entrada.getEnt_fecha()));
entradaDB.setEnt_fila(Rutinas.nuevoSiNoVacio(entradaDB.getEnt_fila(), entrada.getEnt_fila()));
entradaDB.setEnt_numero(Rutinas.nuevoSiNoVacio(entradaDB.getEnt_numero(), entrada.getEnt_numero()));
entradaDB.setEntCine(Rutinas.nuevoSiNoVacio(entradaDB.getEntCine(), entrada.getEntCine()));
return entradaRepository.save(entradaDB) != null;
}
@Override
public boolean deleteById(Long id_entrada) throws DAOException {
Optional<Entrada> entradaDBO = entradaRepository.findById(id_entrada);
if (entradaDBO.isEmpty()) {
throw new DAOException("El registro:" + id_entrada + ", ya no existe");
}
entradaRepository.deleteById(id_entrada);
return true;
}
@Override
public Optional<Entrada> leerUno(Long id) {
return entradaRepository.findById(id);
}
public List<Entrada> findByIdCliente(String id){
return entradaRepository.findByIdCliente(id);
}
public List<Entrada> findByEntCine(Long id){
return entradaRepository.findByEntCine(id);
}
}
Quiero señalar que, como en el resto del proyecto, estamos utilizando constructores para recibir las inyecciones de dependencia, según se recomienda en las últimas versiones de Spring.
Tambien recordad que en el articulo que menciono, encontrareis las definiciones de las excepciones que iremos utilizando, para mejorar la claridad de los informes
Por ultimo, y como diferencia, en este servicio tengo que hacer las llamadas que he añadido en el repositorio, indicando lo que hacen, aunque el acceso a tabla lo construirá JPA
El controlador
Hemos hablado de escribir una API que cumpla con los protocolos RESTFUL, por lo que esperaremos recibir información en JSON y devolveremos igualmente la información en ese formato.
Las llamadas que atenderemos, serán:
Método | URL | Acción |
GET | /api/entrada | Devuelve todas las entradas |
GET | /api/entrada/<id> | Devuelve la entrada indicada |
GET | /api/entrada/leerporcine/<id> | Devuelve todas la entradas del cine id |
GET | /api/entrada/leerporid/<id> | Devuelve todas las entradas de un Id de cliente |
POST | /api/entrada | Espera los datos de la entrada en el cuerpo (JSON) y la da de alta en la BBDD |
PUT | /api/entrada | Recibe un objeto EntradaDTO como JSON y actualiza los datos existentes. El id del registro a modificar, lo obtiene del objeto recibido |
DELETE | /api/entrada/<id> | Borra de la tabla el registro indicado por id |
La estructura de respuesta podría ser el JSON generado por el objeto a devolver, mas un código http para indicar el resultado, si bien, en el primer ejemplo hemos optado por devolver un JSON con el status, que indica si ha ido bien o no, y como segundo elemento, pueden ir un mensaje, o el dato a devolver en json
Por ejemplo,
o
EntradaController.java
package com.recursosformacion.lcs.controller;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.recursosformacion.lcs.exception.ControllerException;
import com.recursosformacion.lcs.exception.DAOException;
import com.recursosformacion.lcs.exception.DomainException;
import com.recursosformacion.lcs.model.Entrada;
import com.recursosformacion.lcs.model_dto.EntradaDTO;
import com.recursosformacion.lcs.service.CineService;
import com.recursosformacion.lcs.service.EntradaService;
import jakarta.validation.Valid;
@CrossOrigin
@RestController
@RequestMapping("/api/entrada")
public class EntradaController {
private final EntradaService cDao;
private final CineService cDaoCine;
EntradaController(EntradaService cDao, CineService cDaoCine){
this.cDao = cDao;
this.cDaoCine = cDaoCine;
}
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> leerUno(@PathVariable("id") String ids) throws ControllerException {
String mensaje = "";
Map<String, Object> map = new LinkedHashMap<String, Object>();
if (ids != null) {
try {
Long id = Long.parseLong(ids);
Optional<Entrada> entradaDB = (Optional<Entrada>) cDao.leerUno(id);
if (entradaDB.isPresent()) {
map.put("status", 1);
map.put("data", entradaDB.get());
return new ResponseEntity<>(map, HttpStatus.OK);
} else {
mensaje = "No existen datos";
}
} catch (NumberFormatException nfe) {
mensaje = "Formato erroneo";
}
} else {
mensaje = "Formato erroneo";
}
throw new ControllerException(mensaje);
}
@GetMapping({ "", "/" })
public ResponseEntity<Map<String, Object>> leerTodos() throws ControllerException {
Map<String, Object> map = new LinkedHashMap<String, Object>();
List<Entrada> cat = cDao.listAll();
if (!cat.isEmpty()) {
map.put("status", 1);
map.put("data", cat);
return new ResponseEntity<>(map, HttpStatus.OK);
} else {
throw new ControllerException("No existen datos");
}
}
@GetMapping("/leerporid/{idCliente}")
public ResponseEntity<Map<String, Object>> leerPorId(@PathVariable("idCliente") String id) throws ControllerException {
Map<String, Object> map = new LinkedHashMap<String, Object>();
List<Entrada> entradas = cDao.findByIdCliente(id);
if (!entradas.isEmpty()) {
map.put("status", 1);
map.put("data", entradas);
return new ResponseEntity<>(map, HttpStatus.OK);
} else {
throw new ControllerException("No existen datos");
}
}
@GetMapping("/leerporcine/{idCine}")
public ResponseEntity<Map<String, Object>> leerporcine(@PathVariable("idCine") Long id) throws ControllerException {
Map<String, Object> map = new LinkedHashMap<String, Object>();
List<Entrada> entradas = cDao.findByEntCine(id);
if (!entradas.isEmpty()) {
map.put("status", 1);
map.put("data", entradas);
return new ResponseEntity<>(map, HttpStatus.OK);
} else {
throw new ControllerException("No existen datos para cine " + id);
}
}
@PostMapping
public ResponseEntity<Map<String, Object>> alta(@Valid @RequestBody EntradaDTO c )
throws DomainException, ControllerException, DAOException { // ID,NOMBRE,DESCRIPCION
Map<String, Object> map = new LinkedHashMap<String, Object>();
Entrada e = convertirDTO(c);
e.setId_entrada(0l);
e = cDao.insert(e);
if (e != null) {
cDaoCine.addEntrada(e);
map.put("status", 1);
map.put("data", e);
return new ResponseEntity<>(map, HttpStatus.OK);
} else {
throw new ControllerException("Error al hacer la insercion");
}
}
@PutMapping
public ResponseEntity<Map<String, Object>> modificacion(@Valid @RequestBody EntradaDTO c)
throws ControllerException, DomainException, DAOException {
Map<String, Object> map = new LinkedHashMap<String, Object>();
Entrada e = convertirDTO(c);
if (cDao.update(e)) {
map.put("status", 1);
map.put("message", "Actualizacion realizada");
return new ResponseEntity<>(map, HttpStatus.OK);
} else {
throw new ControllerException("Error al hacer la modificacion");
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Map<String, Object>> eliminar(@PathVariable("id") String ids) throws ControllerException {
Map<String, Object> map = new LinkedHashMap<String, Object>();
if (ids != null) {
try {
long id = Long.parseLong(ids);
Optional<Entrada> entradaDB = cDao.leerUno(id);
cDao.deleteById(entradaDB.get().getId_entrada());
map.put("status", 1);
map.put("message", "Registro borrado");
return new ResponseEntity<Map<String, Object>>(map, HttpStatus.OK);
} catch (Exception ex) {
throw new ControllerException("Error al borrar");
}
}
throw new ControllerException("No existe registro al borrar");
}
public Entrada convertirDTO(EntradaDTO d) throws ControllerException {
Entrada e = new Entrada();
if (Objects.isNull(d.getId_entrada())) {
d.setId_entrada(0L);
}
e.setId_entrada(d.getId_entrada());
e.setEnt_fila(d.getEnt_fila());
e.setEnt_numero(d.getEnt_numero());
e.setEnt_fecha_str(d.getEnt_fecha());
e.setIdCliente(d.getIdCliente());
e.setEntCine(d.getEntCine());
return e;
}
}
Lo hemos declarado @CrossOrigin para evitar los errores de seguridad, ya que los vamos a autorizar, lo declaramos @RestMapping, porque estamos construyendo una api REST, y señalamos que todos los métodos de este controlador van a responder a «api/entrada» por lo que a nivel de método, obviaremos esta parte del path
Este controlador requiere cDao de Entrada, y CineServicio, ya que deberá añadir la entrada a la lista existente, y solicita que Spring lo inyecte en el constructor, y después vamos definiendo cada método, tras anotar cuando responderá. (Un planteamiento mas purista, desplazaría la modificación de Cine, al servicio de entrada, ya que es el verdadero responsable de la necesidad de esta modificación).(Tambien es cierto que esa modificación es innecesaria…pero estamos enseñando cosas….)
Como veis, en cada método construyo el ResponseEntity que retornara, y que contiene el map con la información correcta. Si tiene que indicar un error, devuelve la excepción con el error correspondiente, y, anteriormente, ya hemos explicado el modulo de tratamiento de errores
Otro tema que debemos tener presente, es que el formato de la entrada no va a ajustarse al objeto «entrada«, ya que tengo fecha que me llegara en formato String, y, para eso, definí un DTO idéntico a Entrada, pero con la fecha en String
Tambien, esa diferencia, hace que necesite un metodo para convertir EntradaDTO a Entrada, aunque mas adelante, lo optimizaremos.
Quedan muchas cosas por hacer, pero en este momento, podríamos ver como va todo, utilizando Postman, y, asi, cerrar esta etapa.
Probando
Recordad que en application.properties establecimos como puerto el 8001, aunque ante cualquier duda, podéis acudir a los mensajes de arranque que aparecen cada vez que modificáis algo, o cuando lanzáis la aplicacion, y hacia el final de todo, a la derecha parece el puerto en que está corriendo el servidor
podemos iniciar Postman y probamos de dar un alta
Vemos que la respuesta es correcta, si damos varias alta, después podemos pedir un listado de todas
Si deseamos modificar un registro, podemos hacer
Y ahora, podemos comprobar como nos ha quedado. Recordad que en mi caso el id es 7952, pero este numero es aleatorio, el vuestro será otro
Supongo que ya sabeis como borrar un elemento, hago un list para verlos, escojo el id que me interesa y …
Si ahora intentáis verlo, recibirías un error, con el mensaje correcto, y si habeis añadido el @ControllerAdvice que os comente en el articulo anterior
Tambien podeis comprobar que si los datos son erróneos…. da error
O la consulta por cine
Conclusión
Todo este desarrollo lo tendreis explicado con mas detalle en youTube, a partir de fin de mayo, y, aunque es conveniente que intentéis escribirlo TODO vosotros, si queréis renunciar a ello, lo teneis tambien en GitHub
Este desarrollo esta hecho para disponer de un fuente para explicar otros temas, tal y como se indica en Visión de conjunto con Spring