Ejercicio API + CRUD Biblioteca

En github:

https://github.com/juanpablofuentes/JavaNetmind/tree/main/SpringBootVistas

Entidades:

package com.trifulcas.SpringBootBiblioteca.model;

import java.util.Set;

import com.fasterxml.jackson.annotation.JsonIgnore;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;

@Entity
@Table(name = "autor")
public class Autor {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int idautor;

    private String nombre;

    // Como todas las indicaciones de tablas intermedias y foreign keys
    // Las hemos puesto en libro aquí podemos usar solo autores
    @ManyToMany(mappedBy = "autores")
    @JsonIgnore // Para evitar bucles infinitos
    private Set<Libro> libros;

    // Constructor vacío
    public Autor() {
    }

    // Constructor con parámetros
    public Autor(String nombre) {
        this.nombre = nombre;
    }

    // Getters y Setters
    public int getIdautor() {
        return idautor;
    }

    public void setIdautor(int idautor) {
        this.idautor = idautor;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public Set<Libro> getLibros() {
        return libros;
    }

    public void setLibros(Set<Libro> libros) {
        this.libros = libros;
    }

    // Método toString (opcional)
    @Override
    public String toString() {
        return "Autor{" +
                "idautor=" + idautor +
                ", nombre='" + nombre + '\'' +
                '}';
    }
}
package com.trifulcas.SpringBootBiblioteca.model;

import java.util.Set;

import com.fasterxml.jackson.annotation.JsonIgnore;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;

@Entity
@Table(name = "genero")
public class Genero {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int idgenero;

    private String nombre;

    // Carga perezosa, pero al serializar siempre carga
    // Mapeamos por 'genero', que es el campo en la entidad relacionada
    @OneToMany(mappedBy = "genero")
    @JsonIgnore  // Quitar este campo NO de la entidad SI de la srialización
    private Set<Libro> libros;

    // Constructor vacío
    public Genero() {
    }

    // Constructor con parámetros
    public Genero(String nombre) {
        this.nombre = nombre;
    }

    // Getters y Setters
    public int getIdgenero() {
        return idgenero;
    }

    public void setIdgenero(int idgenero) {
        this.idgenero = idgenero;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public Set<Libro> getLibros() {
        return libros;
    }

    public void setLibros(Set<Libro> libros) {
        this.libros = libros;
    }

    // Método toString (opcional)
    @Override
    public String toString() {
        return "Genero{" +
                "idgenero=" + idgenero +
                ", nombre='" + nombre + '\'' +
                '}';
    }
}

package com.trifulcas.SpringBootBiblioteca.model;

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 jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;

@Entity
@Table(name = "libro")
public class Libro {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int idlibro;

    // Carga ansiosa. El nombre del campo es el mapping en genero
    @ManyToOne
    @JoinColumn(name = "idgenero")
    private Genero genero;

    private String titulo;

    private int paginas;

    // Cuando tenemos many to many tenemos que elegir donde montamos
    // toda la relación con la especificación de la tabla intermedia
    // Y los campos relacionados. Yo lo he hecho aquí pero podría ser
    // en autor, es lo mismo
    @ManyToMany
    @JoinTable(
        name = "libro_autor",
        joinColumns = @JoinColumn(name = "idlibro"),
        inverseJoinColumns = @JoinColumn(name = "idautor")
    )
    // Utilizamos set porque son valores únicos pero podría ser List
    private Set<Autor> autores;

    // Constructor vacío
    public Libro() {
    }

    // Constructor con parámetros
    public Libro(Genero genero, String titulo, int paginas) {
        this.genero = genero;
        this.titulo = titulo;
        this.paginas = paginas;
    }

    // Getters y Setters
    public int getIdlibro() {
        return idlibro;
    }

    public void setIdlibro(int idlibro) {
        this.idlibro = idlibro;
    }

    public Genero getGenero() {
        return genero;
    }

    public void setGenero(Genero genero) {
        this.genero = genero;
    }

    public String getTitulo() {
        return titulo;
    }

    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }

    public int getPaginas() {
        return paginas;
    }

    public void setPaginas(int paginas) {
        this.paginas = paginas;
    }

    public Set<Autor> getAutores() {
        return autores;
    }

    public void setAutores(Set<Autor> autores) {
        this.autores = autores;
    }

    // Método toString (opcional)
    @Override
    public String toString() {
        return "Libro{" +
                "idlibro=" + idlibro +
                ", genero=" + genero +
                ", titulo='" + titulo + '\'' +
                ", paginas=" + paginas +
                '}';
    }
}

Repositorios:

package com.trifulcas.SpringBootBiblioteca.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.trifulcas.SpringBootBiblioteca.model.Autor;
import com.trifulcas.SpringBootBiblioteca.model.Genero;

public interface AutorRepository extends JpaRepository<Autor, Integer> {
	List<Genero> findByLibrosGeneroNombreContaining(String cad);

	List<Autor> findByLibrosTituloContaining(String cad);

}

package com.trifulcas.SpringBootBiblioteca.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.trifulcas.SpringBootBiblioteca.model.Genero;

public interface GeneroRepository extends JpaRepository<Genero, Integer> {
// Dentro del repositorio podemos crear consultas de una manera 'mágica'
	List<Genero> findByNombreContaining(String nombre);
}

package com.trifulcas.SpringBootBiblioteca.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.trifulcas.SpringBootBiblioteca.model.Libro;

public interface LibroRepository extends JpaRepository<Libro, Integer> {

	List<Libro> findByPaginasBetweenOrderByPaginasAsc(Integer a, Integer b);
	List<Libro> findByAutoresIdautor(Integer id);
	List<Libro> findByTituloContaining(String cadena);
}

Controladores Vistas

package com.trifulcas.SpringBootVistas.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
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.RequestMapping;

import com.trifulcas.SpringBootVistas.model.Autor;
import com.trifulcas.SpringBootVistas.repository.AutorRepository;

// La anotación nos indica que es un controlador normal, que requerirá una vista
@Controller
// Nos indica la ruta de entrada general a este controlador
@RequestMapping("/autor")
public class AutorController {

	// Necesitamos acceder a los datos por lo tanto creamos el repositorio
	// el autowired nos hace inyección de dependencia automática
	@Autowired
	AutorRepository autorRepository;

