Construir una API RESTful con Spring Boot y MySQL desde cero

Introducción

En este blog vamos a construir una api que pueda hacer selects, inserts, updates y deletes de la base de datos del acervo de tesis de una universidad.

Análisis y diseño

El siguiente diagrama de clases modela nuestro acervo de tesis:

Diagrama de clases

Lo que equivaldría al siguiente diagrama entidad-relación:

Diagrama entidad-relación

Quiero explicarte el por qué de este diagrama E-R
Existen diferentes tipos de relaciones en los modelos (Entities),pero para este caso vamos a usar Single Table para Tesista y Asesor y usaremos Join Table para tesis-asesor y tesis-tesista. Podrías estar pensando en que la clase Persona debería ser una tabla separada de Tesista y Asesor y que también necesita un Join Table, pero en esta ocasión el problema que tenemos no necesita una implementación tan compleja y pienso que no ganariamos mucho separando (para este ejemplo en particular,repito) las tablas.

Generar el proyecto base

Para poder generar el proyecto base vamos a utilizar la página https://start.spring.io con los siguientes parámetros:

Inicializador del proyecto

Necesitarás descargar y descomprimir el zip que genera el inicializador. Ya sea que te sientas cómodo programando con NetBeans, Eclipse, IntelliJ o algún otro IDE necesitarás importar el proyecto como un proyecto Gradle.

Lo primero que haremos será agregar los datos de conexión en el archivo

acervo-api/src/main/resources/application.properties

    ## Spring DATASOURCE
    spring.datasource.url = jdbc:mysql://localhost:3306/acervo-api?useSSL=false
    spring.datasource.username = root
    spring.datasource.password = root


    ## Hibernate
    # Dialect genera querys optimizados para el gestor que usemos (o al menos lo intentará...)
    spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

Ahora vamos a crear los modelos y los controladores. Necesitaremos crear 4 nuevos paquetes

mx.uaemex.fi.acervoapi.modelo
mx.uaemex.fi.acervoapi.exception
mx.uaemex.fi.acervoapi.controlador
mx.uaemex.fi.acervoapi.repositorio

Dentro del paquete exception vamos a crear una nueva clase llamada ResourceNotFoundException de la siguiente manera:

package mx.uaemex.fi.acervoapi.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    private String tabla;
    private String columna;
    private Object valor;

    public ResourceNotFoundException( String tabla, String columna, Object valor) {
        super(String.format("%s inexistente con %s : '%s'", tabla, columna, valor));
        this.tabla = tabla;
        this.columna = columna;
        this.valor = valor;
    }

    public String getTabla() {
        return tabla;
    }

    public String getColumna() {
        return columna;
    }

    public Object getValor() {
        return valor;
    }
}

Con esta clase podremos enviar un response http con un estado 404, usando la anotación @ResponseStatus, y un body con un mensaje parecido a tesis inexistente con título: ‘hola mundo’, ya lo veremos en unas líneas más…

Escribir nuestros modelos

Ahora crearemos nuestros Entities, que nos permiten manejar la capa de persistencia.

En nuestro diagrama de clases tenemos una clase Persona, de ella heredan Tesista y Asesor. Vamos a escribir la clase Persona.

@MappedSuperclass
public class Persona {
    private String nombre;
    private String apellidoPaterno;
    private String apellidoMaterno;

    //getters y setters
}

La anotación @MappedSuperclass indica que esta clase será padre de otras clases de tipo Entity, significa también que no tiene relacionada una tabla en la base de forma directa en la cual persista datos sino que lo hace en tablas relacionadas con las clases hijas.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Table(name = "tesistas")
public class Tesista extends Persona implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String numeroCuenta;

    @OneToMany(mappedBy = "tesista", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Tesis> tesis = new ArrayList<>();
    
    //getters y setters
}
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Table(name = "asesores")
public class Asesor extends Persona implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String claveAsesor;

    private String area;

    @OneToMany(mappedBy = "asesor", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Tesis> tesis = new ArrayList<>();
    
    //getters y setters
}
@Entity
public class Tesis implements Serializable {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String titulo;

    private Date fecha;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "asesor_id")
    @JsonIgnore
    private Asesor asesor;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "tesista_id")
    @JsonIgnore
    private Tesista tesista;

Existen diferentes tipos de herencia o relación (MappedSuperclass, Single Table, Joined Table o Table Per Class) con las que podemos definir a nuestros modelos, dependiendo de lo que necesitemos podemos elegir uno u otro. Pero para el ejemplo de nuestro acervo de tesis la anotación que necesitamos es Single Table para Tesista y Asesor y Joined Table para Tesis.

