En este apartado vamos a ver el funcionamiento de la parte ORM del framework llamado Spring Boot
Un ORM facilita el trabajo de creación de aplicaciones de base de datos ya que prácticamente automatiza la persistencia y consulta de datos basándose para ello en el mapeo de las entidades en objetos y viceversa.
Esqueleto
El primer paso es descargar el esqueleto de proyecto desde aquí. En el archivo pom.xml están definidas las siguientes dependencias:
spring-boot-starter-data-jpadefine que utilizamos la capa JPA de Spring (Java Persistent API)sqlite-jdbcdefine la base de datos SQLitehibernate-community-dialectsse define que vamos a usar dialectos JDBC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?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>3.0.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.ieselcaminas</groupId>
<artifactId>jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jpa</name>
<description>Proyecto demo para JPA</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.45.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-community-dialects -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
La clase java que tiene el método main es JpaApplication.java

Que contiene el siguiente código. Es común para todas las aplicaciones
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.ieselcaminas.jpa;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JpaApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
//En este método definimos nuestro propio código
@Override
public void run(String... args) {
}
}
SpringApplication.run(JpaApplication.class, args); es quien inicia la aplicación SpringBoot
Propiedades
Debemos definir un archivo llamado application.properties dentro de la carpeta resources con el siguiente contenido:
1
2
3
spring.datasource.url=jdbc:sqlite:customers.sqlite // Importante. Se guarda en raíz del proyecto
spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect
spring.jpa.hibernate.ddl-auto=validate
spring.datasource.url=jdbc:sqlite:jdbc:sqlite:customers.sqlitedefine como base de datoscustomers.sqlitedentro del directorio raíz.spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialecty aquí se define que, de todos los dialectos SQL propietarios o no, se va a usarorg.hibernate.community.dialect.SQLiteDialectspring.jpa.hibernate.ddl-auto=validateCuando la aplicación arranca, se comprueba si las entidades definidas coinciden con la las tablas de la base de datos. Si no es así, la aplicación no arranca hasta que se arregle.
Estructura de una aplicación de base de datos
Una aplicación web bien estructurada se divide en capas, cada una con responsabilidades claras:
1
Controller → Service → Repository → Base de datos
-
Controller (Controlador)
-
Recibe peticiones HTTP (GET, POST, etc.) del usuario o del frontend.
-
No contiene lógica de negocio complicada.
-
Llama al Service para realizar la acción.
-
-
Service (Servicio)
-
Contiene la lógica de negocio (reglas, validaciones, transacciones).
-
Decide cómo se usan los datos, qué crear, qué actualizar, etc.
-
Llama a los Repository para acceder a la base de datos.
-
-
Repository (Repositorio)
-
Interactúa con la base de datos usando JPA/Hibernate.
-
Solo hace consultas, inserciones, borrados.
-
-
Entity (Entidad)
- Clase que mapea los registros de las tablas de la base de datos en objetos java y viceversa
Base de datos
Creamos la base de datos SQLite y creamos la siguiente tabla:
1
2
3
4
5
CREATE TABLE customer (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL
);
Entidad
Primero hemos de definir las entidades como clases POJO (son colo las vistas en el modelo entidad-relación).
Es necesario que las columnas de la base de datos tengan el mismo nombre que los atributos de la clase. Antes de cada camelCase se introduce un
_y se pone en minúsculacamel_case
Podéis instalar el plugin JPA Buddy, que nos facilita la creación de las clases POJO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package org.ieselcaminas.jpa.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
protected Customer() {}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return id + " - " + firstName + " - " + lastName;
}
}
Vemos que esta clase se anota con @Entity para indicarle al framework que esta clase la debe tratar como una entidad que tiene persistencia en base de datos.
1
2
3
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
Estas anotaciones le indican a JPA que el atributo id es la clave primaria y que el valor se genera automáticamente. Después cada atributo de la entidad se mapea en un campo en la base de datos.
Repositorio
El siguiente paso es crear CustomerRepository:
1
2
3
4
5
6
7
8
package org.ieselcaminas.jpa.repository;
import org.ieselcaminas.jpa.entity.Customer;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
}
Extiende CrudRepository para poder realizar las operaciones CRUD <Customer, Long>. Long es el tipo de la clave primaria de la entidad Customer.
De momento no nos hace falta definir métodos de acceso a datos porque CrudRepository ya provee findById que selecciona un elemento a partir su id y findAll que devuelve una lista de todos los elementos.
Vamos a probar el repositorio creando algunos Customer. Primero hemos de crear un constructor para que Spring cree la clase customerRepository por nosotros. Esto se denomina Dependency Injection
1
2
3
4
5
private final CustomerRepository customerRepository;
public JpaApplication(CustomerRepository customerRepository){
this.customerRepository = customerRepository;
}
Este repositorio permite realizar las operaciones CRUD, así como muchas operaciones de consulta predefinidas.

