12 - Pasos para crear una aplicación con Spring Boot elemental con acceso a MySQL que sirva una API

Problema

Continuaremos con la aplicación de chistes, agregando la funcionalidad para almacenar los mismos en una base de datos de MySQL.

  • En el concepto anterior ya creamos la base de datos llamada 'bd1':

    Creación de la base de datos
  • Desde el menú de opciones seleccionamos File-> New -> Spring Starter Project:

    Creación aplicación que sirve páginas estáticas
  • En el primer diálogo procedemos a definir el nombre de nuestro proyecto: proyecto007.

    En el segundo diálogo seleccionamos ahora tres dependencias

    Selección de las dependencias Spring Web, MySQL Driver y Spring Data JPA

    Todos los proyectos hasta ahora hemos utilizado la dependencia Sprint Web, ahora utilizaremos además las dependencias:

    MySQL Driver: La dependencia MySQL Driver en Spring Boot proporciona las clases y funcionalidades necesarias para conectarse y comunicarse con una base de datos MySQL desde una aplicación Java, específicamente en el contexto de una aplicación Spring Boot.

    Cuando agregamos esta dependencia, Spring Boot incluye el controlador JDBC de MySQL en el classpath de la aplicación. Esto permite que Spring Boot interactúe con la base de datos MySQL utilizando las API estándar de Java para la conectividad con bases de datos (JDBC).

    Spring Data JPA: La dependencia Spring Data JPA en Spring Boot proporciona una capa de abstracción sobre la API de Java Persistence API (JPA), lo que simplifica significativamente el acceso y la manipulación de datos en una base de datos relacional desde una aplicación Spring Boot. Aquí hay algunas funcionalidades clave que ofrece:

    • Simplifica la Capa de Acceso a Datos: Spring Data JPA reduce la cantidad de código que necesitamos escribir para interactuar con la base de datos al proporcionar métodos predefinidos para realizar operaciones CRUD (Crear, Leer, Actualizar, Eliminar) en las clases definidas en nuestro modelo.

    • Consulta Automática: Spring Data JPA genera consultas SQL automáticamente basadas en convenciones de nombres de métodos. Esto significa que podemos definir métodos en los repositorios que representan operaciones de consulta, y Spring Data JPA generará las consultas SQL necesarias en tiempo de ejecución.

    • Soporte para Consultas Personalizadas: Si necesitamos escribir consultas SQL personalizadas, Spring Data JPA te permite definir consultas nativas o consultas JPQL (Java Persistence Query Language) utilizando anotaciones.

    • Soporte para Asociaciones y Relaciones entre entidades: Spring Data JPA maneja automáticamente las relaciones entre entidades, lo que facilita el trabajo con asociaciones como uno a uno, uno a muchos y muchos a muchos.

  • Ahora especificamos que accederemos a la base de datos 'bd1', dicha actividad se hace en el archivo 'application.properties':

    application.properties

    spring.application.name=proyecto007
    spring.datasource.url=jdbc:mysql://localhost:3306/bd1?useSSL=false
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.jpa.hibernate.ddl-auto=update
    

    spring.datasource.url=jdbc:mysql://localhost:3306/bd1?useSSL=false
    Esta propiedad especifica la URL de la base de datos que la aplicación utilizará. En este caso, se está utilizando una base de datos MySQL que se encuentra en localhost en el puerto 3306, con el nombre de la base de datos bd1. La parte ?useSSL=false indica que no se está utilizando SSL para la conexión.

    spring.datasource.username=root
    Esta propiedad establece el nombre de usuario que se utilizará para conectarse a la base de datos. En este caso, el nombre de usuario es root, que es comúnmente el nombre de usuario predeterminado para MySQL.

    spring.datasource.password=123456
    Esta propiedad establece la contraseña que se utilizará para conectarse a la base de datos. En este caso, la contraseña es 123456 (la que creamos al instalar MySQL)

    spring.jpa.hibernate.ddl-auto=update
    Esta propiedad configura la estrategia de generación y actualización del esquema de la base de datos. En este caso, se utiliza update, lo que significa que Hibernate (que es la implementación JPA utilizada por Spring Boot) actualizará automáticamente el esquema de la base de datos según las entidades JPA definidas en la aplicación. Esto puede ser útil durante el desarrollo, pero debe tenerse cuidado en entornos de producción para evitar la pérdida de datos o cambios no deseados en la estructura de la base de datos. Otras opciones comunes para esta propiedad son create (para crear el esquema desde cero en cada inicio de la aplicación) y validate (para validar el esquema existente sin realizar cambios en él).

  • Procedemos a crear la carpeta 'model' y en la misma la clase 'Chiste':

    Creación de la carpeta model y la clase Chiste

    Chiste.java

    package com.example.demo.model;
    
    import jakarta.persistence.Entity;
    import jakarta.persistence.GeneratedValue;
    import jakarta.persistence.GenerationType;
    import jakarta.persistence.Id;
    
    @Entity
    public class Chiste {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
        private String texto;
        private String autor;
    
        public Chiste() {
        }
    
        public Chiste(int id, String texto, String autor) {
            this.id = id;
            this.texto = texto;
            this.autor = autor;
        }
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getTexto() {
            return texto;
        }
    
        public void setTexto(String texto) {
            this.texto = texto;
        }
    
        public String getAutor() {
            return autor;
        }
    
        public void setAutor(String autor) {
            this.autor = autor;
        }
    }
    

    @Entity: Esta anotación marca la clase como una entidad de persistencia JPA. Indica que la clase Chiste está mapeada a una tabla en la base de datos (en nuestro caso a una base de datos de MySQL).

    @Id: Esta anotación marca el campo id como la clave primaria de la entidad Chiste.

    @GeneratedValue(strategy = GenerationType.IDENTITY): Esta anotación especifica cómo se generará el valor de la clave primaria para cada nueva instancia de Chiste. En este caso, se utiliza la estrategia GenerationType.IDENTITY, que delega la generación del valor de la clave primaria al sistema de base de datos.

    private int id;: Este es el campo que representa la clave primaria de la entidad Chiste.

    private String texto; y private String autor;: Estos son los otros campos de la entidad Chiste, que representan el texto del chiste y el nombre del autor, respectivamente.

    Con estos simples datos Spring Boot hace toda la magia para solicitar a MySQL que cree la tabla 'chistes' con los campos respectivos.

    Luego veremos que hay más anotaciones para por ejemplo definir nombres de campos, extensiones de campos etc.

  • Ahora creamos la carpeta 'repository' y dentro creamos la interface 'ChisteRepository':

    interface 'ChisteRepository'

    package com.example.demo.repository;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    
    import com.example.demo.model.Chiste;
    
    public interface ChisteRepository extends JpaRepository<Chiste, Integer> {
    
    }
    
    public interface ChisteRepository extends JpaRepository {
    

    Esta línea define la interfaz ChisteRepository, la cual extiende de JpaRepository. La interfaz ChisteRepository actúa como un repositorio de datos para la entidad Chiste. Toma dos parámetros genéricos: el tipo de la entidad (Chiste) y el tipo del identificador de la entidad (Integer). Esto significa que la interfaz ChisteRepository proporciona métodos para interactuar con entidades Chiste cuyos identificadores son del tipo Integer.

    En resumen, la interfaz ChisteRepository define un repositorio de datos para la entidad Chiste, proporcionando métodos para realizar operaciones CRUD y otras operaciones relacionadas con la persistencia de datos en la base de datos, utilizando Spring Data JPA.

  • Creamos la carpeta 'service' y dentro de la misma la clase 'ChisteService'.

    carpeta service y clase ChisteService

    ChisteService.java

    package com.example.demo.service;
    
    import java.util.List;
    import java.util.Random;
    
    import org.springframework.stereotype.Service;
    
    import com.example.demo.model.Chiste;
    import com.example.demo.repository.ChisteRepository;
    
    @Service
    public class ChisteService {
        ChisteRepository repo;
    
        public ChisteService(ChisteRepository repo) {
            this.repo = repo;
        }
    
        public List<Chiste> retornarChistes() {
            return repo.findAll();
        }
    
        public void agregarChiste(Chiste chiste) {
            repo.save(chiste);
        }
    
        public void eliminarChiste(int id) {
            repo.deleteById(id);
        }
    
        public void actualizarChiste(int id, Chiste chiste) {
            Chiste chisteModificado = repo.findById(id).get();
            chisteModificado.setTexto(chiste.getTexto());
            chisteModificado.setAutor(chiste.getAutor());
            repo.save(chisteModificado);
        }
    
        public Chiste obtenerChiste(int id) {
            return repo.findById(id).get();
        }
    
        public Chiste obtenerChisteAleatorio() {
            List<Chiste> todosLosChistes = repo.findAll();
            Random rand = new Random();
            int indiceAleatorio = rand.nextInt(todosLosChistes.size());
            return todosLosChistes.get(indiceAleatorio);
        }
    
    }
    
        public ChisteService(ChisteRepository repo) {
            this.repo = repo;
        }
    

    Es el constructor de la clase ChisteService. Toma un parámetro de tipo ChisteRepository, que es una interfaz proporcionada por Spring Data JPA para interactuar con la base de datos. En este constructor, se inicializa el atributo repo de la clase ChisteService con el objeto repo proporcionado (es el proceso de inyección de dependencias)

        public List<Chiste> retornarChistes() {
            return repo.findAll();
        }
    

    Este método devuelve una lista de todos los chistes almacenados en la base de datos. Utiliza el método findAll() proporcionado por ChisteRepository.

        public void agregarChiste(Chiste chiste) {
            repo.save(chiste);
        }
    

    Este método recibe un objeto Chiste como parámetro y lo guarda en la base de datos utilizando el método save() proporcionado por ChisteRepository.

        public void eliminarChiste(int id) {
            repo.deleteById(id);
        }
    

    Elimina un chiste de la base de datos según su ID utilizando el método deleteById() proporcionado por ChisteRepository.

        public void actualizarChiste(int id, Chiste chiste) {
            Chiste chisteModificado = repo.findById(id).get();
            chisteModificado.setTexto(chiste.getTexto());
            chisteModificado.setAutor(chiste.getAutor());
            repo.save(chisteModificado);
        }
    

    Este método actualiza un chiste existente en la base de datos. Recibe el ID del chiste a actualizar y un objeto Chiste con los nuevos datos. Primero obtiene el chiste existente de la base de datos utilizando su ID, luego actualiza los campos del chiste existente con los datos del chiste pasado como parámetro y finalmente guarda el chiste actualizado en la base de datos.

        public Chiste obtenerChiste(int id) {
            return repo.findById(id).get();
        }
    

    Devuelve un chiste específico de la base de datos según su ID utilizando el método findById().

        public Chiste obtenerChisteAleatorio() {
            List<Chiste> todosLosChistes = repo.findAll();
            Random rand = new Random();
            int indiceAleatorio = rand.nextInt(todosLosChistes.size());
            return todosLosChistes.get(indiceAleatorio);
        }
    

    Devuelve un chiste aleatorio. Primero obtiene todos los chistes de la base de datos utilizando findAll(), luego genera un índice aleatorio dentro del rango de la lista de chistes y finalmente devuelve el chiste en ese índice.

  • Finalmente nos queda crear los endpoints en el controlador. Creamos la carpeta controller y dentro la clase 'ChisteController':

    carpeta controller y clase ChisteController

    ChisteController.java

    package com.example.demo.controller;
    
    import java.util.List;
    
    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.RestController;
    
    import com.example.demo.model.Chiste;
    import com.example.demo.service.ChisteService;
    
    @RestController
    public class ChisteController {
    
        private final ChisteService chisteService;
    
        public ChisteController(ChisteService chisteService) {
            this.chisteService = chisteService;
        }
    
        // Endpoint para obtener todos los chistes
        @GetMapping("/chistes")
        public List<Chiste> obtenerTodosLosChistes() {
            return chisteService.retornarChistes();
        }
    
        // Endpoint para agregar un nuevo chiste
        @PostMapping("/chistes")
        public void agregarNuevoChiste(@RequestBody Chiste chiste) {
            chisteService.agregarChiste(chiste);
        }
    
        // Endpoint para eliminar un chiste por su ID
        @DeleteMapping("/chistes/{id}")
        public void eliminarChiste(@PathVariable int id) {
            chisteService.eliminarChiste(id);
        }
    
        // Endpoint para actualizar un chiste existente
        @PutMapping("/chistes/{id}")
        public void actualizarChiste(@PathVariable int id, @RequestBody Chiste nuevoChiste) {
            chisteService.actualizarChiste(id, nuevoChiste);
        }
    
        // Endpoint para obtener un chiste por su ID
        @GetMapping("/chistes/{id}")
        public Chiste obtenerChistePorId(@PathVariable int id) {
            return chisteService.obtenerChiste(id);
        }
    
        // Endpoint para obtener un chiste aleatorio
        @GetMapping("/chistes/aleatorio")
        public Chiste obtenerChisteAleatorio() {
            return chisteService.obtenerChisteAleatorio();
        }
    
    }
    

    El código de la clase 'ChisteController' es el mismo que analizamos cuando trabajamos con los chistes almacenados en memoria Ram en una lista.

Ejecutemos la aplicación, controlemos que no se generen errores. Podemos entrar ahora a Workbench y verificar que se ha creado en forma automática la tabla 'chiste' con sus tres campos (esta entre otras son la magia que hace Spring Boot para agilizar el desarrollo de un proyecto):

tabla creada automaticamente por Spring Boot

Ahora procedamos a probar los 'endpoints' desde Postman.

Agreguemos un chiste a nuestra tabla de la base de datos:

Postman post

Luego podemos consultar la tabla 'chiste' desde Workbench para verificar si se han registrado los datos:

fila cargada en tabla mysql workbench

Luego de insertar otros chistes, probemos los otros endpoints.

Recuperar todos los chistes:

retornar todas las filas

Recuperar un chiste por su código:

recuperar un chiste por su codigo

Borrar un chiste:

borrar un chiste

Luego de borrarlo si consultamos todos los chistes, debería estar borrado el chiste con código 1:

retornar todas las filas

Los últimos endpoints que podemos probar son el que modifica un registro de la tabla:

modificar una fila de la tabla

Y el que retorna un chiste al azar:

retornar chiste al azar

Accesos a los endpoints desde un sitio web.

Si bien hemos dicho que este curso se centra en APIs con Spring Boot, es iteresante como se los consume a los endpoints desde una página web, una aplicación de escritorio, una aplicación móvil etc.

Agreguemos los archivos index.html, index.js y estilos.css en el proyecto para consumir los endpoints desde una aplicación web.

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chistes App</title>
    <link rel="stylesheet" href="estilos.css">
</head>

<body>
    <h1>Chistes</h1>

    <div id="chistes-container">
        <table id="chistes-table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Texto</th>
                    <th>Autor</th>
                    <th>Borra?</th>
                </tr>
            </thead>
            <tbody id="chistes-body"></tbody>
        </table>
    </div>

    <form id="chiste-form">
        <label for="chiste-texto">Texto:</label>
        <input type="text" id="chiste-texto" name="chiste-texto"><br>
        <label for="chiste-autor">Autor:</label>
        <input type="text" id="chiste-autor" name="chiste-autor"><br>
        <button type="submit">Agregar Chiste</button>
    </form>

    <script src="index.js"></script>
</body>

</html>

index.js

document.addEventListener('DOMContentLoaded', function () {
    const chistesContainer = document.getElementById('chistes-body')
    const chisteForm = document.getElementById('chiste-form')

    // Función para cargar todos los chistes
    function cargarChistes() {
        fetch('/chistes')
            .then(response => response.json())
            .then(data => {
                chistesContainer.innerHTML = ''
                data.forEach(chiste => {
                    const tr = document.createElement('tr')
                    tr.innerHTML = `
                        <td>${chiste.id}</td>
                        <td>${chiste.texto}</td>
                        <td>${chiste.autor}</td>
                        <td><button class="eliminar" data-id="${chiste.id}">Eliminar</button></td>
                    `
                    chistesContainer.appendChild(tr)
                })

                // Agregar event listener para los botones de eliminar
                document.querySelectorAll('.eliminar').forEach(button => {
                    button.addEventListener('click', function () {
                        const chisteId = this.getAttribute('data-id')
                        eliminarChiste(chisteId)
                    })
                })
            })
    }

    // Función para agregar un nuevo chiste
    chisteForm.addEventListener('submit', function (event) {
        event.preventDefault()
        const texto = document.getElementById('chiste-texto').value
        const autor = document.getElementById('chiste-autor').value

        const nuevoChiste = {
            texto: texto,
            autor: autor
        }

        fetch('/chistes', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(nuevoChiste)
        })
        .then(() => {
            cargarChistes()
            chisteForm.reset()
        })
    })

    // Función para eliminar un chiste
    function eliminarChiste(id) {
        fetch(`/chistes/${id}`, {
            method: 'DELETE'
        })
        .then(() => cargarChistes())
    }

    // Cargar los chistes al cargar la página
    cargarChistes()
})

estilos.css

body {
    font-family: Arial, sans-serif;
}

h1 {
    text-align: center;
}

#chiste-form {
    margin-top: 20px;
    text-align: center;
}

#chiste-form label {
    display: block;
    margin-bottom: 5px;
}

#chiste-form input {
    margin-bottom: 10px;
}

#chiste-form button {
    padding: 5px 10px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 3px;
    cursor: pointer;
}

#chiste-form button:hover {
    background-color: #45a049;
}

#chistes-container {
    margin-top: 20px;
}

#chistes-table {
    width: 100%;
    border-collapse: collapse;
}

#chistes-table th,
#chistes-table td {
    border: 1px solid #dddddd;
    text-align: left;
    padding: 8px;
}

#chistes-table th {
    background-color: #f2f2f2;
}

Cuando ejecutamos la aplicación tenemos como resultado:

aplicación web que accede a los endpoints