Imaginemos que tenemos 2 tesis en nuestra base de datos, 3 asesores y 2 tesistas. La anotación @JsonIgnore evita un mortal Harakiri de nuestra api, sin ella, al hacer una petición GET api/tesis o /api/tesis/1 se generaría un ciclo infiniiiito ya que una tesis, tiene un asesor, un asesor puede dirigir una o más tesis y esas tesis tiene un asesor …. bueno ya me entendiste.

Ahora, la anotación @ManyToOne en el Entity Tesis para sus atributos tesista y asesor, tienen el nombre de la columna de esa tabla que hace referencia a la tabla tesistas o asesores. El Entity Tesista y Asesor tienen una anotación @OneToMany las cuales tienen un parámetro mappedBy = “asesor” o “mappedBy = “tesista” que es el atributo en el Entity Tesis.

Escribir los repositorios

Ahora tenemos que escribir nuestros repositorios, estos nos permiten tener la implementación de los métodos save, findOne, findAll, count, delete y de esta forma persistir cambios que sufren nuestros modelos.

@Repository
public interface TesistaRepositorio extends JpaRepository<Tesista, Integer>{
}
@Repository
public interface TesisRepositorio extends JpaRepository<Tesis, Integer>{
    Page<Tesis> findByTesistaId(Integer tesistaId, Pageable pageable);
    Page<Tesis> findByAsesorId(Integer asesorId, Pageable pageable);
}
@Repository
public interface AsesorRepositorio extends JpaRepository<Asesor, Integer> {
}

Los métodos findByTesistaId y findByAsesorId los usaremos para que la api pueda tener búsquedas un poco más específicas, así podremos encontrar una tesis por su id, las tesis de un tesista y las tesis de un asesor.

Ahora se va a poner bueno, la mejor parte de todas Los controladores!

Escribir los controladores

Aquí es cuando nuestro proyecto empieza a verse como una API de verdad, en los controladores tendremos nuestras rutas y nuestra lógica del negocio. Primero trataré el controlador de Tesis ya que es el más rico en contenido y complejidad.

@RestController
@RequestMapping("/api")
public class TesisControlador {
    private Tesis tesis;
    
    @Autowired
    TesisRepositorio tesisRepositorio;
    
    @Autowired
    TesistaRepositorio tesistaRepositorio;
    
    @Autowired
    AsesorRepositorio asesorRepositorio;

    @GetMapping("/tesis")
    public Page<Tesis> getTesis(Pageable pageable) {
        return tesisRepositorio.findAll(pageable);
    }

    @GetMapping("/tesis/{id}")
    public Tesis getTesisById(@PathVariable(value = "id") Integer tesisId) {
        return tesisRepositorio.findById(tesisId)
                .orElseThrow(() -> new ResourceNotFoundException("Tesis", "id", tesisId));
    }
    
    @GetMapping("/tesista/{tesistaId}/tesis")
    public Page<Tesis> getTesisByTesistaId(@PathVariable (value = "tesistaId") Integer tesistaId, Pageable pageable) {
        return tesisRepositorio.findByTesistaId(tesistaId, pageable);
        }
    
    @GetMapping("/asesor/{asesorId}/tesis")
    public Page<Tesis> getTesisByAsesorId(@PathVariable (value = "asesorId") Integer asesorId, Pageable pageable) {
        return tesisRepositorio.findByAsesorId(asesorId, pageable);
        }
    
    @PostMapping("tesista/{tesistaId}/asesor/{asesorId}/tesis")
    public Tesis createTesis(@PathVariable (value = "tesistaId") Integer tesistaId,@PathVariable (value = "asesorId") Integer asesorId,
                                 @Valid @RequestBody Tesis tesis) {
        this.tesis = tesis;
        tesistaRepositorio.findById(tesistaId).map(tesista -> {
            this.tesis.setTesista(tesista);
            return this.tesis;
        }).orElseThrow(() -> new ResourceNotFoundException("Tesista ","id",tesistaId));
        
        asesorRepositorio.findById(asesorId).map(asesor -> {
            this.tesis.setAsesor(asesor);
            return this.tesis;
        }).orElseThrow(() -> new ResourceNotFoundException("Asesor ","id",asesorId));
        
        return tesisRepositorio.save(tesis);
    }

    @PutMapping("/tesista/{tesistaId}/asesor/{asesorId}/tesis/{tesisId}")
    public Tesis updateTesis(@PathVariable (value = "tesistaId") Integer tesistaId,
                                 @PathVariable (value = "asesorId") Integer asesorId,
                                 @PathVariable (value = "tesisId") Integer tesisId,
                                 @Valid @RequestBody Tesis tesisRequest) {
        if(!tesistaRepositorio.existsById(tesistaId)) {
            throw new ResourceNotFoundException("Tesista ","id",tesistaId);
        }
        
        if(!asesorRepositorio.existsById(asesorId)) {
            throw new ResourceNotFoundException("Asesor ","id",asesorId);
        }

        return tesisRepositorio.findById(tesisId).map(tesis -> {
            tesis.setTitulo(tesisRequest.getTitulo());
            tesis.setFecha(tesisRequest.getFecha());
            return tesisRepositorio.save(tesis);
        }).orElseThrow(() -> new ResourceNotFoundException("Tesis ", "id", tesisId));
    }

