Skip to content

Actividad 5 - Spring Data JPA y Spring Data REST

El objetivo de esta actividad es desarrollar servicios REST utilizando Spring Data JPA y Spring Data REST con una base de datos PostgreSQL. Se implementarán dos proyectos: uno con una API REST personalizada y otro utilizando Spring Data REST para generar endpoints automáticamente.

Se utiliza Docker Compose para levantar el servicio de PostgreSQL:

docker-compose.yml
services:
postgres:
image: postgres:18.3-alpine3.23
environment:
POSTGRES_DB: dwsc
POSTGRES_USER: estudiante
POSTGRES_PASSWORD: estudiante
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
volumes:
postgres_data:

El servicio de PostgreSQL se configura con:

  • Imagen: postgres:18.3-alpine3.23
  • Base de datos: dwsc
  • Usuario: estudiante
  • Contraseña: estudiante
  • Puerto: 5432:5432

El esquema de datos se define en el script init.sql:

init.sql
CREATE TABLE IF NOT EXISTS public.student (
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY (INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1),
name text COLLATE pg_catalog."default",
surnames text COLLATE pg_catalog."default",
dni character varying(255) COLLATE pg_catalog."default",
CONSTRAINT student_pkey PRIMARY KEY (id)
) TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.student OWNER TO estudiante;
CREATE TABLE IF NOT EXISTS public.degree (
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY (INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1),
code text COLLATE pg_catalog."default",
name text COLLATE pg_catalog."default",
programme text COLLATE pg_catalog."default",
CONSTRAINT degree_pkey PRIMARY KEY (id)
) TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.degree OWNER TO estudiante;
CREATE TABLE IF NOT EXISTS public.student_degree (
student_id bigint NOT NULL,
degree_id bigint NOT NULL,
CONSTRAINT student_degree_pkey PRIMARY KEY (student_id, degree_id),
CONSTRAINT student_degree_ibfk_1 FOREIGN KEY (student_id) REFERENCES public.student (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT student_degree_ibfk_2 FOREIGN KEY (degree_id) REFERENCES public.degree (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION
) TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.student_degree OWNER TO estudiante;
INSERT INTO public.student (dni, name, surnames) VALUES
('12345678V', 'Carlos', 'López Jiménez'),
('12345678G', 'César', 'González García');
INSERT INTO public.degree (code, name, programme) VALUES
('GRAD04015', 'Grado en Ingeniería Informática', 'Plan 2015'),
('MASTER7109', 'Máster en Tecnologías y Aplicaciones en Ingeniería Informática', NULL);
INSERT INTO public.student_degree (student_id, degree_id)
SELECT s.id, d.id FROM public.student s, public.degree d
WHERE s.dni = '12345678V' AND d.code = 'GRAD04015';
INSERT INTO public.student_degree (student_id, degree_id)
SELECT s.id, d.id FROM public.student s, public.degree d
WHERE s.dni = '12345678G' AND d.code = 'MASTER7109';

Se crean tres tablas:

  • student: Alumnos con id, dni, name y surnames
  • degree: Titulaciones con id, code, name y programme
  • student_degree: Tabla relación Many-to-Many entre estudiantes y titulaciones

La aplicación utiliza Hibernate con la propiedad ddl-auto=update para crear automáticamente las tablas a partir de las entidades JPA:

spring.jpa.hibernate.ddl-auto=update

2. Actividad 5 - JPA (API REST personalizada)

Section titled “2. Actividad 5 - JPA (API REST personalizada)”

El primer proyecto utiliza Spring Data JPA con controladores REST personalizados.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>es.ual.dwsc</groupId>
<artifactId>activididad5-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>actividad5-jpa</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

Se crean las entidades Student y Degree con una relación Many-to-Many:

Student.java
package es.ual.dwsc.actividad5.domain;
import java.util.Set;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String dni;
private String name;
private String surnames;
@ManyToMany
@JoinTable(name = "student_degree", joinColumns = { @JoinColumn(name = "student_id") }, inverseJoinColumns = {
@JoinColumn(name = "degree_id") })
private Set<Degree> degrees;
}
Degree.java
package es.ual.dwsc.actividad5.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Degree {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
private String name;
private String programme;
}

Se utilizan interfaces CrudRepository con métodos de consulta personalizados:

StudentRepository.java
package es.ual.dwsc.actividad5.repository;
import org.springframework.data.repository.CrudRepository;
import es.ual.dwsc.actividad5.domain.Student;
public interface StudentRepository extends CrudRepository<Student, Long> {
Student findByName(String name);
Student findByDni(String dni);
}
DegreeRepository.java
package es.ual.dwsc.actividad5.repository;
import org.springframework.data.repository.CrudRepository;
import es.ual.dwsc.actividad5.domain.Degree;
public interface DegreeRepository extends CrudRepository<Degree, Long> {
Degree findByCode(String code);
}
StudentController.java
package es.ual.dwsc.actividad5.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.DeleteMapping;
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 es.ual.dwsc.actividad5.domain.Student;
import es.ual.dwsc.actividad5.repository.StudentRepository;
@RestController
@RequestMapping("/students")
public class StudentController {
@Autowired
StudentRepository studentRepo;
@GetMapping
public ResponseEntity<Iterable<Student>> getStudents() {
return ResponseEntity.ok(studentRepo.findAll());
}
@GetMapping("/{name}")
public ResponseEntity<Student> getStudentByName(@PathVariable String name) {
Student student = studentRepo.findByName(name);
if (student == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(student);
}
@GetMapping("/dni/{dni}")
public ResponseEntity<Student> getStudentByDni(@PathVariable String dni) {
Student student = studentRepo.findByDni(dni);
if (student == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(student);
}
@PutMapping("/{dni}")
public ResponseEntity<Student> updateStudent(@PathVariable String dni, @RequestBody Student student) {
Student existing = studentRepo.findByDni(dni);
if (existing == null) {
return ResponseEntity.notFound().build();
}
existing.setName(student.getName());
existing.setSurnames(student.getSurnames());
return ResponseEntity.ok(studentRepo.save(existing));
}
@DeleteMapping("/{dni}")
public ResponseEntity<Void> deleteStudent(@PathVariable String dni) {
Student student = studentRepo.findByDni(dni);
if (student == null) {
return ResponseEntity.notFound().build();
}
studentRepo.delete(student);
return ResponseEntity.noContent().build();
}
}
DegreeController.java
package es.ual.dwsc.actividad5.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import es.ual.dwsc.actividad5.domain.Degree;
import es.ual.dwsc.actividad5.repository.DegreeRepository;
@RestController
@RequestMapping("/degrees")
public class DegreeController {
@Autowired
DegreeRepository degreeRepo;
@GetMapping
public ResponseEntity<Iterable<Degree>> getAllDegrees() {
return ResponseEntity.ok(degreeRepo.findAll());
}
@GetMapping("/{code}")
public ResponseEntity<Degree> getDegreeByCode(@PathVariable String code) {
Degree degree = degreeRepo.findByCode(code);
if (degree == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(degree);
}
@PutMapping("/{code}")
public ResponseEntity<Degree> updateDegree(@PathVariable String code, @RequestBody Degree degree) {
Degree existing = degreeRepo.findByCode(code);
if (existing == null) {
return ResponseEntity.notFound().build();
}
existing.setName(degree.getName());
existing.setProgramme(degree.getProgramme());
return ResponseEntity.ok(degreeRepo.save(existing));
}
@DeleteMapping("/{code}")
public ResponseEntity<Void> deleteDegree(@PathVariable String code) {
Degree degree = degreeRepo.findByCode(code);
if (degree == null) {
return ResponseEntity.notFound().build();
}
degreeRepo.delete(degree);
return ResponseEntity.noContent().build();
}
}

Listar todos los estudiantes

Terminal
curl -X GET http://localhost:8080/students

Respuesta (200 OK):

[
{
"id": 1,
"dni": "12345678V",
"name": "Carlos",
"surnames": "López Jiménez",
"degrees": [...]
},
{
"id": 2,
"dni": "12345678G",
"name": "César",
"surnames": "González García",
"degrees": [...]
}
]

Listar todas las titulaciones

Terminal
curl -X GET http://localhost:8080/degrees

Respuesta (200 OK):

[
{
"id": 1,
"code": "GRAD04015",
"name": "Grado en Ingeniería Informática",
"programme": "Plan 2015"
},
{
"id": 2,
"code": "MASTER7109",
"name": "Máster en Tecnologías y Aplicaciones en Ingeniería Informática",
"programme": null
}
]

El segundo proyecto utiliza Spring Data REST para generar automáticamente endpoints RESTful a partir de los repositorios JPA.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.5</version>
<relativePath></relativePath>
<!-- lookup parent from repository -->
</parent>
<groupId>es.ual.dwsc</groupId>
<artifactId>actividad5-rest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>actividad5-rest</name>
<description>Demo project for Spring Boot</description>
<url></url>
<licenses>
<license></license>
</licenses>
<developers>
<developer></developer>
</developers>
<scm>
<connection></connection>
<developerConnection></developerConnection>
<tag></tag>
<url></url>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-explorer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-explorer</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

Las entidades son similares a las del proyecto JPA, pero con @JsonIgnore para evitar referencias circulares en la relación Many-to-Many:

Student.java
package es.ual.dwsc.actividad5_rest.domain;
import java.util.Set;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.ManyToMany;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String dni;
private String name;
private String surnames;
@JsonIgnore
@ManyToMany
@JoinTable(name = "student_degree", joinColumns = { @JoinColumn(name = "student_id") }, inverseJoinColumns = {
@JoinColumn(name = "degree_id") })
private Set<Degree> degrees;
}
Degree.java
package es.ual.dwsc.actividad5_rest.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Degree {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
private String name;
private String programme;
}

Se utilizan interfaces que extienden PagingAndSortingRepository:

StudentRepository.java
package es.ual.dwsc.actividad5_rest.repository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RestResource;
import es.ual.dwsc.actividad5_rest.domain.Student;
@RestResource(path = "students", rel = "students")
public interface StudentRepository extends CrudRepository<Student, Long> {
Student findByName(@Param("name") String name);
Student findByDni(@Param("dni") String dni);
}
DegreeRepository.java
package es.ual.dwsc.actividad5_rest.repository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RestResource;
import es.ual.dwsc.actividad5_rest.domain.Degree;
@RestResource(path = "degrees", rel = "degrees")
public interface DegreeRepository extends CrudRepository<Degree, Long> {
}

Spring Data REST genera automáticamente los siguientes endpoints:

MétodoEndpointDescripción
GET/studentsListar todos los estudiantes (paginado)
GET/students/{id}Obtener estudiante por ID
POST/studentsCrear nuevo estudiante
PUT/students/{id}Actualizar estudiante
DELETE/students/{id}Eliminar estudiante
GET/degreesListar todas las titulaciones
GET/degrees/{id}Obtener titulación por ID

Listar estudiantes (HAL JSON)

Terminal
curl -X GET http://localhost:8080/students

Respuesta (200 OK):

{
"_embedded": {
"students": [
{
"id": 1,
"dni": "12345678V",
"name": "Carlos",
"surnames": "López Jiménez"
},
{
"id": 2,
"dni": "12345678G",
"name": "César",
"surnames": "González García"
}
]
},
"_links": {
"self": { "href": "http://localhost:8080/students" },
"profile": { "href": "http://localhost:8080/profile/students" }
}
}

Listar titulaciones (HAL JSON)

Terminal
curl -X GET http://localhost:8080/degrees

Respuesta (200 OK):

{
"_embedded": {
"degrees": [
{
"id": 1,
"code": "GRAD04015",
"name": "Grado en Ingeniería Informática",
"programme": "Plan 2015"
},
{
"id": 2,
"code": "MASTER7109",
"name": "Máster en Tecnologías y Aplicaciones en Ingeniería Informática",
"programme": null
}
]
},
"_links": {
"self": { "href": "http://localhost:8080/degrees" },
"profile": { "href": "http://localhost:8080/profile/degrees" }
}
}
  1. Iniciar la base de datos PostgreSQL

    Se inicia el servicio de PostgreSQL con Docker Compose:

    Terminal window
    docker-compose up -d
  2. Compilar y ejecutar actividad5-jpa

    Terminal window
    cd actividad5-jpa
    ./mvnw spring-boot:run
  3. Compilar y ejecutar actividad5-rest

    Terminal window
    cd actividad5-rest
    ./mvnw spring-boot:run
  4. Verificar los endpoints

5.1. La Base de Datos (El contenedor principal)

Section titled “5.1. La Base de Datos (El contenedor principal)”

Si estás utilizando un motor de base de datos tradicional (como MySQL, PostgreSQL, SQL Server, etc.), Spring Boot necesita un lugar al cual conectarse. Por defecto, tienes que crear la base de datos en tu gestor antes de arrancar la aplicación (por ejemplo, ejecutando CREATE DATABASE mi_proyecto;).

Excepciones a esta regla:

  • El truco de MySQL: Si usas MySQL, puedes decirle al driver JDBC que cree la base de datos si no existe añadiendo un parámetro a tu URL en el archivo application.properties:

    spring.datasource.url=jdbc:mysql://localhost:3306/mi_proyecto?createDatabaseIfNotExist=true
  • Bases de datos en memoria (H2, HSQLDB): Si usas una base de datos en memoria para pruebas o desarrollo, no tienes que crear absolutamente nada. Spring Boot la levanta en la memoria RAM y crea todo automáticamente al iniciar.

5.2. ¿Tengo que crear cada tabla antes de poder acceder a ella?

Section titled “5.2. ¿Tengo que crear cada tabla antes de poder acceder a ella?”

No, no es necesario crear las tablas manualmente. La propiedad de configuración spring.jpa.hibernate.ddl-auto=update indica a Hibernate que cree y actualice las tablas automáticamente a partir de las entidades JPA definidas en el código.

5.3. ¿Puedo crear e inicializar un esquema de datos desde mi aplicación cuando la ejecuto por primera vez (o cada vez que se ejecuta)?

Section titled “5.3. ¿Puedo crear e inicializar un esquema de datos desde mi aplicación cuando la ejecuto por primera vez (o cada vez que se ejecuta)?”

Sí, es posible hacerlo de dos formas:

  1. init.sql automático: El script init.sql se monta en el directorio /docker-entrypoint-initdb.d/ del contenedor PostgreSQL, por lo que se ejecuta automáticamente la primera vez que se crea la base de datos.

  2. Hibernate con ddl-auto: La propiedad ddl-auto de Hibernate puede tomar diferentes valores:

    • none: No se genera nada
    • update: Se actualiza el esquema existente
    • create: Se crea el esquema eliminando el anterior
    • create-drop: Crea el esquema al iniciar y lo elimina al cerrar

    Con update, Hibernate crea las tablas automáticamente si no existen.