Este es el CRUD a desarrollar inicialmente; nuestro trabajo inicial lo realizaré con una base de datos Oracle Express 21, Java 17, SpringBoot 3.1, y, como IDE, utilizaré STS 4, vamos, Eclipse adaptado para el desarrollo Spring. Tambien he procurado mantener todas las versiones lo más actualizadas posible para el momento de escritura (Octubre 2023)
Tablas a mantener
Nuestro application.properties
Como se que si os gusta más la estructura de yaml no os será difícil obtenerla, voy a utilizar properties.
Pero, por motivos de seguridad, y, como explique en el artículo «Mejorando la seguridad en Spring» (Acudid a él si teneis dudas), los datos los escribiré en un fichero que llamaré «secret.properties» y los importaré a application.properties. Como el primer fichero lo pondré en .gitignore, nadie conocerá los datos sensibles y cada participante podrá tener su versión
Aqui esta mi fichero
y ahora lo añado al fichero .gitignore que ya me genero la creación del proyecto.
Si no lo veis, puede que tengáis que revisar los filtros de la pantalla Package Explorer
Ahora se trata que hagamos que los datos de «secret» se asignen al sitio correcto que es el application.properties
He dejado marcado toda la información que proviene de secrets, y que es lo que nos va a permitir conectarnos con la base de datos. Como veis, en la linea 7, he añadido la importación del fichero, y, luego, solo tengo que ir añadiendo cada referencia en donde quiero que sea usada…..
Ahora ya estamos preparados para crear nuestras entities.
La creación de nuestros modelo
Para crear nuestros modelos (Entities), vamos a definir primero algun metodo comun por medio de una interface; para ello, creo los paquetes com.recursosformacion.lcs.model y com.recursosformacion.lcs.model.interfaces, y, en el segundo de ellos, puedo escribir
Modelo.java
package com.recursosformacion.lcs.model.interfaces;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Transient;
public interface Modelo {
@Transient
@JsonIgnore
public boolean isValidInsert();
@Transient
@JsonIgnore
public boolean isValidUpdate();
}
Realmente, solo defino dos métodos a los que podré consultar antes de insertar o actualizar, y, los marco para que esos valores no se graben en la base de datos, ni se generen en los informes de salida.
Ahora, ya puedo generar los modelos. Debo utilizar las anotaciones de JPA, de las que en este caso, abuso un poco, únicamente para que recordéis sus posibilidades
Cine.java
package com.recursosformacion.lcs.model;
import java.util.ArrayList;
import java.util.List;
import com.recursosformacion.lcs.model.interfaces.Modelo;
import com.recursosformacion.lcs.util.Rutinas;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "Cine")
public class Cine implements Modelo {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id_cine;
@Column(nullable = false, length = 50)
private String ci_nombre;
@Column(nullable = true, length = 100)
private String ci_calle;
@Column(nullable = true, length = 100)
private String ci_barrio;
@Column(nullable = false)
private int ci_capacidad;
@ElementCollection
private List<Long> ci_lista_entradas;
public Cine() {
super();
}
public Cine(long id_cine, String ci_nombre, int ci_capacidad, List<Long> ci_lista_entradas) {
super();
this.id_cine = id_cine;
this.ci_nombre = ci_nombre;
this.ci_capacidad = ci_capacidad;
setCi_lista_entradas(ci_lista_entradas);
}
public long getId_cine() {
return id_cine;
}
public void setId_cine(long id_cine) {
this.id_cine = id_cine;
}
public String getCi_nombre() {
return ci_nombre;
}
public void setCi_nombre(String ci_nombre) {
this.ci_nombre = ci_nombre;
}
public int getCi_capacidad() {
return ci_capacidad;
}
public void setCi_capacidad(int ci_capacidad) {
this.ci_capacidad = ci_capacidad;
}
@Override
public String toString() {
return "Cine [id_cine=" + id_cine + ", ci_nombre=" + ci_nombre + ", ci_capacidad=" + ci_capacidad + "]";
}
public String getCi_calle() {
return ci_calle;
}
public void setCi_calle(String ci_calle) {
this.ci_calle = ci_calle;
}
public String getCi_barrio() {
return ci_barrio;
}
public void setCi_barrio(String ci_barrio) {
this.ci_barrio = ci_barrio;
}
public List<Long> getCi_lista_entradas() {
return ci_lista_entradas;
}
public void setCi_lista_entradas(List<Long> ci_lista_entradas) {
if (Rutinas.isEmptyOrNull(ci_lista_entradas)) {
ci_lista_entradas = new ArrayList<Long>();
}
this.ci_lista_entradas = ci_lista_entradas;
}
@Override
public boolean isValidInsert() {
return true;
}
@Override
public boolean isValidUpdate() {
return true;
}
}
Los getters y setters y constructores, asi como el «toString«, han sido generados con las funciones de Eclipse, por lo que me es tan fácil como si utilizara lombok, y, para mi, un poco más claro.
Como no hemos establecido ninguna norma de validez para actualizar o insertar, como campos existentes, validaciones múltiples…, me limito a responder True para dejar todo funcionando
Si todo está bien, en este punto se puede crear la base de datos vacía, y lanzar la aplicación y vereis como se crean las distintas tablas. Cada vez que modifiques datos relevantes de los modelos, automáticamente, JPA (Hibernate), intentara modificar las tablas. Si tenéis problemas, mientras estemos en pruebas, siempre os queda la posibilidad de borrar la tabla, y, en la siguiente ejecución se volverá a crear.
Para lanzar el proceso:
Los repositorios
El repositorio básico es terriblemente sencillo de crear; aquí está, en el package correspondiente
ICine.java
package com.recursosformacion.lcs.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.recursosformacion.lcs.model.Cine;
public interface ICine extends JpaRepository<Cine, Long>{
}
Tambien, señalaros que supongo que os habéis dado cuenta de que cada vez que grabáis algo, automáticamente, se os reinicia spring; eso es debido a que hemos añadido en el pom.xml, la dependencia spring-boot-devtools
Construcción de Servicios
En la siguiente etapa, debemos definir los servicios que realizarán las funciones de acceso a datos y que serán invocados por los controladores que crearemos a continuación. Ahora, aparecerán los filtros, por lo que necesitaremos objetos de error para procesarlos. Para esto, crearé un paquete llamado com.recursosformacion.lcs.exception y crearé las clases correspondientes, estableciendo solo el nombre y dejando igual el resto de funcionalidades.
DAOException.java
package com.recursosformacion.lcs.exception;
public class DAOException extends Exception {
/**
*
*/
private static final long serialVersionUID = 1L;
public DAOException() {
}
public DAOException(String mensaje) {
super(mensaje);
}
}
DomainException.java
package com.recursosformacion.lcs.exception;
@SuppressWarnings("serial")
public class DomainException extends Exception {
public DomainException() {
}
public DomainException(String mensaje) {
super(mensaje);
}
}
Como siempre. me preparo una interface tipo, para los servicios, en com.recursosformacion.lcs.service.interface. Naturalmente, la tengo que hacer genérica para poder soportar modelos distintos, aqui esta el fuente.
IServicio.java
package com.recursosformacion.lcs.service.interfaces;
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;
@Service
public interface IServicio<T, S> {
public T insert(T t) throws DomainException, DAOException ;
public boolean update(T t) throws DomainException, DAOException ;
public boolean deleteById(S s);
public List<T> listAll();
public Optional<T>leerUno(S s);
}
Como veis, es muy sencilla y solo tiene 5 métodos obligatorios, creo que autoexplicados
Ahora, ya puedo crear los servicios, de forma que en el package correspondiente (com.recursosformacion.lcs.service) paso a escribir las clases responsables de cada tabla
CineService.java
package com.recursosformacion.lcs.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.recursosformacion.lcs.exception.DAOException;
import com.recursosformacion.lcs.exception.DomainException;
import com.recursosformacion.lcs.model.Cine;
import com.recursosformacion.lcs.model.Entrada;
import com.recursosformacion.lcs.repository.ICine;
import com.recursosformacion.lcs.service.interfaces.IServicio;
import com.recursosformacion.lcs.util.Rutinas;
@Service
public class CineService implements IServicio<Cine, Long> {
@Autowired
private ICine cineRepository;
@Override
public Cine insert(Cine cine) throws DAOException {
List<Long> list_entradas = cine.getCi_lista_entradas();
if (Rutinas.isEmptyOrNull(list_entradas)) {
list_entradas = new ArrayList<Long>();
}
cine.setCi_lista_entradas(list_entradas);
if (cine.isValidInsert()) {
return cineRepository.save(cine);
} else {
throw new DAOException("El registro no se puede insertar");
}
}
@Override
public List<Cine> listAll() {
return cineRepository.findAll();
}
@Override
public boolean update(Cine cine) throws DomainException, DAOException {
Optional<Cine> cineDBO = cineRepository.findById(cine.getId_cine());
if (cineDBO.isEmpty()) {
throw new DAOException("El registro ya no existe");
}
Cine cineDB = cineDBO.get();
if (Objects.nonNull(cine.getCi_nombre()) && !"".equalsIgnoreCase(cine.getCi_nombre())) {
cineDB.setCi_nombre(cine.getCi_nombre());
}
if (Objects.nonNull(cine.getCi_calle()) && !"".equalsIgnoreCase(cine.getCi_calle())) {
cineDB.setCi_calle(cine.getCi_calle());
}
if (Objects.nonNull(cine.getCi_barrio()) && !"".equalsIgnoreCase(cine.getCi_barrio())) {
cineDB.setCi_barrio(cine.getCi_barrio());
}
if (Objects.nonNull(cine.getCi_capacidad())) {
cineDB.setCi_capacidad(cine.getCi_capacidad());
}
if (cine.isValidUpdate()) {
return cineRepository.save(cineDB) != null;
} else {
throw new DAOException("El registro no es valido para actualizacion");
}
}
@Override
public boolean deleteById(Long id_cine) {
cineRepository.deleteById(id_cine);
return true;
}
@Override
public Optional<Cine> leerUno(Long id) {
return cineRepository.findById(id);
}
public boolean addEntrada(Entrada entrada) throws DomainException, DAOException {
Optional<Cine> cineDBO = cineRepository.findById(entrada.getEnt_cine());
if (cineDBO.isEmpty()) {
throw new DAOException("El registro ya no existe");
}
Cine cineDB = cineDBO.get();
List<Long> list_entradas = cineDB.getCi_lista_entradas();
if (Rutinas.isEmptyOrNull(list_entradas)) {
list_entradas = new ArrayList<Long>();
}
list_entradas.add(entrada.getId_entrada());
cineDB.setCi_lista_entradas(list_entradas);
return cineRepository.save(cineDB) != null;
}
}
Aqui teneis lo que podria ser una primera version (funcional, pero muy tonta) del servicio para cines, fijaros que genera errores, y controla alguna cosa…. que no todo, aunque para la primera prueba, lo dejaremos asi.
Hemos previsto la inclusion de una entrada en la lista, para poderlo utilizar desde el servicio de alta de entrada
Vamos a dejar el resto de servicios, porque no dudo que tenéis claro cómo hacerlo, y vamos a revisar los Controller
Los controladores
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, de momento, serán:
Método | URL | Acción |
GET | /api/cine | Devuelve todos los cines |
GET | /api/cine/<id> | Devuelve el cine mencionado |
POST | /api/cine | Espera los datos de cine en el cuerpo (JSON) y lo da de alta en la BBDD |
PUT | /api/cine | Recibe un objeto Cine como JSON y actualiza los datos existentes. El id del registro a modificar, lo obtiene del objeto recibido |
DELETE | /api/cine/<id> | Borra de la tabla el registro indicado por id |
La tabla anterior se va repitiendo para cada tabla de la base de datos, modificando «cine» por «entrada», «pelicula»,»programa»…..
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
Necesitamos una Excepcion mas, para controlar los errores que se puedan producir en este modulo, por lo que en el package de «excepciones», añadimos la clase
ControllerException.java
package com.recursosformacion.lcs.exception;
@SuppressWarnings("serial")
public class ControllerException extends Exception {
public ControllerException() {
}
public ControllerException(String mensaje) {
super(mensaje);
}
}
Y en el paquete de controladores (controller) escribimos
CineController.java
package com.recursosformacion.lcs.controller;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
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.Cine;
import com.recursosformacion.lcs.service.CineService;
@CrossOrigin
@RestController
@RequestMapping("/api/cine")
public class CineController {
@Autowired
private CineService cDao;
@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<Cine> cineDB = cDao.leerUno(id);
if (cineDB.isPresent()) {
map.put("status", 1);
map.put("data", cineDB.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<Cine> cat = cDao.listAll();
if (!cat.isEmpty()) {
System.out.println(cat);
map.put("status", 1);
map.put("data", cat);
return new ResponseEntity<>(map, HttpStatus.OK);
} else {
throw new ControllerException("No existen datos");
}
}
@PostMapping
public ResponseEntity<Map<String, Object>> alta(@RequestBody Cine c) throws DomainException, ControllerException, DAOException { //ID,NOMBRE,DESCRIPCION
Map<String, Object> map = new LinkedHashMap<String, Object>();
c.setId_cine(0);
c=cDao.insert(c);
if (c!=null) {
System.out.println("En alta-" + c.toString());
map.put("status", 1);
map.put("message", "Registro salvado");
return new ResponseEntity<>(map, HttpStatus.OK);
} else {
throw new ControllerException("Error al hacer la insercion");
}
}
@PutMapping
public ResponseEntity<Map<String, Object>> modificacion(@RequestBody Cine c) throws ControllerException, DomainException, DAOException {
Map<String, Object> map = new LinkedHashMap<String, Object>();
if (cDao.update(c)) {
map.put("status", 1);
map.put("message", "Error al actualizar");
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<Cine> cineDB = cDao.leerUno(id);
cDao.deleteById(cineDB.get().getId_cine());
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");
}
}
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/cine» por lo que a nivel de método, obviaremos esta parte del path
Este controlador solo requiere CineServicio, que solicita que Spring lo inyecte, y después vamos definiendo cada método, tras anotar cuando responderá.
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, mas adelante, añadiremos el modulo de tratamiento de errores
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 aplicación, 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 4952, pero este número es aleatorio, el vuestro será otro
Supongo que ya sabéis 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, pero con muy mala presentación, pero eso…. es un tema para otro dia
Conclusión
Todo este desarrollo lo teneis explicado con mas detalle en youTube (a partir de 4/12/23) , 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
Lista en YouTube
Preparacion de application.Properties y creacion del Entity
Creacion del repositorio, el servicio y el controlador
Pruebas con Postman
Relacionado
Descubre más desde Recursos para formacion
Suscríbete y recibe las últimas entradas en tu correo electrónico.