	// Aquí especificamos que accedemos vía get
	@GetMapping("")
	// Pongo el parámetro Model que nos permita pasar datos a la vista
	public String getAutores(Model model) {
		try {
			// Obtengo los datos como en la API rest
			List<Autor> autores = autorRepository.findAll();
			// Paso la información a la vista vía model
			// La vista tendrá una variable 'autores' con la lista de autores
			model.addAttribute("autores", autores);
			// Le digo que me cargue la vista 'autores' la buscará en templates
			return "autores";
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}

	// Mapeo que me pasen un parámetro id
	@GetMapping("/{id}")
	// Tengo el parámetro id que me pasan y el model para pasar datos a la vista
	public String getAutor(Model model, @PathVariable Integer id) {
		try {
			// Recupero el autor
			Autor autor = autorRepository.findById(id).orElse(null);
			if (autor != null) {
				// Lo paso a la vista
				model.addAttribute("autor", autor);
				// Devuelvo la vista
				return "autor";
			} else {
				// Me he creado una vista para mostrar un error
				return "error";
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}

	// El primer mapeo es con get para simplemente mostrar la vista
	@GetMapping("/add")
	// Pasamos como parámetro el autor para que la vista lo pueda tener disponible
	public String addAutor(Autor autor) {
		// Simplemente mostramos la vista
		return "addautor";
	}

	// Cuando desde la vista nos añaden el autor entramos por 'POST'
	@PostMapping("/add")
	// Con @Validated recuperamos los datos y los metemos dentro de una entidad,
	// spring lo hace solo
	// En result se guardan los datos de la validación, es decir ¿Lo que nos mandan
	// son datos válidos? Si es que sí, no dará error, en caso contrario
	// en result tenemos la lista de errores
	public String addAutorDatos(@Validated Autor autor, BindingResult result) {
		System.out.println(autor);
		System.out.println(result);
		try {
			// Si hay algún error volvemos a mostrar la vista y además
			// fields.error tendrá la información de los errores
			if (result.hasErrors()) {
				return "addautor";
			}
			// Si no hay ningún error guardamos el autor
			autorRepository.save(autor);
			// Y en vez de devolver una vista, redirigimos al índice
			return "redirect:/autor";
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}

	@GetMapping("/edit/{id}")
	// Usamos el model porque tenemos que recuperar al autor
	public String addAutor(@PathVariable Integer id, Model model) {
		try {
			// Primero, buscamos el autor que se quiere editar
			Autor autor = autorRepository.findById(id).orElse(null);
			if (autor != null) {
				// Añadimos el autor al modelo
				model.addAttribute("autor", autor);
				return "updateautor";
			} else {
				return "error";
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}

	}

	@PostMapping("/update/{id}")
	public String updateAutor(@PathVariable Integer id, @Validated Autor autor, BindingResult result) {
		System.out.println(autor);
		try {
			if (result.hasErrors()) {
				return "updateautor";
			}
			autorRepository.save(autor);
			return "redirect:/autor";
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}

	@GetMapping("/delete/{id}")
	// Usamos el model porque tenemos que recuperar al autor
	public String deleteAutor(@PathVariable Integer id, Model model) {
		try {
			// Primero, buscamos el autor que se quiere editar
			// Añadimos el autor al modelo
			autorRepository.deleteById(id);
			return "redirect:/autor";

		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}

	}
}

package com.trifulcas.SpringBootVistas.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
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.RequestMapping;

import com.trifulcas.SpringBootVistas.model.Genero;
import com.trifulcas.SpringBootVistas.repository.GeneroRepository;

@Controller
@RequestMapping("/genero")
public class GeneroController {

	@Autowired
	GeneroRepository generoRepository;

	// La primera vez que estamos haciendo un MVC como dios manda
	@GetMapping("/{id}")
	public String getGenero(Model model, @PathVariable int id) {
		// Voy al modelo para obtener los datos del genero
		Genero genero = generoRepository.findById(id).orElse(null);
		// Una vez tengo esos datos los envío a la vista vía model
		model.addAttribute("genero", genero);

		// Lo que devolvemos aquí es el nombre de la vista
		// Spring boot automáticamente buscará una página index.html
		// En la carpeta resources/templates
		return "genero";
	}

	// La primera vez que estamos haciendo un MVC como dios manda
	@GetMapping("")
	public String getGeneros(Model model) {
		// Voy al modelo para obtener los datos del genero
		List<Genero> generos = generoRepository.findAll();
		// Una vez tengo esos datos los envío a la vista vía model
		model.addAttribute("generos", generos);

		// Lo que devolvemos aquí es el nombre de la vista
		// Spring boot automáticamente buscará una página index.html
		// En la carpeta resources/templates
		return "generos";
	}

	// El primer mapeo es con get para simplemente mostrar la vista
	@GetMapping("/add")
	// Pasamos como parámetro el autor para que la vista lo pueda tener disponible
	public String addGenero(Genero genero) {
		// Simplemente mostramos la vista
		return "addgenero";
	}

	// Cuando desde la vista nos añaden el autor entramos por 'POST'
		@PostMapping("/add")
		// Con @Validated recuperamos los datos y los metemos dentro de una entidad,
		// spring lo hace solo
		// En result se guardan los datos de la validación, es decir ¿Lo que nos mandan
		// son datos válidos? Si es que sí, no dará error, en caso contrario
		// en result tenemos la lista de errores
		public String addGeneroDatos(@Validated Genero genero, BindingResult result) {
			System.out.println(genero);
			System.out.println(result);
			try {
				// Si hay algún error volvemos a mostrar la vista y además
				// fields.error tendrá la información de los errores
				if (result.hasErrors()) {
					return "addautor";
				}
				// Si no hay ningún error guardamos el autor
				generoRepository.save(genero);
				// Y en vez de devolver una vista, redirigimos al índice
				return "redirect:/genero";
			} catch (Exception ex) {
				System.out.println(ex.getMessage());
				return "error";
			}
		}
		
		@GetMapping("/edit/{id}")
		// Usamos el model porque tenemos que recuperar al autor
		public String editGenero(@PathVariable Integer id, Model model) {
			try {
				// Primero, buscamos el autor que se quiere editar
				Genero genero= generoRepository.findById(id).orElse(null);
				if (genero != null) {
					// Añadimos el autor al modelo
					model.addAttribute("genero", genero);
					return "updategenero";
				} else {
					return "error";
				}
			} catch (Exception ex) {
				System.out.println(ex.getMessage());
				return "error";
			}

		}
		@PostMapping("/update/{id}")
		public String updateGenero(@PathVariable Integer id, @Validated Genero genero, BindingResult result) {
			System.out.println(genero);
			try {
				if (result.hasErrors()) {
					return "updategenero";
				}
				generoRepository.save(genero);
				return "redirect:/genero";
			} catch (Exception ex) {
				System.out.println(ex.getMessage());
				return "error";
			}
		}
		@GetMapping("/delete/{id}")
		// Usamos el model porque tenemos que recuperar al autor
		public String deleteGenero(@PathVariable Integer id, Model model) {
			try {
				// Primero, buscamos el autor que se quiere editar
				Genero genero= generoRepository.findById(id).orElse(null);
				if (genero != null) {
					// Añadimos el autor al modelo
					model.addAttribute("genero", genero);
					return "deletegenero";
				} else {
					return "error";
				}
			} catch (Exception ex) {
				System.out.println(ex.getMessage());
				return "error";
			}

		}
		@PostMapping("/delete/{id}")
		public String destroyGenero(@PathVariable Integer id, @Validated Genero genero, BindingResult result) {
			System.out.println(genero);
			try {
				
				generoRepository.delete(genero);
				return "redirect:/genero";
			} catch (Exception ex) {
				System.out.println(ex.getMessage());
				return "error";
			}
		}
}

package com.trifulcas.SpringBootVistas.controller;

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 com.trifulcas.SpringBootVistas.repository.GeneroRepository;

@Controller
public class MainController {

	@Autowired
	GeneroRepository generoRepository;
	
	@GetMapping("/")
	public String index(Model model) {
		// Para pasar datos a la vista usamos model
		
		model.addAttribute("nombre", "Pepito pérez");
		
		// Lo que devolvemos aquí es el nombre de la vista
		// Spring boot automáticamente buscará una página index.html
		// En la carpeta resources/templates
		return "index";
	}

}

Controladores API

package com.trifulcas.SpringBootVistas.controller;

import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.trifulcas.SpringBootVistas.model.Autor;
import com.trifulcas.SpringBootVistas.model.Libro;
import com.trifulcas.SpringBootVistas.repository.AutorRepository;


@RestController
@RequestMapping("/api/autor")
public class AutorRestController {

	@Autowired
	AutorRepository autorRepository;

	@GetMapping("")
	public List<Autor> getAll() {
		try {
			return autorRepository.findAll();
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Poner los valores en la URL, no parámetros nombrados
	@GetMapping("/{id}")
	public ResponseEntity<Autor> getById(@PathVariable int id) {
		System.out.println(id);

		try {
			Autor cat = autorRepository.findById(id).orElse(null);
			if (cat != null) {
				return new ResponseEntity<>(cat, HttpStatus.OK);
			} else {
				return new ResponseEntity<>(HttpStatus.NOT_FOUND);
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED);
		}
	}

	// Poner los valores en la URL, no parámetros nombrados
	@GetMapping("/{id}/libros")
	public Set<Libro> getLibrosByIdAutor(@PathVariable int id) {
		System.out.println(id);

		try {
			Autor cat = autorRepository.findById(id).orElse(null);
			if (cat != null) {
				return cat.getLibros();
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Poner los valores en la URL, no parámetros nombrados
	@GetMapping("/titulo/{cadena}")
	public List<Autor> getAutoresByTitulo(@PathVariable String cadena) {
		System.out.println(cadena);

		try {
			return autorRepository.findByLibrosTituloContaining(cadena);
			
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PostMapping("")
	public Autor add(@RequestBody Autor cat) {
		System.out.println(cat);
		try {
			if (cat.getIdautor() == 0 && cat.getNombre() != null) {
				return autorRepository.save(cat);
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PutMapping("/{id}")
	public Autor put(@RequestBody Autor cat, @PathVariable int id) {
		System.out.println(cat);
		System.out.println(id);
		try {
			if (cat.getIdautor() == id) {
				return autorRepository.save(cat);
			} else {
				return null;
			}

		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@DeleteMapping("/{id}")
	public int delete(@PathVariable int id) {
		try {
			System.out.println(id);
			autorRepository.deleteById(id);
			return id;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return 0;
		}
	}
}

package com.trifulcas.SpringBootVistas.controller;

import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.trifulcas.SpringBootVistas.model.Genero;
import com.trifulcas.SpringBootVistas.model.Libro;
import com.trifulcas.SpringBootVistas.repository.GeneroRepository;


@RestController
@RequestMapping("/api/genero")
public class GeneroRestController {

	@Autowired
	GeneroRepository generoRepository;

	@GetMapping("")
	public List<Genero> getAll(@RequestParam(required = false) String nombre) {
		try {
			if (nombre == null) {
				return generoRepository.findAll();
			} else {
				return generoRepository.findByNombreContaining(nombre);
			}

		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Poner los valores en la URL, no parámetros nombrados
	@GetMapping("/{id}")
	public ResponseEntity<Genero> getById(@PathVariable int id) {
		System.out.println(id);

		try {
			Genero cat = generoRepository.findById(id).orElse(null);
			if (cat != null) {
				return new ResponseEntity<>(cat, HttpStatus.OK);
			} else {
				return new ResponseEntity<>(HttpStatus.NOT_FOUND);
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Si yo quiero obtener los libros de un género, algo que he perdido
	// con el JSON ignore pues me lo monto yo
	@GetMapping("/{id}/libros")
	public Set<Libro> getLibrosByIdGenero(@PathVariable int id) {
		System.out.println(id);

		try {
			Genero cat = generoRepository.findById(id).orElse(null);
			if (cat != null) {
				return cat.getLibros();
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PostMapping("/nuevo/{nombre}")
	public Genero addNuevo(@PathVariable String nombre) {
		try {
			System.out.println(nombre);
			Genero nuevo = new Genero(nombre);
			generoRepository.save(nuevo);
			return nuevo;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PostMapping("")
	public Genero add(@RequestBody Genero cat) {
		System.out.println(cat);
		try {
			if (cat.getIdgenero() == 0 && cat.getNombre() != null) {
				return generoRepository.save(cat);
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PutMapping("/{id}")
	public Genero put(@RequestBody Genero cat, @PathVariable int id) {
		System.out.println(cat);
		System.out.println(id);
		try {
			if (cat.getIdgenero() == id) {
				return generoRepository.save(cat);
			} else {
				return null;
			}

		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@DeleteMapping("/{id}")
	public int delete(@PathVariable int id) {
		try {
			System.out.println(id);
			generoRepository.deleteById(id);
			return id;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return 0;
		}
	}
}

package com.trifulcas.SpringBootVistas.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.trifulcas.SpringBootVistas.model.Autor;
import com.trifulcas.SpringBootVistas.model.Libro;
import com.trifulcas.SpringBootVistas.repository.AutorRepository;
import com.trifulcas.SpringBootVistas.repository.LibroRepository;


@RestController
@RequestMapping("/api/libro")
public class LibroRestController {

	@Autowired
	LibroRepository libroRepository;
	@Autowired
	AutorRepository autorRepository;
	private int pageSize = 5;

	@GetMapping("")
	public List<Libro> getAll(@RequestParam(required = false) Integer pagina) {
		try {
			if (pagina == null) {
				return libroRepository.findAll();
			} else {
				return libroRepository.findAll(PageRequest.of(pagina, pageSize)).getContent();
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@GetMapping("/proceso")
	public String procesarlibros() {
		try {
			System.out.println("Esto es un ejemplo sencillo de un proceso");
			// Imaginemos que yo quiero obtener una cadena con la primera letra
			// de cada título
			// Y para no bloquear la base de datos pidiendo todos los libros los hago por
			// páginas
			int pagina = 0;
			String res = "";
			List<Libro> libros = libroRepository.findAll(PageRequest.of(pagina, pageSize)).getContent();
			while (libros.size() > 0) {
				for (Libro libro : libros) {
					res += libro.getTitulo().substring(0, 1);
				}
				res+="#";
				pagina++;
				libros = libroRepository.findAll(PageRequest.of(pagina, pageSize)).getContent();
			}

			return res;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@GetMapping("/autor/{id}")
	public List<Libro> getByIdAutor(@PathVariable int id) {
		System.out.println(id);

		try {
			return libroRepository.findByAutoresIdautor(id);
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@GetMapping("/titulo/{cadena}")
	public List<Libro> getByTitle(@PathVariable String cadena) {
		System.out.println(cadena);

		try {
			return libroRepository.findByTituloContaining(cadena);
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Poner los valores en la URL, no parámetros nombrados
	@GetMapping("/{id}")
	public ResponseEntity<Libro> getById(@PathVariable int id) {
		System.out.println(id);

		try {
			Libro cat = libroRepository.findById(id).orElse(null);
			if (cat != null) {
				return new ResponseEntity<>(cat, HttpStatus.OK);
			} else {
				return new ResponseEntity<>(HttpStatus.NOT_FOUND);
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PostMapping("")
	public Libro add(@RequestBody Libro cat) {
		System.out.println(cat);
		try {
			if (cat.getIdlibro() == 0 && cat.getTitulo() != null) {
				return libroRepository.save(cat);
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PutMapping("/{id}")
	public Libro put(@RequestBody Libro cat, @PathVariable int id) {
		System.out.println(cat);
		System.out.println(id);
		try {
			if (cat.getIdlibro() == id) {
				return libroRepository.save(cat);
			} else {
				return null;
			}

		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@DeleteMapping("/{id}")
	public int delete(@PathVariable int id) {
		try {
			System.out.println(id);
			libroRepository.deleteById(id);
			return id;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return 0;
		}
	}

	// Gestión de libros y autores
	@PostMapping("{idlibro}/autor/{idautor}")
	public Libro addLibroAutor(@PathVariable int idlibro, @PathVariable int idautor) {
		System.out.println(idlibro);
		System.out.println(idautor);
		try {
			Libro libro = libroRepository.findById(idlibro).orElse(null);
			Autor autor = autorRepository.findById(idautor).orElse(null);
			if (libro != null && autor != null) {
				libro.getAutores().add(autor);
				return libroRepository.save(libro);
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Gestión de libros y autores
	@DeleteMapping("{idlibro}/autor/{idautor}")
	public Libro deleteLibroAutor(@PathVariable int idlibro, @PathVariable int idautor) {
		System.out.println(idlibro);
		System.out.println(idautor);
		try {
			Libro libro = libroRepository.findById(idlibro).orElse(null);
			Autor autor = autorRepository.findById(idautor).orElse(null);
			if (libro != null && autor != null) {
				libro.getAutores().remove(autor);
				return libroRepository.save(libro);
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}
}

Vistas:
addautor

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Añadir Autor</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
<h2>Añadir Autor</h2>
<!-- Lo primero es hacer un formulario con los campos de autor, en este caso el nombre
	-->
	<form th:action="@{/autor/add}" th:object="${autor}" method="POST">
		<label for="name">Nombre</label>
		<!-- el campo hace referencia a la propiedad nombre -->
		<input type="text" th:field="*{nombre}" id="nombre" placeholder="Nombre">
		<!-- Si hay algún error lo mostramos (viene de result) -->
		<span th:if="${#fields.hasErrors('nombre')}" th:errors="*{nombre}"></span>
		<input class="btn btn-success" type="submit" value="Añadir">
	  	<a class="btn btn-primary" th:href="@{/autor}">Ir al índice</a>

	</form>
</body>

</html>

addgenero

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Añadir Género</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
<h2>Añadir Género</h2>
<!-- Lo primero es hacer un formulario con los campos de genero, en este caso el nombre
	-->
	<form th:action="@{/genero/add}" th:object="${genero}" method="POST">
		<label for="name">Nombre</label>
		<!-- el campo hace referencia a la propiedad nombre -->
		<input type="text" th:field="*{nombre}" id="nombre" placeholder="Nombre">
		<!-- Si hay algún error lo mostramos (viene de result) -->
		<span th:if="${#fields.hasErrors('nombre')}" th:errors="*{nombre}"></span>
		<input class="btn btn-success" type="submit" value="Añadir">
		<a class="btn btn-primary" th:href="@{/genero}">Ir al índice</a>

	</form>
</body>

</html>

autor

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Autor</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
<h2>Autor</h2>
<!-- El th:each es como un bucle foreach, en este caso le estamos diciendo 
que recorra la variable autores y lo guarde en una variable llamada autor
	-->
	<div class="card w-50">
		<!-- Si yo estoy guardando cada elemento en una variable llamada 'autor'
		puedo acceder a sus propiedades y mostrarlas -->
		<div class="card-header"><span th:text="${autor.idautor}"></span></div>
		<!--
			Cuando accedemos a la propiedad nombre spring boot busca el getter
			Es decir, intentará acceder a getNombre() -->
		<div class="card-body"><span th:text="${autor.nombre}"></span></div>
	</div>
	<table class="table table-striped">
    <thead>
      <tr>
        <th>Id</th>
        <th>Titulo</th>
        <th>Páginas</th>
      </tr>
    </thead>
    <tbody>
		<!-- Puedo acceder a la propiedad libros porque mi entidad autor
		tiene un getLibros() que me devuelve los libros y los recorro con el each -->
      <tr th:each="libro: ${autor.libros}">
        <td><span th:text="${libro.idlibro}"></span></td>
         <td><span th:text="${libro.titulo}"></span></td>
       <td><span th:text="${libro.paginas}"></span></td>
    
      </tr>
     
    </tbody>
  </table>
  	<a class="btn btn-primary" th:href="@{/autor}">Ir al índice</a>

</body>

</html>

autores

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Lista de autores</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
	<h2>Autores</h2>
	<!-- El th:each es como un bucle foreach, en este caso le estamos diciendo 
que recorra la variable autores y lo guarde en una variable llamada autor
	-->
	<a class="btn btn-success" th:href="@{/autor/add}">Añadir autor</a>
	<a class="btn btn-primary" th:href="@{/}">Ir al índice</a>

	<div class="card w-50" th:each="autor: ${autores}">
		<!-- Si yo estoy guardando cada elemento en una variable llamada 'autor'
		puedo acceder a sus propiedades y mostrarlas
		Pongo un enlace al detalle del autor -->
		<div class="card-header"><a class="btn btn-primary" th:href="@{/autor/{id}(id=${autor.idautor})}"><span
					th:text="${autor.idautor}"></span></a></div>
		<!--
			Cuando accedemos a la propiedad nombre spring boot busca el getter
			Es decir, intentará acceder a getNombre() -->
		<div class="card-body"><span th:text="${autor.nombre}"></span>
			<a class="btn btn-secondary" th:href="@{/autor/edit/{id}(id=${autor.idautor})}">Editar</a>
			<a class="btn btn-danger" th:href="@{/autor/delete/{id}(id=${autor.idautor})}">Borrar</a>
		</div>
	</div>
</body>

</html>

deletegenero

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Editar Género</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
	<h2>Borrar Género</h2>
	<!-- Lo primero es hacer un formulario con los campos de autor, en este caso el nombre
	-->
	<form th:action="@{/genero/delete/{id}(id=${genero.idgenero})}" th:object="${genero}" method="POST">
		<input type="hidden" th:field="*{idgenero}" id="idgenero">

		<label for="name">¿Está seguro de que quiere eliminar este género?</label>
		<!-- el campo hace referencia a la propiedad nombre -->
		<input type="text" th:field="*{nombre}" id="nombre" placeholder="Nombre" readonly>
		<input class="btn btn-success" type="submit" value="Eliminar">
	<a class="btn btn-primary" th:href="@{/genero}">Ir al índice</a>

	</form>
</body>

</html>

error

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Error</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
	<div class="alert alert-danger">
		<strong>Error</strong> La petición ha generado un error.
	</div>
	<!-- Creo un enlace con th:href en principio me calcula la ruta adecuada -->
	<a class="btn btn-primary" th:href="@{/autor}">Ir al índice</a>
</body>

</html>

genero

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Genero</title>
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
	<h2>Género</h2>
	<!-- A mí me han pasado los datos en la entidad genero
	Y muestro las propiedades de esa entidad -->
	<p th:text="${genero.idgenero}"></p>
	<!-- Lo que hace Spring aquí es llamar al método genero.getNombre()-->
	<p th:text="${genero.nombre}"></p>
	<table class="table table-striped">
		<thead>
			<tr>
				<th>Id</th>
				<th>Titulo</th>
				<th>Páginas</th>
			</tr>
		</thead>
		<tbody>
			<!-- Puedo acceder a la propiedad libros porque mi entidad autor
		tiene un getLibros() que me devuelve los libros y los recorro con el each -->
			<tr th:each="libro: ${genero.libros}">
				<td><span th:text="${libro.idlibro}"></span></td>
				<td><span th:text="${libro.titulo}"></span></td>
				<td><span th:text="${libro.paginas}"></span></td>

			</tr>

		</tbody>
	</table>
	<a class="btn btn-primary" th:href="@{/genero}">Ir al índice</a>
</body>

</html>

generos

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Generos</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>

<body>
	<h2>Géneros</h2>
	<a class="btn btn-success" th:href="@{/genero/add}">Añadir género</a>
	<a class="btn btn-primary" th:href="@{/}">Ir al índice</a>

	<div class="card w-50" th:each="genero: ${generos}">
		<div class="card-header"><a class="btn btn-primary" th:href="@{/genero/{id}(id=${genero.idgenero})}"><span
					th:text="${genero.idgenero}"></span></a></div>
		<div class="card-body"><span th:text="${genero.nombre}"></span>
			<a class="btn btn-secondary" th:href="@{/genero/edit/{id}(id=${genero.idgenero})}">Editar</a>
			<a class="btn btn-danger" th:href="@{/genero/delete/{id}(id=${genero.idgenero})}">Eliminar</a>
		</div>
</div>
</body>

</html>

index

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Gestión biblioteca</title>
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>

	<a class="btn btn-success" th:href="@{/autor}">Autores</a>
	<a class="btn btn-success" th:href="@{/genero}">Géneros</a>

</body>

</html>

updateautor

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Editar Autor</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
	<h2>Editar Autor</h2>
	<!-- Lo primero es hacer un formulario con los campos de autor, en este caso el nombre
	-->
	<form th:action="@{/autor/update/{id}(id=${autor.idautor})}" th:object="${autor}" method="POST">
		<input type="hidden" th:field="*{idautor}" id="idautor">

		<label for="name">Nombre</label>
		<!-- el campo hace referencia a la propiedad nombre -->
		<input type="text" th:field="*{nombre}" id="nombre" placeholder="Nombre">
		<!-- Si hay algún error lo mostramos (viene de result) -->
		<span th:if="${#fields.hasErrors('nombre')}" th:errors="*{nombre}"></span>
		<input class="btn btn-success" type="submit" value="Modificar">
		<a class="btn btn-primary" th:href="@{/autor}">Ir al índice</a>

	</form>
</body>

</html>

updategenero

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Editar Género</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
	<h2>Editar Género</h2>
	<!-- Lo primero es hacer un formulario con los campos de autor, en este caso el nombre
	-->
	<form th:action="@{/genero/update/{id}(id=${genero.idgenero})}" th:object="${genero}" method="POST">
		<input type="hidden" th:field="*{idgenero}" id="idgenero">

		<label for="name">Nombre</label>
		<!-- el campo hace referencia a la propiedad nombre -->
		<input type="text" th:field="*{nombre}" id="nombre" placeholder="Nombre">
		<!-- Si hay algún error lo mostramos (viene de result) -->
		<span th:if="${#fields.hasErrors('nombre')}" th:errors="*{nombre}"></span>
		<input class="btn btn-success" type="submit" value="Modificar">
	<a class="btn btn-primary" th:href="@{/genero}">Ir al índice</a>

	</form>
</body>

</html>

Editar autor

Controlador

package com.trifulcas.SpringBootVistas.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
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.RequestMapping;

import com.trifulcas.SpringBootVistas.model.Autor;
import com.trifulcas.SpringBootVistas.repository.AutorRepository;

// La anotación nos indica que es un controlador normal, que requerirá una vista
@Controller
// Nos indica la ruta de entrada general a este controlador
@RequestMapping("/autor")
public class AutorController {

	// Necesitamos acceder a los datos por lo tanto creamos el repositorio
	// el autowired nos hace inyección de dependencia automática
	@Autowired
	AutorRepository autorRepository;

	// Aquí especificamos que accedemos vía get
	@GetMapping("")
	// Pongo el parámetro Model que nos permita pasar datos a la vista
	public String getAutores(Model model) {
		try {
			// Obtengo los datos como en la API rest
			List<Autor> autores = autorRepository.findAll();
			// Paso la información a la vista vía model
			// La vista tendrá una variable 'autores' con la lista de autores
			model.addAttribute("autores", autores);
			// Le digo que me cargue la vista 'autores' la buscará en templates
			return "autores";
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}

	// Mapeo que me pasen un parámetro id
	@GetMapping("/{id}")
	// Tengo el parámetro id que me pasan y el model para pasar datos a la vista
	public String getAutor(Model model, @PathVariable Integer id) {
		try {
			// Recupero el autor
			Autor autor = autorRepository.findById(id).orElse(null);
			if (autor != null) {
				// Lo paso a la vista
				model.addAttribute("autor", autor);
				// Devuelvo la vista
				return "autor";
			} else {
				// Me he creado una vista para mostrar un error
				return "error";
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}

	// El primer mapeo es con get para simplemente mostrar la vista
	@GetMapping("/add")
	// Pasamos como parámetro el autor para que la vista lo pueda tener disponible
	public String addAutor(Autor autor) {
		// Simplemente mostramos la vista
		return "addautor";
	}

	// Cuando desde la vista nos añaden el autor entramos por 'POST'
	@PostMapping("/add")
	// Con @Validated recuperamos los datos y los metemos dentro de una entidad,
	// spring lo hace solo
	// En result se guardan los datos de la validación, es decir ¿Lo que nos mandan
	// son datos válidos? Si es que sí, no dará error, en caso contrario
	// en result tenemos la lista de errores
	public String addAutorDatos(@Validated Autor autor, BindingResult result) {
		System.out.println(autor);
		System.out.println(result);
		try {
			// Si hay algún error volvemos a mostrar la vista y además
			// fields.error tendrá la información de los errores
			if (result.hasErrors()) {
				return "addautor";
			}
			// Si no hay ningún error guardamos el autor
			autorRepository.save(autor);
			// Y en vez de devolver una vista, redirigimos al índice
			return "redirect:/autor";
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}

	@GetMapping("/edit/{id}")
	// Usamos el model porque tenemos que recuperar al autor
	public String addAutor(@PathVariable Integer id, Model model) {
		try {
			// Primero, buscamos el autor que se quiere editar
			Autor autor = autorRepository.findById(id).orElse(null);
			if (autor != null) {
				// Añadimos el autor al modelo
				model.addAttribute("autor", autor);
				return "updateautor";
			} else {
				return "error";
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}

	}

	@PostMapping("/update/{id}")
	public String updateAutor(@PathVariable Integer id, @Validated Autor autor, BindingResult result) {
		System.out.println(autor);
		try {
			if (result.hasErrors()) {
				return "updateautor";
			}
			autorRepository.save(autor);
			return "redirect:/autor";
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}
}

Vistas

autores

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Lista de autores</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
	<h2>Autores</h2>
	<!-- El th:each es como un bucle foreach, en este caso le estamos diciendo 
que recorra la variable autores y lo guarde en una variable llamada autor
	-->
	<a class="btn btn-success" th:href="@{/autor/add}">Añadir autor</a>
	<div class="card w-50" th:each="autor: ${autores}">
		<!-- Si yo estoy guardando cada elemento en una variable llamada 'autor'
		puedo acceder a sus propiedades y mostrarlas
		Pongo un enlace al detalle del autor -->
		<div class="card-header"><a class="btn btn-primary" th:href="@{/autor/{id}(id=${autor.idautor})}"><span
					th:text="${autor.idautor}"></span></a></div>
		<!--
			Cuando accedemos a la propiedad nombre spring boot busca el getter
			Es decir, intentará acceder a getNombre() -->
		<div class="card-body"><span th:text="${autor.nombre}"></span> <a class="btn btn-secondary" th:href="@{/autor/edit/{id}(id=${autor.idautor})}">Editar</a></div>
	</div>
</body>

</html>

addautor

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Añadir Autor</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
<h2>Añadir Autor</h2>
<!-- Lo primero es hacer un formulario con los campos de autor, en este caso el nombre
	-->
	<form th:action="@{/autor/add}" th:object="${autor}" method="POST">
		<label for="name">Nombre</label>
		<!-- el campo hace referencia a la propiedad nombre -->
		<input type="text" th:field="*{nombre}" id="nombre" placeholder="Nombre">
		<!-- Si hay algún error lo mostramos (viene de result) -->
		<span th:if="${#fields.hasErrors('nombre')}" th:errors="*{nombre}"></span>
		<input class="btn btn-success" type="submit" value="Añadir">
	
	</form>
</body>

</html>

updateautor

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Editar Autor</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
	<h2>Editar Autor</h2>
	<!-- Lo primero es hacer un formulario con los campos de autor, en este caso el nombre
	-->
	<form th:action="@{/autor/update/{id}(id=${autor.idautor})}" th:object="${autor}" method="POST">
		<input type="hidden" th:field="*{idautor}" id="idautor">

		<label for="name">Nombre</label>
		<!-- el campo hace referencia a la propiedad nombre -->
		<input type="text" th:field="*{nombre}" id="nombre" placeholder="Nombre">
		<!-- Si hay algún error lo mostramos (viene de result) -->
		<span th:if="${#fields.hasErrors('nombre')}" th:errors="*{nombre}"></span>
		<input class="btn btn-success" type="submit" value="Modificar">

	</form>
</body>

</html>

autor

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Autor</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
<h2>Autor</h2>
<!-- El th:each es como un bucle foreach, en este caso le estamos diciendo 
que recorra la variable autores y lo guarde en una variable llamada autor
	-->
	<div class="card w-50">
		<!-- Si yo estoy guardando cada elemento en una variable llamada 'autor'
		puedo acceder a sus propiedades y mostrarlas -->
		<div class="card-header"><span th:text="${autor.idautor}"></span></div>
		<!--
			Cuando accedemos a la propiedad nombre spring boot busca el getter
			Es decir, intentará acceder a getNombre() -->
		<div class="card-body"><span th:text="${autor.nombre}"></span></div>
	</div>
	<table class="table table-striped">
    <thead>
      <tr>
        <th>Id</th>
        <th>Titulo</th>
        <th>Páginas</th>
      </tr>
    </thead>
    <tbody>
		<!-- Puedo acceder a la propiedad libros porque mi entidad autor
		tiene un getLibros() que me devuelve los libros y los recorro con el each -->
      <tr th:each="libro: ${autor.libros}">
        <td><span th:text="${libro.idlibro}"></span></td>
         <td><span th:text="${libro.titulo}"></span></td>
       <td><span th:text="${libro.paginas}"></span></td>
    
      </tr>
     
    </tbody>
  </table>
  	<a class="btn btn-primary" th:href="@{/autor}">Ir al índice</a>

</body>

</html>

Añadir un autor

Añado esta dependencia en el POM:

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>

Añado esta anotación en la entidad autor:

@Column(nullable = false) // Asegura que la columna no permita valores nulos a nivel de base de datos
	@NotEmpty
	private String nombre;

Esto lo hacemos para comprobar el error cuando nos mandan un valor vacío

El controlador queda así:

package com.trifulcas.SpringBootVistas.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
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.RequestMapping;

import com.trifulcas.SpringBootVistas.model.Autor;
import com.trifulcas.SpringBootVistas.repository.AutorRepository;

// La anotación nos indica que es un controlador normal, que requerirá una vista
@Controller
// Nos indica la ruta de entrada general a este controlador
@RequestMapping("/autor")
public class AutorController {

	// Necesitamos acceder a los datos por lo tanto creamos el repositorio
	// el autowired nos hace inyección de dependencia automática
	@Autowired
	AutorRepository autorRepository;

	// Aquí especificamos que accedemos vía get
	@GetMapping("")
	// Pongo el parámetro Model que nos permita pasar datos a la vista
	public String getAutores(Model model) {
		try {
			// Obtengo los datos como en la API rest
			List<Autor> autores = autorRepository.findAll();
			// Paso la información a la vista vía model
			// La vista tendrá una variable 'autores' con la lista de autores
			model.addAttribute("autores", autores);
			// Le digo que me cargue la vista 'autores' la buscará en templates
			return "autores";
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}

	// Mapeo que me pasen un parámetro id
	@GetMapping("/{id}")
	// Tengo el parámetro id que me pasan y el model para pasar datos a la vista
	public String getAutor(Model model, @PathVariable Integer id) {
		try {
			// Recupero el autor
			Autor autor = autorRepository.findById(id).orElse(null);
			if (autor != null) {
				// Lo paso a la vista
				model.addAttribute("autor", autor);
				// Devuelvo la vista
				return "autor";
			} else {
				// Me he creado una vista para mostrar un error
				return "error";
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}
	// El primer mapeo es con get para simplemente mostrar la vista
	@GetMapping("/add")
	// Pasamos como parámetro el autor para que la vista lo pueda tener disponible
	public String addAutor(Autor autor) {
		// Simplemente mostramos la vista
		return "addautor";
	}
	// Cuando desde la vista nos añaden el autor entramos por 'POST'
	@PostMapping("/add")
	// Con @Validated recuperamos los datos y los metemos dentro de una entidad, spring lo hace solo
	// En result se guardan los datos de la validación, es decir ¿Lo que nos mandan
	// son datos válidos? Si es que sí, no dará error, en caso contrario
	// en result tenemos la lista de errores
	public String addAutorDatos(@Validated Autor autor, BindingResult result) {
		System.out.println(autor);
		System.out.println(result);
		// Si hay algún error volvemos a mostrar la vista y además
		// fields.error tendrá la información de los errores
		if (result.hasErrors()) {
			return "addautor";
		}
		// Si no hay ningún error guardamos el autor
		autorRepository.save(autor);
		// Y en vez de devolver una vista, redirigimos al índice
		return "redirect:/autor";
	}
}

Y la vista addautor así:

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Añadir Autor</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
<h2>Añadir Autor</h2>
<!-- Lo primero es hacer un formulario con los campos de autor, en este caso el nombre
	-->
	<form th:action="@{/autor/add}" th:object="${autor}" method="POST">
		<label for="name">Nombre</label>
		<!-- el campo hace referencia a la propiedad nombre -->
		<input type="text" th:field="*{nombre}" id="nombre" placeholder="Nombre">
		<!-- Si hay algún error lo mostramos (viene de result) -->
		<span th:if="${#fields.hasErrors('nombre')}" th:errors="*{nombre}"></span>
		<input class="btn btn-success" type="submit" value="Añadir">
	
	</form>
</body>

</html>

Solución ejercicio autores

Controlador

package com.trifulcas.SpringBootVistas.controller;

import java.util.List;

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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.trifulcas.SpringBootVistas.model.Autor;
import com.trifulcas.SpringBootVistas.repository.AutorRepository;

// La anotación nos indica que es un controlador normal, que requerirá una vista
@Controller
// Nos indica la ruta de entrada general a este controlador
@RequestMapping("/autor")
public class AutorController {

	// Necesitamos acceder a los datos por lo tanto creamos el repositorio
	// el autowired nos hace inyección de dependencia automática
	@Autowired
	AutorRepository autorRepository;

	// Aquí especificamos que accedemos vía get
	@GetMapping("")
	// Pongo el parámetro Model que nos permita pasar datos a la vista
	public String getAutores(Model model) {
		try {
			// Obtengo los datos como en la API rest
			List<Autor> autores = autorRepository.findAll();
			// Paso la información a la vista vía model
			// La vista tendrá una variable 'autores' con la lista de autores
			model.addAttribute("autores", autores);
			// Le digo que me cargue la vista 'autores' la buscará en templates
			return "autores";
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}

	// Mapeo que me pasen un parámetro id
	@GetMapping("/{id}")
	// Tengo el parámetro id que me pasan y el model para pasar datos a la vista
	public String getAutor(Model model, @PathVariable Integer id) {
		try {
			// Recupero el autor
			Autor autor = autorRepository.findById(id).orElse(null);
			if (autor != null) {
				// Lo paso a la vista
				model.addAttribute("autor", autor);
				// Devuelvo la vista
				return "autor";
			} else {
				// Me he creado una vista para mostrar un error
				return "error";
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return "error";
		}
	}
}

Plantilla autores

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Lista de autores</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
<h2>Autores</h2>
<!-- El th:each es como un bucle foreach, en este caso le estamos diciendo 
que recorra la variable autores y lo guarde en una variable llamada autor
	-->
	<div class="card w-50" th:each="autor: ${autores}">
		<!-- Si yo estoy guardando cada elemento en una variable llamada 'autor'
		puedo acceder a sus propiedades y mostrarlas
		Pongo un enlace al detalle del autor -->
		<div class="card-header"><a class="btn btn-primary"
									th:href="@{/autor/{id}(id=${autor.idautor})}"><span th:text="${autor.idautor}"></span></a></div>
		<!--
			Cuando accedemos a la propiedad nombre spring boot busca el getter
			Es decir, intentará acceder a getNombre() -->
		<div class="card-body"><span th:text="${autor.nombre}"></span></div>
	</div>
</body>

</html>

Plantilla autor

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Autor</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
<h2>Autor</h2>
<!-- El th:each es como un bucle foreach, en este caso le estamos diciendo 
que recorra la variable autores y lo guarde en una variable llamada autor
	-->
	<div class="card w-50">
		<!-- Si yo estoy guardando cada elemento en una variable llamada 'autor'
		puedo acceder a sus propiedades y mostrarlas -->
		<div class="card-header"><span th:text="${autor.idautor}"></span></div>
		<!--
			Cuando accedemos a la propiedad nombre spring boot busca el getter
			Es decir, intentará acceder a getNombre() -->
		<div class="card-body"><span th:text="${autor.nombre}"></span></div>
	</div>
	<table class="table table-striped">
    <thead>
      <tr>
        <th>Id</th>
        <th>Titulo</th>
        <th>Páginas</th>
      </tr>
    </thead>
    <tbody>
		<!-- Puedo acceder a la propiedad libros porque mi entidad autor
		tiene un getLibros() que me devuelve los libros y los recorro con el each -->
      <tr th:each="libro: ${autor.libros}">
        <td><span th:text="${libro.idlibro}"></span></td>
         <td><span th:text="${libro.titulo}"></span></td>
       <td><span th:text="${libro.paginas}"></span></td>
    
      </tr>
     
    </tbody>
  </table>
  	<a class="btn btn-primary" th:href="@{/autor}">Ir al índice</a>

</body>

</html>

Plantilla error

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Error</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

</head>

<body>
	<div class="alert alert-danger">
		<strong>Error</strong> La petición ha generado un error.
	</div>
	<!-- Creo un enlace con th:href en principio me calcula la ruta adecuada -->
	<a class="btn btn-primary" th:href="@{/autor}">Ir al índice</a>
</body>

</html>

Ejemplos thymeleaf

package com.trifulcas.SpringBootVistas.controller;

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.PathVariable;

import com.trifulcas.SpringBootVistas.model.Genero;
import com.trifulcas.SpringBootVistas.repository.GeneroRepository;

@Controller
public class MainController {

	@Autowired
	GeneroRepository generoRepository;
	
	@GetMapping("/index")
	public String index(Model model) {
		// Para pasar datos a la vista usamos model
		
		model.addAttribute("nombre", "Pepito pérez");
		
		// Lo que devolvemos aquí es el nombre de la vista
		// Spring boot automáticamente buscará una página index.html
		// En la carpeta resources/templates
		return "index";
	}
	// La primera vez que estamos haciendo un MVC como dios manda
	@GetMapping("/genero/{id}")
	public String getGenero(Model model, @PathVariable int id) {
		// Voy al modelo para obtener los datos del genero
		Genero genero=generoRepository.findById(id).orElse(null);
		// Una vez tengo esos datos los envío a la vista vía model
		model.addAttribute("genero", genero);
		
		// Lo que devolvemos aquí es el nombre de la vista
		// Spring boot automáticamente buscará una página index.html
		// En la carpeta resources/templates
		return "genero";
	}
// La primera vez que estamos haciendo un MVC como dios manda
		@GetMapping("/generos")
		public String getGeneros(Model model) {
			// Voy al modelo para obtener los datos del genero
			List<Genero> generos=generoRepository.findAll();
			// Una vez tengo esos datos los envío a la vista vía model
			model.addAttribute("generos", generos);
			
			// Lo que devolvemos aquí es el nombre de la vista
			// Spring boot automáticamente buscará una página index.html
			// En la carpeta resources/templates
			return "generos";
		}
}


index.html

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Mi primera vista</title>
</head>

<body>
	<!-- Las plantillas Thymeleaf tienen una serie de comandos para
	utilizar y mostrar la información que nos pasan-->
	<h1>Hola <span th:text="${nombre}"></span> ¿Qué tal?</h1>
</body>

</html>

genero.html

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Genero</title>
</head>

<body>
	<h2>Género</h2>
<!-- A mí me han pasado los datos en la entidad genero
	Y muestro las propiedades de esa entidad -->
	<p th:text="${genero.idgenero}"></p>
	<p th:text="${genero.nombre}"></p>
</body>

</html>

generos.html

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Generos</title>
	<!-- Latest compiled and minified CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- Latest compiled JavaScript -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>

<body>
	<h2>Géneros</h2>
	<div class="card w-50" th:each="genero: ${generos}">
		<div class="card-header"><span th:text="${genero.idgenero}"></span></div>
		<div class="card-body"><span th:text="${genero.nombre}"></span></div>
	</div>

</body>

</html>

Si yo voy a esta url: http://localhost:8080/genero/3
Podré ver los datos del género que nos pidan, en este caso 3

Un ejemplo (tonto) de proceso por paginación

@GetMapping("/proceso")
	public String procesarlibros() {
		try {
			System.out.println("Esto es un ejemplo sencillo de un proceso");
			// Imaginemos que yo quiero obtener una cadena con la primera letra
			// de cada título
			// Y para no bloquear la base de datos pidiendo todos los libros los hago por
			// páginas
			int pagina = 0;
			String res = "";
			List<Libro> libros = libroRepository.findAll(PageRequest.of(pagina, pageSize)).getContent();
			while (libros.size() > 0) {
				for (Libro libro : libros) {
					res += libro.getTitulo().substring(0, 1);
				}
				pagina++;
				libros = libroRepository.findAll(PageRequest.of(pagina, pageSize)).getContent();
			}

			return res;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

Ejercicio biblioteca completo

package com.trifulcas.SpringBootBiblioteca.model;

import java.util.Set;

import com.fasterxml.jackson.annotation.JsonIgnore;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;

@Entity
@Table(name = "autor")
public class Autor {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int idautor;

    private String nombre;

    // Como todas las indicaciones de tablas intermedias y foreign keys
    // Las hemos puesto en libro aquí podemos usar solo autores
    @ManyToMany(mappedBy = "autores")
    @JsonIgnore // Para evitar bucles infinitos
    private Set<Libro> libros;

    // Constructor vacío
    public Autor() {
    }

    // Constructor con parámetros
    public Autor(String nombre) {
        this.nombre = nombre;
    }

    // Getters y Setters
    public int getIdautor() {
        return idautor;
    }

    public void setIdautor(int idautor) {
        this.idautor = idautor;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public Set<Libro> getLibros() {
        return libros;
    }

    public void setLibros(Set<Libro> libros) {
        this.libros = libros;
    }

    // Método toString (opcional)
    @Override
    public String toString() {
        return "Autor{" +
                "idautor=" + idautor +
                ", nombre='" + nombre + '\'' +
                '}';
    }
}
package com.trifulcas.SpringBootBiblioteca.model;

import java.util.Set;

import com.fasterxml.jackson.annotation.JsonIgnore;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;

@Entity
@Table(name = "genero")
public class Genero {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int idgenero;

    private String nombre;

    // Carga perezosa, pero al serializar siempre carga
    // Mapeamos por 'genero', que es el campo en la entidad relacionada
    @OneToMany(mappedBy = "genero")
    @JsonIgnore  // Quitar este campo NO de la entidad SI de la srialización
    private Set<Libro> libros;

    // Constructor vacío
    public Genero() {
    }

    // Constructor con parámetros
    public Genero(String nombre) {
        this.nombre = nombre;
    }

    // Getters y Setters
    public int getIdgenero() {
        return idgenero;
    }

    public void setIdgenero(int idgenero) {
        this.idgenero = idgenero;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public Set<Libro> getLibros() {
        return libros;
    }

    public void setLibros(Set<Libro> libros) {
        this.libros = libros;
    }

    // Método toString (opcional)
    @Override
    public String toString() {
        return "Genero{" +
                "idgenero=" + idgenero +
                ", nombre='" + nombre + '\'' +
                '}';
    }
}

package com.trifulcas.SpringBootBiblioteca.model;

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 jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;

@Entity
@Table(name = "libro")
public class Libro {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int idlibro;

    // Carga ansiosa. El nombre del campo es el mapping en genero
    @ManyToOne
    @JoinColumn(name = "idgenero")
    private Genero genero;

    private String titulo;

    private int paginas;

    // Cuando tenemos many to many tenemos que elegir donde montamos
    // toda la relación con la especificación de la tabla intermedia
    // Y los campos relacionados. Yo lo he hecho aquí pero podría ser
    // en autor, es lo mismo
    @ManyToMany
    @JoinTable(
        name = "libro_autor",
        joinColumns = @JoinColumn(name = "idlibro"),
        inverseJoinColumns = @JoinColumn(name = "idautor")
    )
    // Utilizamos set porque son valores únicos pero podría ser List
    private Set<Autor> autores;

    // Constructor vacío
    public Libro() {
    }

    // Constructor con parámetros
    public Libro(Genero genero, String titulo, int paginas) {
        this.genero = genero;
        this.titulo = titulo;
        this.paginas = paginas;
    }

    // Getters y Setters
    public int getIdlibro() {
        return idlibro;
    }

    public void setIdlibro(int idlibro) {
        this.idlibro = idlibro;
    }

    public Genero getGenero() {
        return genero;
    }

    public void setGenero(Genero genero) {
        this.genero = genero;
    }

    public String getTitulo() {
        return titulo;
    }

    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }

    public int getPaginas() {
        return paginas;
    }

    public void setPaginas(int paginas) {
        this.paginas = paginas;
    }

    public Set<Autor> getAutores() {
        return autores;
    }

    public void setAutores(Set<Autor> autores) {
        this.autores = autores;
    }

    // Método toString (opcional)
    @Override
    public String toString() {
        return "Libro{" +
                "idlibro=" + idlibro +
                ", genero=" + genero +
                ", titulo='" + titulo + '\'' +
                ", paginas=" + paginas +
                '}';
    }
}
package com.trifulcas.SpringBootBiblioteca.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.trifulcas.SpringBootBiblioteca.model.Autor;
import com.trifulcas.SpringBootBiblioteca.model.Genero;

public interface AutorRepository extends JpaRepository<Autor, Integer> {
	List<Genero> findByLibrosGeneroNombreContaining(String cad);

	List<Autor> findByLibrosTituloContaining(String cad);

}

package com.trifulcas.SpringBootBiblioteca.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.trifulcas.SpringBootBiblioteca.model.Genero;

public interface GeneroRepository extends JpaRepository<Genero, Integer> {
// Dentro del repositorio podemos crear consultas de una manera 'mágica'
	List<Genero> findByNombreContaining(String nombre);
}

package com.trifulcas.SpringBootBiblioteca.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.trifulcas.SpringBootBiblioteca.model.Libro;

public interface LibroRepository extends JpaRepository<Libro, Integer> {

	List<Libro> findByPaginasBetweenOrderByPaginasAsc(Integer a, Integer b);
	List<Libro> findByAutoresIdautor(Integer id);
	List<Libro> findByTituloContaining(String cadena);
}

package com.trifulcas.SpringBootBiblioteca.controller;

import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.trifulcas.SpringBootBiblioteca.model.Genero;
import com.trifulcas.SpringBootBiblioteca.model.Libro;
import com.trifulcas.SpringBootBiblioteca.repository.GeneroRepository;

@RestController
@RequestMapping("/genero")
public class GeneroController {

	@Autowired
	GeneroRepository generoRepository;

	@GetMapping("")
	public List<Genero> getAll(@RequestParam(required = false) String nombre) {
		try {
			if (nombre == null) {
				return generoRepository.findAll();
			} else {
				return generoRepository.findByNombreContaining(nombre);
			}

		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Poner los valores en la URL, no parámetros nombrados
	@GetMapping("/{id}")
	public ResponseEntity<Genero> getById(@PathVariable int id) {
		System.out.println(id);

		try {
			Genero cat = generoRepository.findById(id).orElse(null);
			if (cat != null) {
				return new ResponseEntity<>(cat, HttpStatus.OK);
			} else {
				return new ResponseEntity<>(HttpStatus.NOT_FOUND);
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Si yo quiero obtener los libros de un género, algo que he perdido
	// con el JSON ignore pues me lo monto yo
	@GetMapping("/{id}/libros")
	public Set<Libro> getLibrosByIdGenero(@PathVariable int id) {
		System.out.println(id);

		try {
			Genero cat = generoRepository.findById(id).orElse(null);
			if (cat != null) {
				return cat.getLibros();
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PostMapping("/nuevo/{nombre}")
	public Genero addNuevo(@PathVariable String nombre) {
		try {
			System.out.println(nombre);
			Genero nuevo = new Genero(nombre);
			generoRepository.save(nuevo);
			return nuevo;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PostMapping("")
	public Genero add(@RequestBody Genero cat) {
		System.out.println(cat);
		try {
			if (cat.getIdgenero() == 0 && cat.getNombre() != null) {
				return generoRepository.save(cat);
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PutMapping("/{id}")
	public Genero put(@RequestBody Genero cat, @PathVariable int id) {
		System.out.println(cat);
		System.out.println(id);
		try {
			if (cat.getIdgenero() == id) {
				return generoRepository.save(cat);
			} else {
				return null;
			}

		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@DeleteMapping("/{id}")
	public int delete(@PathVariable int id) {
		try {
			System.out.println(id);
			generoRepository.deleteById(id);
			return id;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return 0;
		}
	}
}

package com.trifulcas.SpringBootBiblioteca.controller;

import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.trifulcas.SpringBootBiblioteca.model.Autor;
import com.trifulcas.SpringBootBiblioteca.model.Libro;
import com.trifulcas.SpringBootBiblioteca.repository.AutorRepository;

@RestController
@RequestMapping("/autor")
public class AutorController {

	@Autowired
	AutorRepository autorRepository;

	@GetMapping("")
	public List<Autor> getAll() {
		try {
			return autorRepository.findAll();
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Poner los valores en la URL, no parámetros nombrados
	@GetMapping("/{id}")
	public ResponseEntity<Autor> getById(@PathVariable int id) {
		System.out.println(id);

		try {
			Autor cat = autorRepository.findById(id).orElse(null);
			if (cat != null) {
				return new ResponseEntity<>(cat, HttpStatus.OK);
			} else {
				return new ResponseEntity<>(HttpStatus.NOT_FOUND);
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED);
		}
	}

	// Poner los valores en la URL, no parámetros nombrados
	@GetMapping("/{id}/libros")
	public Set<Libro> getLibrosByIdAutor(@PathVariable int id) {
		System.out.println(id);

		try {
			Autor cat = autorRepository.findById(id).orElse(null);
			if (cat != null) {
				return cat.getLibros();
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Poner los valores en la URL, no parámetros nombrados
	@GetMapping("/titulo/{cadena}")
	public List<Autor> getAutoresByTitulo(@PathVariable String cadena) {
		System.out.println(cadena);

		try {
			return autorRepository.findByLibrosTituloContaining(cadena);
			
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PostMapping("")
	public Autor add(@RequestBody Autor cat) {
		System.out.println(cat);
		try {
			if (cat.getIdautor() == 0 && cat.getNombre() != null) {
				return autorRepository.save(cat);
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PutMapping("/{id}")
	public Autor put(@RequestBody Autor cat, @PathVariable int id) {
		System.out.println(cat);
		System.out.println(id);
		try {
			if (cat.getIdautor() == id) {
				return autorRepository.save(cat);
			} else {
				return null;
			}

		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@DeleteMapping("/{id}")
	public int delete(@PathVariable int id) {
		try {
			System.out.println(id);
			autorRepository.deleteById(id);
			return id;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return 0;
		}
	}
}

package com.trifulcas.SpringBootBiblioteca.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpStatus;
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.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.trifulcas.SpringBootBiblioteca.model.Autor;
import com.trifulcas.SpringBootBiblioteca.model.Libro;
import com.trifulcas.SpringBootBiblioteca.repository.AutorRepository;
import com.trifulcas.SpringBootBiblioteca.repository.LibroRepository;

@RestController
@RequestMapping("/libro")
public class LibroController {

	@Autowired
	LibroRepository libroRepository;
	@Autowired
	AutorRepository autorRepository;

	@GetMapping("")
	public List<Libro> getAll(@RequestParam(required=false) Integer min,@RequestParam(required=false) Integer max) {
		try {
			if (min==null || max==null) {
				return libroRepository.findAll();
			}else {
				return libroRepository.findByPaginasBetweenOrderByPaginasAsc(Math.min(min, max),Math.max(min, max));
						}
		}catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}
	@GetMapping("/autor/{id}")
	public List<Libro> getByIdAutor(@PathVariable int id) {
		System.out.println(id);

		try {
			return libroRepository.findByAutoresIdautor(id);
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}
	
	@GetMapping("/titulo/{cadena}")
	public List<Libro> getByTitle(@PathVariable String cadena) {
		System.out.println(cadena);

		try {
			return libroRepository.findByTituloContaining(cadena);
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}
	
	// Poner los valores en la URL, no parámetros nombrados
	@GetMapping("/{id}")
	public ResponseEntity<Libro> getById(@PathVariable int id) {
		System.out.println(id);

		try {
			Libro cat = libroRepository.findById(id).orElse(null);
			if (cat != null) {
				return new ResponseEntity<>(cat, HttpStatus.OK);
			} else {
				return new ResponseEntity<>(HttpStatus.NOT_FOUND);
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PostMapping("")
	public Libro add(@RequestBody Libro cat) {
		System.out.println(cat);
		try {
			if (cat.getIdlibro() == 0 && cat.getTitulo() != null) {
				return libroRepository.save(cat);
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@PutMapping("/{id}")
	public Libro put(@RequestBody Libro cat, @PathVariable int id) {
		System.out.println(cat);
		System.out.println(id);
		try {
			if (cat.getIdlibro() == id) {
				return libroRepository.save(cat);
			} else {
				return null;
			}

		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	@DeleteMapping("/{id}")
	public int delete(@PathVariable int id) {
		try {
			System.out.println(id);
			libroRepository.deleteById(id);
			return id;
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return 0;
		}
	}

	// Gestión de libros y autores
	@PostMapping("{idlibro}/autor/{idautor}")
	public Libro addLibroAutor(@PathVariable int idlibro, @PathVariable int idautor) {
		System.out.println(idlibro);
		System.out.println(idautor);
		try {
			Libro libro = libroRepository.findById(idlibro).orElse(null);
			Autor autor = autorRepository.findById(idautor).orElse(null);
			if (libro != null && autor != null) {
				libro.getAutores().add(autor);
				return libroRepository.save(libro);
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

	// Gestión de libros y autores
	@DeleteMapping("{idlibro}/autor/{idautor}")
	public Libro deleteLibroAutor(@PathVariable int idlibro, @PathVariable int idautor) {
		System.out.println(idlibro);
		System.out.println(idautor);
		try {
			Libro libro = libroRepository.findById(idlibro).orElse(null);
			Autor autor = autorRepository.findById(idautor).orElse(null);
			if (libro != null && autor != null) {
				libro.getAutores().remove(autor);
				return libroRepository.save(libro);
			} else {
				return null;
			}
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}
}

Ejemplos JPA

package com.trifulcas.SpringBootBiblioteca.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.trifulcas.SpringBootBiblioteca.model.Libro;

public interface LibroRepository extends JpaRepository<Libro, Integer> {

	List<Libro> findByPaginasBetweenOrderByPaginasAsc(Integer a, Integer b);
}

package com.trifulcas.SpringBootBiblioteca.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.trifulcas.SpringBootBiblioteca.model.Genero;

public interface GeneroRepository extends JpaRepository<Genero, Integer> {
// Dentro del repositorio podemos crear consultas de una manera 'mágica'
	List<Genero> findByNombreContaining(String nombre);
}

En el controlador:

	@GetMapping("")
	public List<Genero> getAll(@RequestParam(required=false) String nombre) {
		try {
			if (nombre==null) {
				return generoRepository.findAll();
			}else {
				return generoRepository.findByNombreContaining(nombre);
			}
			
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

Url:

http://localhost:8080/libro?nombre=fi

@GetMapping("")
	public List<Libro> getAll(@RequestParam(required=false) Integer min,@RequestParam(required=false) Integer max) {
		try {
			if (min==null || max==null) {
				return libroRepository.findAll();
			}else {
				return libroRepository.findByPaginasBetweenOrderByPaginasAsc(min, max);
			}
			
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}

Otro ejemplo, buscar los libros de un autor determinado

En el repository:

	List<Libro> findByAutoresIdautor(Integer id);

En el controlador, creo un endpoint nuevo

@GetMapping("/autor/{id}")
	public List<Libro> getByIdAutor(@PathVariable int id) {
		System.out.println(id);

		try {
			return libroRepository.findByAutoresIdautor(id);
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
			return null;
		}
	}