    @DeleteMapping("/tesis/{id}")
    public ResponseEntity<?> deleteTesis(@PathVariable(value = "id") Integer tesisId) {
        Tesis tesis = tesisRepositorio.findById(tesisId)
                .orElseThrow(() -> new ResourceNotFoundException("Tesis", "id", tesisId));

        tesisRepositorio.delete(tesis);

        return ResponseEntity.ok().build();
    }

Como puedes darte cuenta, existe una relación entre los métodos HTTP y las operaciones del CRUD. Una petición GET están asociada a un query SELECT, una petición POST está asociada a un query INSERT, una petición PUT a un UPDATE y una petición Delete con ese mismo query. Esto no tiene por qué ser siempre así, por ejemplo, podría darse el caso de que una API que únicamente consulta información de la base tenga mapeados métodos POST y no implemente ningún GET porque no se desea tener visibles en la URL los parámetros de entrada. Esto dependerá de las necesidades del proyecto, pero en general funciona de la manera antes mencionada.

La anotación @RequestMapping(“/api”) agregará como un prefijo a todos los métodos de este controlador esa ruta, de esta forma tendremos las siguientes rutas para nuestro controlador de tesis:

Ruta Método Funcionalidad
/api/tesis GET Consultar todas las tesis paginadas
/api/tesis/1 GET Consultar la tesis con id 1
/api/tesista/1/tesis GET Consultar las tesis del tesista con id 1
/api/asesor/1/tesis GET Consultar las tesis del asesor con id 1
/api/tesista/1/asesor/2/tesis POST Registrar la tesis enviada con tesistaId = 1 y asesorId = 2
/api/tesista/1/asesor/2/tesis/1 PUT Actualizar la tesis enviada con tesisId=1, tesistaId = 1 y asesorId = 2
/api/tesis/2 DELETE Eliminar la tesis con 2

El ruta del método PUT podría ser más corta pero para efectos de validación en el controlador se escribió de esta manera.

A continuación el controlador de asesores

@RestController
@RequestMapping("/api")
public class AsesorControlador {
    @Autowired
    AsesorRepositorio asesorRepositorio;
    
    @Autowired
    TesisRepositorio tesisRepositorio;

    @GetMapping("/asesor")
    public List<Asesor> getAsesores() {
        return asesorRepositorio.findAll();
    }

    @PostMapping("/asesor")
    public Asesor createAsesor(@Valid @RequestBody Asesor asesor) {
        return asesorRepositorio.save(asesor);
    }

    @GetMapping("/asesor/{id}")
    public Asesor getAsesorById(@PathVariable(value = "id") Integer asesorId) {
        return asesorRepositorio.findById(asesorId)
                .orElseThrow(() -> new ResourceNotFoundException("Asesor", "id", asesorId));
    }

    @PutMapping("/asesor/{id}")
    public Asesor updateAsesor(@PathVariable(value = "id") Integer asesorId,
                                            @Valid @RequestBody Asesor nuevoAsesor) {

        Asesor asesor = asesorRepositorio.findById(asesorId)
                .orElseThrow(() -> new ResourceNotFoundException("Asesor", "id", asesorId));

        asesor.setClaveAsesor(nuevoAsesor.getClaveAsesor());
        asesor.setArea(nuevoAsesor.getArea());
        asesor.setNombre(nuevoAsesor.getNombre());
        asesor.setApellidoPaterno(nuevoAsesor.getApellidoPaterno());
        asesor.setApellidoMaterno(nuevoAsesor.getApellidoMaterno());

        Asesor asesorActualizado = asesorRepositorio.save(asesor);
        return asesorActualizado;
    }

    @DeleteMapping("/asesor/{id}")
    public ResponseEntity<?> deleteAsesor(@PathVariable(value = "id") Integer asesorId) {
        Asesor asesor = asesorRepositorio.findById(asesorId)
                .orElseThrow(() -> new ResourceNotFoundException("Asesor", "id", asesorId));

        asesorRepositorio.delete(asesor);

        return ResponseEntity.ok().build();
    }

Ahora el controlador de tesistas, es básicamente el mismo que el controlador anterior.

@RestController
@RequestMapping("/api")
public class TesistaControlador {
    @Autowired
    TesistaRepositorio tesistaRepositorio;
    
