Enjoy A New Student Discount All 55,000 Courses on sale for Only $12.99

Ends in 05h 23m 49s

Desarrollo de un CRUD con SpringBoot. (Cine)

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étodoURLAcción
GET/api/cineDevuelve todos los cines
GET/api/cine/<id>Devuelve el cine mencionado
POST/api/cineEspera los datos de cine en el cuerpo (JSON) y lo da de alta en la BBDD
PUT/api/cineRecibe 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

Deja un comentario

/*Si te ha gustado el artículo
no dudes en compartirlo*/

Facebook
Twitter
LinkedIn

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies

Ver mi IP

Ver ip de mi máquina
tipo valor
Ip: 3.87.11.93
Proxy: 3.87.11.93
Remote host: ec2-3-87-11-93.compute-1.amazonaws.com
Remote port: 42030
** 3.87.11.93, 172.70.34.40