Y ahora ya podemos crear algún Customer y mostrar todos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Override
@Transactional // IMPORTANTE - Para no dar errores de sesiones.
public void run(String... args) {
Customer c = new Customer("Pepe", "García");
//El repositorio es donde están todos los métodos que tratan con la base de datos.
//En este caso está haciendo un INSERT ya que el objeto es nuevo
this.customerRepository.save(c);
//Pero también puedo modificar un registro
c.setFirstName("Juan");
this.customerRepository.save(c);
//Vamos a seleccionar el Customer con id 1
//Se escribe 1L porque es un dato escrito a mano de tipo long
Optional<Customer> clienteOp = this.customerRepository.findById(1L);
clienteOp.ifPresent(System.out::println);
//Si queremos acceder al objeto Customer
if (clienteOp.isPresent()){
c = clienteOp.get();
System.out.println(c);
}
//El método findAll devuelve todos los registros de la entidad asociada
this.customerRepository.findAll().forEach(System.out::println);
//En este código estamos guardando los datos en un Iterable que es lo que devuelven los métodos findAll
Iterable<Customer> l = this.customerRepository.findAll();
for (Customer customer : l) {
System.out.println(customer);
}
//Cuidado que si no existe el registro con ID 5, saltará una excepción
try {
c = this.customerRepository.findById(5L).get();
this.customerRepository.delete(c);
System.out.println(c);
} catch (NoSuchElementException e) {
System.out.println(" No existe el elemento con ID 5");
}
// Vamos a hacer una búsqueda por FirstName que nos devuelva un lista de clientes que tengan ese apellido
clientes = this.customerRepository.findCustomerByFirstName("Pepe");
clientes.forEach(System.out::println);
// Que nos devuelve sólo uno
c = this.customerRepository.findFirstCustomerByFirstName("Pepe");
/**
También se puede usar findOneCustomerByFirstName, pero si hay más de un usuario, saltará una excepción
*/
}
Tal vez os aparezca
findCustomerByFirstNameen rojo. Si es así, con el botón secundario del ratón, seleccionad la opciónShow Context Actions
Ahora seleccionad la opción
Create repository method 'findCustomerByFirstName' in 'CustomerRepository'
Esto os debe haber creado el método en el repositorio:
1
2
3
4
5
6
7
8
9
10
11
package org.ieselcaminas.jpa.repository;
import org.ieselcaminas.jpa.entity.Customer;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
// Como es una interface, sólo se especifica la signatura
List<Customer> findCustomerByFirstName(String firstName);
}
Construcción de Consultas
En el repositorio se definen todas aquellas consultas que sean necesarias. Hay algunas consultas que ya están hechas. Las puedes ver en org.springframework.data.repository.CrudRepository.
En general, no hace falta escribir el cuerpo de las consultas SQL ya que lo hace Spring Boot automáticamente por nosotros a partir del nombre del método y los parámetros. Por ejemplo:
1
2
3
// En CustomerRepository
public List<Customer> findCustomersByFirstName(String firstName);
public Customer findCustomerByFirstNameAndLastName(String firstName, String lastName)
Si creamos el método findCustomersByFirstName(String firstName) devolvería aquellos con dicho firstName y findCustomerByFirstNameAndLastName que coincida el firstName y el lastName.
Fijaos en la diferencia:
- Si empieza por
findCustomerBydevuelve un únicoCustomer- Si empieza por
findCustomersBy(cons) devuelve una lista deCustomer
Tienes el listado completo de Querys aquí.
Si la consulta que queremos hacer no se puede crear con el asistente, siempre se puede escribir a mano el SQL de la misma.
1
2
@Query("SELECT COUNT(c) FROM Customer c")
public int countAllRecords();
Notes
Vamos a implementar la funcionalidad de poder crear notas que se adjuntan a un cliente. Es decir, vamos a crear una relación 1:N
Primero creamos la tabla note:
1
2
3
4
5
6
CREATE TABLE note (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"text" TEXT NOT NULL,
id_customer INTEGER,
CONSTRAINT note_customer_FK FOREIGN KEY (id_customer) REFERENCES customer(id)
);
Ahora creamos la entidad Note:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package org.ieselcaminas.jpa;
import jakarta.persistence.*;
@Entity
public class Note {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String text;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name="id_customer")
private Customer customer;
protected Note() {}
public Note(String text, Customer customer) {
this.text = text;
this.customer = customer;
}
public Long getId() {
return id;
}
public String getText() {
return text;
}
public Customer getCustomer() {
return customer;
}
@Override
public String toString() {
return text;
}
}
Mediante …
1
2
3
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name="customer_id")
private Customer customer;
.. estamos creando un relación N:1, la columna se llamará customer_id y la entidad relacionada es Customer
Y ahora vamos a ver la parte 1, es decir, Customer
1
2
@OneToMany(mappedBy = "customer", fetch=FetchType.EAGER)
private List<Note> notes = new ArrayList<>;
En este caso le indicamos mappedBy = "customer" donde customer es el nombre del campo en la relación N
Ahora creamos los setters.
1
2
3
4
5
6
7
public List<Note> getNotes(){
return this.notes;
}
public void addNote(Note note){
this.notes.add(note);
}
Y generamos el repositorio para Note
1
2
3
4
5
6
7
8
package org.ieselcaminas.jpa.repository;
import org.ieselcaminas.jpa.entity.Note;
import org.springframework.data.repository.CrudRepository;
public interface NoteRepository extends CrudRepository<Note, Long> {
}
Y lo inyectamos en el constructor:
1
2
3
4
5
private final NoteRepository noteRepository;
public JpaApplication(CustomerRepository customerRepository, NoteRepository noteRepository){
this.customerRepository = customerRepository;
this.noteRepository = noteRepository;
}
Y ya podemos crear y consultar notas:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
// Nos evitamos conflictos al trabajar en una única transacción
@Transactional
public void run(String... args) {
//Creamos una nota
// 1º el cliente
Customer c = customerRepository.findById(1L).get();
// Y ahora la nota
Note n = new Note("Primera nota", c);
noteRepository.save(n);
Customer c2 = customerRepository.findById(2L).get();
n = new Note("Primera nota María", c2);
noteRepository.save(n);
customerRepository.findCustomersByFirstName("Pepe").forEach(System.out::println);
}
Servicios
Un Service es una clase que:
-
Recibe peticiones del Controller
-
Aplica lógica (validaciones, reglas, cálculos…)
-
Usa los Repository para acceder a la base de datos
-
Devuelve el resultado
👉 Es el punto intermedio entre la web (o interfaz de comandos, o interfaz gráfica) y la base de datos.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.ieselcaminas.jpa.service;
import org.ieselcaminas.jpa.entity.Customer;
import org.ieselcaminas.jpa.entity.Note;
import org.ieselcaminas.jpa.repository.CustomerRepository;
import org.ieselcaminas.jpa.repository.NoteRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class CustomerService {
private final CustomerRepository customerRepository;
private final NoteRepository noteRepository;
public CustomerService(CustomerRepository customerRepository,
NoteRepository noteRepository) {
this.customerRepository = customerRepository;
this.noteRepository = noteRepository;
}
// Crear cliente
public Customer createCustomer(String firstName, String lastName) {
return customerRepository.save(new Customer(firstName, lastName));
}
// Añadir nota a un cliente
public Note addNote(Long customerId, String content) {
Optional<Customer> customer = customerRepository.findById(customerId); // clave
Customer c = customer.orElseThrow();
return new Note(content, c);
}
// Obtener notas de un cliente
public List<Note> getNotes(Long customerId) {
return noteRepository.getNotesByCustomerId(customerId);
}
// Obtener cliente
public Customer getCustomer(Long id) {
return customerRepository.findById(id).orElseThrow();
}
}
Controladores
Además de los repositorios, es común crearse un controlador que haga de intermediario entre la base de datos y la entidad.
En el siguiente ejemplo tenemos un método en el controlador de Customer que permite la creación de un Customer desde la entrada estándar.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.ieselcaminas.jpa.controller;
import org.ieselcaminas.jpa.entity.Customer;
import org.ieselcaminas.jpa.service.CustomerService;
import org.springframework.stereotype.Controller;
import java.util.Scanner;
@Controller
public class CustomerController {
private final CustomerService customerService;
public CustomerController(CustomerService customerService){
this.customerService = customerService;ç
}
public void createCustomer(){
String firstName, lastName;
Scanner sc = new Scanner(System.in);
System.out.println("Enter customer's first name:");
firstName = sc.nextLine();
System.out.println("Enter customer's last name:");
lastName = sc.nextLine();
this.customerService.createCustomer(firstName, lastName);
}
}
Y lo usamos en el programa principal JpaApplication:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
......
private final CustomerController customerController;
public JpaApplication(CustomerRepository customerRepository,
NoteRepository noteRepository, CustomerController customerController){
this.customerRepository = customerRepository;
this.noteRepository = noteRepository;
this.customerController = customerController;
}
.....
@Override
@Transactional
public void run(String... args) {
this.customerController.createCustomer();
this.customerRepository.findAll().forEach(System.out::println);
}
Banco
Vamos a crear una relación n:m , como la que vimos en un apartado anterior, entre cliente y cuenta corriente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE clientes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL
);
CREATE TABLE cuentas_corrientes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
cantidad REAL
);
CREATE TABLE cliente_cuenta (
cliente_id INTEGER NOT NULL,
cuenta_id INTEGER NOT NULL,
PRIMARY KEY (cliente_id, cuenta_id),
FOREIGN KEY (cliente_id) REFERENCES clientes(id) ON DELETE CASCADE,
FOREIGN KEY (cuenta_id) REFERENCES cuentas_corrientes(id) ON DELETE CASCADE
);
Y application.properties
1
2
3
spring.datasource.url=jdbc:sqlite:banco.sqlite
spring.datasource.driver-class-name=
spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect
La entidad Cliente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package org.ieselcaminas.jpa.entity;
import jakarta.persistence.*;
@Entity
// Se le puede indicar que el nombre de la tabla no es "cliente", nombre por defecto que es // igual al nombre la entidad (Cliente), sino "clientes"
@Table(name = "clientes")
public class Cliente {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String nombre;
// Siempre un constructor por defecto
public Cliente() {
}
public Cliente(String nombre) {
this.nombre = nombre;
}
// Getters y Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
@Override
public String toString(){
return this.nombre;
}
}
Y la entidad CuentaCorriente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package org.ieselcaminas.jpa.entity;
import jakarta.persistence.*;
import org.ieselcaminas.jpa.Cliente;
import java.util.HashSet;
import java.util.Set;
@Entity
// Se le puede indicar que el nombre de la tabla no es "cuenta_corriente", nombre por
// defecto que es igual al nombre la entidad (CuentaCorriente), sino "cuentas_corrientes"
@Table(name = "cuentas_corrientes")
public class CuentaCorriente {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String nombre;
@Column(nullable = false)
private double cantidad;
//Siempre debe tener un constructor vacío
public CuentaCorriente() {
}
//Se pueden hacer tantos constructores como deseemos
public CuentaCorriente(String nombre) {
this.nombre = nombre;
}
// Getters y Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
//Le decimos que, en la entidad Cliente, las cuentas se almacenan en el atributo con nombre cuentas
@ManyToMany(mappedBy = "cuentas", fetch = FetchType.EAGER)
//Hay que inicializar la lista siempre
private Set<Cliente> clientes = new HashSet<>();
public Set<Cliente> getClientes() {
return this.clientes;
}
public void setClientes(Set<Cliente> clientes) {
this.clientes = clientes;
}
// Se puede crear un método para añadir la entidad relacionada a la lista.
// En otro caso, habría que usar cuenta.getClientes().add(cliente)
public void addCliente(Cliente cliente){
this.clientes.add(cliente);
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public double getCantidad() {
return cantidad;
}
public void setCantidad(double cantidad) {
this.cantidad = cantidad;
}
// Otros setters
public void ingresar(double cantidad){
if (cantidad > 0 )
this.cantidad += cantidad;
}
public void retirar(double cantidad){
if (cantidad > 0 )
this.cantidad -= cantidad;
}
@Override
public String toString(){
return this.nombre + " - " + this.cantidad;
}
}
Y ahora ya podemos crear la relación n:m:
En la entidad Cliente, añadimos la relación :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* IMPORTANTE fetch = FetchType.EAGER. En caso contrario es FetchType.LAZY y da
* error porque no se inicializa la lista de cuentas
*/
@ManyToMany(fetch = FetchType.EAGER)
/*
En este caso le decimos:
- que la tabla se llama cliente_cuenta
- que mi columna id se llama en esa tabla cliente_id
- que la columna de la cuenta se llama cuenta_id en dicha tabla
*/
@JoinTable(
name = "cliente_cuenta",
joinColumns = @JoinColumn(name = "cliente_id"),
inverseJoinColumns = @JoinColumn(name = "cuenta_id")
)
// `cuentas` es el nombre que tiene el atributo `mappedBy` de
// la relación `ManyToMany` en `CuentaCorriente`
// Inicializar siempre el Set
private Set<CuentaCorriente> cuentas = new HashSet<>();
public Set<CuentaCorriente> getCuentas() {
return this.cuentas;
}
public void setCuentas(Set<CuentaCorriente> cuentas) {
this.cuentas = cuentas;
}
// Se puede crear un método para añadir la entidad relacionada a la lista.
// En otro caso, habría que usar cliente.getCuentas().add(cuenta)
public void addCuenta(CuentaCorriente cuenta) {
this.cuentas.add(cuenta);
}
Y en la entidad CuentaCorriente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Le decimos que, en la entidad Cliente, las cuentas se almacenan en el atributo con nombre cuentas
@ManyToMany(mappedBy = "cuentas", fetch = FetchType.EAGER)
// Hay que inicializar la lista siempre
private Set<Cliente> clientes = new HashSet<>();
public Set<Cliente> getClientes() {
return this.clientes;
}
public void setClientes(Set<Cliente> clientes) {
this.clientes = clientes;
}
// Se puede crear un método para añadir la entidad relacionada a la lista.
// En otro caso, habría que usar cuenta.getClientes().add(cliente)
public void addCliente(Cliente cliente){
this.clientes.add(cliente);
}
Los repositorios
1
2
3
4
5
6
package org.ieselcaminas.jpa.repository;
import org.ieselcaminas.jpa.entity.Cliente;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ClienteRepository extends JpaRepository<Cliente, Long> {}
y
1
2
3
4
5
6
package org.ieselcaminas.jpa.repository;
import org.ieselcaminas.jpa.entity.CuentaCorriente;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CuentaCorrienteRepository extends JpaRepository<CuentaCorriente, Long> {}
Y ahora ya podemos probarlo.
Vamos a hacerlo mediante un Servicio. En un servicio vamos creando los métodos relacionados con una entidad y que no deban estar en el repositorio.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package org.ieselcaminas.jpa.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Se pueden hacer tantos servicios como deseemos, pero siempre hemos de crear métodos relacionados
* en ellos.
*/
@Service
public class ClienteService {
/*
La anotación @Autowired indica a Spring que nos "auto inyecte" esta clase, en este caso ClienteRepository y
CuentaCorrienteRepository.
De esta forma ya no es necesario inyectarlas en el constructor
*/
@Autowired
private ClienteRepository clienteRepository;
@Autowired
private CuentaCorrienteRepository cuentaCorrienteRepository;
public void crearClienteConCuenta(String nombreCliente, String nombreCuenta) {
Cliente cliente = new Cliente(nombreCliente);
CuentaCorriente cuenta = new CuentaCorriente();
cuenta.setNombre(nombreCuenta);
cuenta.addCliente(cliente);
cliente.addCuenta(cuenta);
this.clienteRepository.save(cliente);
this.cuentaCorrienteRepository.save(cuenta);
}
}
Y ahora en main lo probamos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.ieselcaminas.jpa;
import org.ieselcaminas.jpa.service.ClienteService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JpaApplication implements CommandLineRunner {
private final ClienteService clienteService;
public JpaApplication(ClienteService clienteService) {
this.clienteService = clienteService;
}
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
//En este método definimos nuestro propio código
@Override
@Transactional
public void run(String... args) {
this.clienteService.crearClienteConCuenta("Mengano", "Cmengano");
}
}
Vamos a crear otro método que permita asociar una cuenta con varios clientes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void crearCuentaConClientes(String nombreCuenta) {
CuentaCorriente cuenta = new CuentaCorriente();
cuenta.setNombre(nombreCuenta);
cuenta.setCantidad(100);
Cliente cliente1 = new Cliente("Cliente 1");
Cliente cliente2 = new Cliente("Cliente 2");
cliente1.getCuentas().add(cuenta); // o cliente1.addCuenta(cuenta);
cuenta.getClientes().add(cliente1); // o cuenta.addCliente(cliente);
cliente2.getCuentas().add(cuenta);
cuenta.getClientes().add(cliente2);
this.cuentaCorrienteRepository.save(cuenta);
this.clienteRepository.save(cliente1);
this.clienteRepository.save(cliente2);
}
y en main
1
2
3
4
5
@Override
@Transactional
public void run(String... args) {
this.clienteService.crearCuentaConClientes("CuentaConMásDeUnCliente");
}
Vamos a usar el asistente para creación de relaciones que trae incorporado JPA Buddy, suponiendo que ya habéis creado las tablas.
-
Abre la entidad
ClienteEn la parte superior de la ventana aparece una barra de botones:
Haz clic en el tercero por la izquierda y elige la opción AssociationAhora elige las opciones que se muestran a continuación
Pulsa el botón OK. Aparecerá otra ventana para las opciones del atributo inverso:
Vuelve a pulsar OK
Como resultado te debe haber generado el código para las dos relaciones.
Tal vez quieras, hacer addCuenta() en Cliente y addCliente en Cuenta
Resumen
Los pasos para crear una aplicación con Spring boot son:
- definir las dependencias en el archivo
pom.xml - crear las entidades donde se mapean los campos de la tabla con los atributos de la clase POJO
- crear los repositorios. Uno para cada entidad. Se puede ir definiendo los métodos conforme nos vayan haciendo falta.
- crear los controladores, donde crearemos métodos que usarán las entidades y los repositorios.
- crear la aplicación, según el ejemplo.
Creación de una mini web.
Vamos a crear una web a partir de la bd de Customer. Va a ser muy sencilla: mostrará los nombres de los clientes y, a continuación, la lista de las notas.
Primero, vamos a añadir las dependencias para desarrollar webs con Spring:
1
2
3
4
5
6
7
8
9
10
<!-- Añade la funcionalidad de servidor de aplicaciones -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Añade el motor de plantillas html `thymeleaf` -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Para ejecutar el servidor Web hay que lanzar el siguiente comando en la terminal ./mvnw spring-boot:run o hacer clic en el botón verde de flecha.

Y arrancará el el puerto 8080, por defecto

Cada vez que modifiquemos algo del proyecto, hay que parar y volver a arrancar
Aparecerá un mensaje de error, pero es normal, ya que no hemos creado todavía ninguna ruta en un controlador. Para crear un nuevo controlador para web:

Y luego, elegimos Controller

Cuidado que hay que hacer pública la clase
En CustomerController creamos la siguiente ruta:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ieselcaminas.jpa.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/")
public class CustomerWebController {
@GetMapping
public String inicio(Model model){
return "index";
}
}
@GetMapping("/")le indica que este método se encarga de la ruta/, es decir, la raíz de la webindexes el nombre de la plantilla que va a mostrar. Usamos el motor de plantillasThymeleaf
Ahora, creamos la plantilla resources/templates/index.html (el nombre de la plantilla debe coincidir con la cadena devuelta por el controlador, en este caso index) con el siguiente código:
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Web de clientes</h1>
</body>
</html>
Reinicia el servidor y debe aparecer la siguiente ventana:

Si es así, es que todo ha ido bien.
Listar clientes
Vamos a mostrar los clientes en la web. El proceso consta de las siguientes partes:
-
Obtener los datos de todos los clientes
- Pasárselos a la plantilla
index - Hacer un
foreachen la plantilla para recorrer todos los clientes.
Para obtener todos los clientes, usaremos el método findAll que ya está implementado en el repositorio
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.ieselcaminas.jpa.controller;
import org.ieselcaminas.jpa.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/") // La ruta a la página de inicio
public class CustomerController {
// Dependencias
private final CustomerRepository customerRepository;
CustomerController(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
@GetMapping // Ruta por defecto del Controlador, es decir, `/`
public String inicio(Model model){
model.addAttribute("customers", customerRepository.findAll());
return "index";
}
}
El Model es la forma que tenemos para pasar datos a la plantilla html.
model.addAttribute("customers", customerRepository.findAll()); indica que la variable customers va a estar disponible en la plantilla index
Ahora vamos a pintarlos en la plantilla.
🧾 ¿Qué es una plantilla?
Es un archivo HTML que usa Thymeleaf, un motor de plantillas muy usado con Spring Boot.
👉 Sirve para mezclar HTML + datos Java que vienen del servidor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Web de clientes</h1>
<ul>
<li th:each="customer : ${customers}"
th:text="${customer.firstName + ' ' + customer.lastName}">
Cliente de ejemplo
</li>
</ul>
</body>
</html>
Estructura básica
1
2
<!DOCTYPE html>
<html lang="en">
Esto es HTML normal. Define el documento.
Título de la página
1
<h1>Web de clientes</h1>
Texto fijo. No depende de Java.
Bucle con Thymeleaf
1
2
<ul>
<li th:each="customer : ${customers}"
Aquí empieza lo interesante:
🔹 th:each
Es como un for en Java.
👉 Significa:
“Para cada
customerdentro de la listacustomers”
🔹 ${customers}
Es una variable que viene del controlador Java:
1
model.addAttribute("customers", lista);
👉 Es decir:
customers= lista de objetosCustomer
Mostrar datos
1
th:text="${customer.firstName + ' ' + customer.lastName}"
Esto hace:
👉 Sustituye el contenido del <li> por:
1
Nombre Apellido
Ejemplo real:
1
2
Juan Pérez
Ana García
🔹 ¿Qué significa exactamente?
customer.firstName→ nombrecustomer.lastName→ apellido+ ' '→ añade un espacio
👉 Es como en Java:
1
customer.getFirstName() + " " + customer.getLastName()
Resultado final en el navegador
Si tienes 3 clientes, el HTML generado será:
1
2
3
4
5
<ul>
<li>Juan Pérez</li>
<li>Ana García</li>
<li>Luis López</li>
</ul>
Texto por defecto
1
Cliente de ejemplo
Esto sirve para:
👉 Ver algo en el HTML si Thymeleaf no se ejecuta 👉 Es reemplazado automáticamente cuando la app corre
Notas
Ahora vamos a mostrar las notas de los clientes.
Objetivo
Mostrar:
- Cliente (nombre + apellido)
- Debajo, su lista de notas
Plantilla modificada
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Clientes</title>
</head>
<body>
<h1>Web de clientes</h1>
<ul>
<li th:each="customer : ${customers}">
<!-- Nombre del cliente -->
<span th:text="${customer.firstName + ' ' + customer.lastName}"></span>
<!-- Lista de notas. Otro for por cada nota del cliente -->
<ul>
<li th:each="note : ${customer.notes}"
th:text="${note.text}">
Nota de ejemplo
</li>
</ul>
</li>
</ul>
</body>
</html>
Explicación
🔁 Bucle exterior
1
<li th:each="customer : ${customers}">
👉 Recorre todos los clientes
🔁 Bucle interior
1
<li th:each="note : ${customer.notes}">
👉 Para cada cliente, recorre sus notas
Esto es como en Java:
1
2
3
4
5
for (Customer c : customers) {
for (Note n : c.getNoteList()) {
// mostrar nota
}
}
Ejemplo de resultado
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<ul>
<li>
<!-- Nombre del cliente -->
<span>Pepe Navarro</span>
<!-- Lista de notas -->
<ul>
<li>Llamar mañana</li>
<li>Enviar correo</li>
</ul>
</li>
<li>
<!-- Nombre del cliente -->
<span>María García</span>
<!-- Lista de notas -->
<ul>
<li>Pedir cita</li>
</ul>
</li>
</ul>
Y este es el resultado:

Cómo servir archivos státicos
Es habitual en las páginas web mostrar archivos estáticos tal como imágenes, hojas de estilo, scripts …
En desarrollo, lo habitual es tener configurado un directorio para estas imágenes. Vamos a suponer que este directorio es /home-tu-nombre-de-usuario/spring/uploads/img.
El primer paso es añadir una propiedad en el archivo application.properties
1
upload.img.path=/home/tu-nombre-de-usuario/spring/uploads/img/
Y ahora configurar el spring para que sirva los archivos que residan en ese directorio. Este clase se puede poner al mismo nivel que JpaApplication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.ieselcaminas.jpa;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
// Este valor es el mismo que hemos configurado en application.properties
@Value("${upload.img.path}")
private String uploadImgPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/img/**") // Lo servimos en la ruta /uploads/img/
.addResourceLocations("file:" + uploadImgPath);
}
}
Y ya podemos tener imágenes guardadas en dicho directorio y servirlas como recurso web en /uploads/img/
Para usarlo con ThymeLeaf, suponiendo que el ` Customer tiene un campo image`
1
<img th:src="@{/uploads/img/{img}(img=${customer.image})}" />