    @Autowired
    TesisRepositorio tesisRepositorio;

    @GetMapping("/tesista")
    public List<Tesista> getTesistas() {
        return tesistaRepositorio.findAll();
    }

    @PostMapping("/tesista")
    public Tesista createTesista(@Valid @RequestBody Tesista tesista) {
        return tesistaRepositorio.save(tesista);
    }

    @GetMapping("/tesista/{id}")
    public Tesista getTesistaById(@PathVariable(value = "id") Integer tesistaId) {
        return tesistaRepositorio.findById(tesistaId)
                .orElseThrow(() -> new ResourceNotFoundException("Tesista", "id", tesistaId));
    }

    @PutMapping("/tesista/{id}")
    public Tesista updateTesista(@PathVariable(value = "id") Integer tesistaId,
                                            @Valid @RequestBody Tesista nuevoTesista) {

        Tesista tesista = tesistaRepositorio.findById(tesistaId)
                .orElseThrow(() -> new ResourceNotFoundException("Tesista", "id", tesistaId));

        tesista.setNumeroCuenta(nuevoTesista.getNumeroCuenta());
        tesista.setNombre(nuevoTesista.getNombre());
        tesista.setApellidoPaterno(nuevoTesista.getApellidoPaterno());
        tesista.setApellidoMaterno(nuevoTesista.getApellidoMaterno());

        Tesista tesistaActualizado = tesistaRepositorio.save(tesista);
        return tesistaActualizado;
    }

    @DeleteMapping("/tesista/{id}")
    public ResponseEntity<?> deleteTesista(@PathVariable(value = "id") Integer tesistaId) {
        Tesista tesista = tesistaRepositorio.findById(tesistaId)
                .orElseThrow(() -> new ResourceNotFoundException("Tesista", "id", tesistaId));

        tesistaRepositorio.delete(tesista);

        return ResponseEntity.ok().build();
    }

Iniciamos el servidor

En la terminal ejecutamos el comando Gradle clean build bootRun para que sean borrados assets y clases generados previamente (si existieran), build crea los assets y las clases que serán despachados por el servidor tomcat, bootRun levanta el servidor Tomcat en el puerto 8080 o algún otro puerto si modificamos nuestro application.properties, y despacha nuestra aplicación.

Tenemos que tener una salida en la terminal como la siguiente:

...
2018-07-09 12:17:29.474  INFO 32740 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2018-07-09 12:17:29.477  INFO 32740 --- [  restartedMain] m.u.fi.acervoapi.AcervoApiApplication    : Started AcervoApiApplication in 4.474 seconds (JVM running for 4.928)
2018-07-09 12:17:32.386  INFO 32740 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2018-07-09 12:17:32.386  INFO 32740 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2018-07-09 12:17:32.405  INFO 32740 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 19 ms

Una vez que podamos leer estos mensajes sabremos que nuestra API está lista para recibir y responder a las peticiones que programamos en nuestros controladores. Vamos a probar.

Probando nuestra API

Finalmente, utilizamos postman para probar nuestra API. Para hacer más breve esta sección, previamente registré algunos asesores,tesistas y una tesis.


Ejecutamos un GET de asesores. GET tesis En esta imágen podemos notar que nuestros asesores pueden tener una o más tesis, pero Rodrigo es el único array en el que este sub-array no aparece vacio


Registramos una tesis sobre una impresora 3D. GET tesis


Podemos ver en la imágen anterior que el response tiene un estado 200, eso es bueno. Si consultamos las tesis, veremos algo similar a lo siguiente: GET tesis


Ejecutamos un GET de asesores nuevamente y veremos que Marta ya cuenta con una nueva tesis, la impresora 3D. GET tesis


Ahora veamos por qué las validaciones son una parte muy importante de los controladores… GET tesis Ahora tenemos sólo 4 asesores, pero al intentar hacer un registro con un asesor con id 9, nuestro ResourceNotFoundException hace lo suyo y aplaca al usuario (en este caso nosotros) diciendonos que nos portemos bien y le demos datos válidos.


Para probar que nuestro controlador de Tesis actualiza correctamente, avanzaremos 2800 años y 4 dimensiones (tal vez 3…) para poder registrar una impresora 7D en el año 4818 ._. GET tesis


Finalmente, después de registrar una tercera tesis … vamos a intentar eliminar la primera con un request DELETE. GET tesis


Una consulta general a las tesis… GET tesis


El código completo de este tutorial está disponible en GitHub a través de la siguiente liga: https://github.com/chidocodigo/2018-07-09-acervo-api

En próximos blogs seguiremos trabajando en automatizar los test, escribir migraciones,crear imágenes docker y algunos puntos más.

Gracias. Comenta y comparte.

spring  java  api 
comments powered by Disqus