Objetivo
Crear una sección de preguntas frecuentes (FAQ) en la aplicación, accesible en http://localhost:8080/faqs. Es una entidad
nueva, completamente independiente del resto del proyecto: no se relaciona con casas, reservas, reseñas ni usuarios. Por eso
no necesitas coordinarte con nadie y no vas a tener conflictos de Git con tus compañeros.
¿Por qué esta tarea?
El objetivo es que practiques a tu ritmo el ciclo completo de Spring MVC con una pieza aislada y manejable:
- Entidad (
@Entity) → cómo se define una tabla
- Repositorio (
extends JpaRepository) → cómo se accede a la base de datos
- DataInitializer (
CommandLineRunner) → cómo se cargan datos de ejemplo
- Controller (
@Controller, @GetMapping) → cómo se atiende una petición HTTP
- Plantilla Thymeleaf (
th:each, th:text) → cómo se pinta el HTML
Cada paso está en un archivo distinto y los cinco son archivos nuevos. No tienes que modificar absolutamente nada de lo que
ya existe.
Lo que vas a crear
| # |
Archivo |
Carpeta |
| 1 |
Faq.java |
src/main/java/com/demo/model/ |
| 2 |
FaqRepository.java |
src/main/java/com/demo/repository/ |
| 3 |
FaqDataInitializer.java |
src/main/java/com/demo/config/ |
| 4 |
FaqController.java |
src/main/java/com/demo/controller/ |
| 5 |
faq-list.html |
src/main/resources/templates/faq/ (crea también la carpeta faq/) |
Paso 1 — Crear la entidad Faq
Crea src/main/java/com/demo/model/Faq.java:
package com.demo.model;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "faqs")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Faq {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 300)
private String question;
@Column(nullable = false, length = 2000)
private String answer;
@Column(length = 100)
private String category;
}
¿Qué significa cada anotación?
@Entity → le dice a JPA que esta clase representa una tabla.
@Table(name = "faqs") → la tabla se llamará faqs en la base de datos.
@Id + @GeneratedValue(...IDENTITY) → el campo id es la clave primaria autoincremental.
@Column(nullable = false, length = ...) → restricciones de la columna en BD.
@Getter, @Setter, @Builder, @NoArgsConstructor, @AllArgsConstructor → Lombok te genera automáticamente los
getters/setters, el patrón builder y los dos constructores.
Paso 2 — Crear el repositorio
Crea src/main/java/com/demo/repository/FaqRepository.java:
package com.demo.repository;
import com.demo.model.Faq;
import org.springframework.data.jpa.repository.JpaRepository;
public interface FaqRepository extends JpaRepository<Faq, Long> {
}
Solo eso. Es una interfaz vacía. Heredando de JpaRepository<Faq, Long> ya tienes gratis: findAll(), findById(),
save(), deleteById(), count(), etc. Spring genera la implementación automáticamente.
Paso 3 — Crear el DataInitializer
Crea src/main/java/com/demo/config/FaqDataInitializer.java:
package com.demo.config;
import com.demo.model.Faq;
import com.demo.repository.FaqRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Component
@Profile("!test")
public class FaqDataInitializer implements CommandLineRunner {
private final FaqRepository faqRepository;
public FaqDataInitializer(FaqRepository faqRepository) {
this.faqRepository = faqRepository;
}
@Override
public void run(String... args) {
if (faqRepository.count() > 0) return;
faqRepository.save(Faq.builder()
.question("¿Cómo reservo una casa?")
.answer("Entra al detalle de la casa, elige las fechas de entrada y salida y pulsa 'Reservar'. Recibirás una
confirmación cuando el anfitrión acepte la reserva.")
.category("Reservas")
.build());
faqRepository.save(Faq.builder()
.question("¿Puedo cancelar una reserva?")
.answer("Sí, puedes cancelar desde la sección 'Mis reservas' siempre que la reserva esté en estado PENDING o
CONFIRMED.")
.category("Reservas")
.build());
faqRepository.save(Faq.builder()
.question("¿Cómo me convierto en anfitrión?")
.answer("Cualquier usuario registrado puede publicar casas. Solo tienes que ir a 'Nueva casa' desde tu panel y
rellenar los datos del alojamiento.")
.category("Anfitriones")
.build());
faqRepository.save(Faq.builder()
.question("¿Qué hago si tengo un problema con mi estancia?")
.answer("Contacta primero con el anfitrión. Si no se resuelve, puedes escribir al equipo de soporte desde el
panel de usuario.")
.category("Soporte")
.build());
faqRepository.save(Faq.builder()
.question("¿Las mascotas están permitidas?")
.answer("Depende de cada casa. Filtra por el equipamiento 'Admite mascotas' para ver las que sí las aceptan.")
.category("General")
.build());
}
}
¿Qué hace cada anotación?
@Component → Spring registra esta clase como un bean (la instancia y la gestiona él).
@Profile("!test") → este código NO se ejecuta cuando se corren los tests (importante para no contaminar los tests).
implements CommandLineRunner → al arrancar la aplicación, Spring llama al método run() automáticamente.
if (faqRepository.count() > 0) return; → si ya hay datos, no inserta duplicados al reiniciar.
💡 Date cuenta de que este DataInitializer es propio tuyo: no toca el HouseDataInitializer ni el
BookingDataInitializer de tus compañeros. Conviven sin problema.
Paso 4 — Crear el controller
Crea src/main/java/com/demo/controller/FaqController.java:
package com.demo.controller;
import com.demo.model.Faq;
import com.demo.repository.FaqRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Controller
public class FaqController {
private final FaqRepository faqRepository;
public FaqController(FaqRepository faqRepository) {
this.faqRepository = faqRepository;
}
@GetMapping("/faqs")
public String faqList(Model model) {
List<Faq> faqs = faqRepository.findAll();
model.addAttribute("faqs", faqs);
return "faq/faq-list";
}
}
¿Qué hace cada línea?
@Controller → Spring sabe que esta clase atiende peticiones HTTP y devuelve vistas Thymeleaf.
- Constructor con
FaqRepository → Spring inyecta el repositorio automáticamente (inyección por constructor, no hace falta
@Autowired).
@GetMapping("/faqs") → cuando alguien entra a http://localhost:8080/faqs se ejecuta este método.
faqRepository.findAll() → trae todos los registros de la tabla faqs.
model.addAttribute("faqs", faqs) → pasa la lista a la plantilla con el nombre faqs.
return "faq/faq-list" → renderiza el archivo templates/faq/faq-list.html.
Paso 5 — Crear la plantilla HTML
Primero crea la carpeta src/main/resources/templates/faq/, y dentro el archivo faq-list.html:
<!DOCTYPE html>
<html lang="es" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{layout/head :: head('Preguntas frecuentes')}"></head>
<body>
<nav th:replace="~{layout/navbar :: navbar}"></nav>
<main class="container flex-grow-1">
<h1 class="mb-4">
<i class="fa-solid fa-circle-question text-primary me-2"></i> Preguntas frecuentes
</h1>
<div th:if="${faqs.isEmpty()}" class="alert alert-light border">
Aún no hay preguntas cargadas.
</div>
<div class="accordion" id="faqAccordion">
<div class="accordion-item" th:each="faq, iter : ${faqs}">
<h2 class="accordion-header">
<button class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
th:attr="data-bs-target='#collapse-' + ${iter.index}"
aria-expanded="false">
<span class="badge bg-secondary me-2" th:text="${faq.category}">Categoría</span>
<span th:text="${faq.question}">¿Pregunta?</span>
</button>
</h2>
<div th:id="'collapse-' + ${iter.index}"
class="accordion-collapse collapse"
data-bs-parent="#faqAccordion">
<div class="accordion-body" th:text="${faq.answer}">
Respuesta.
</div>
</div>
</div>
</div>
</main>
<footer th:replace="~{layout/footer :: footer}"></footer>
<script th:replace="~{layout/footer :: scripts}"></script>
</body>
</html>
¿Qué hace cada parte?
th:replace="~{layout/head :: head('Preguntas frecuentes')}" → reutiliza el <head> común (con Bootstrap, FontAwesome,
etc.).
th:replace="~{layout/navbar :: navbar}" → mete el navbar arriba.
th:if="${faqs.isEmpty()}" → si no hay FAQs, muestra el mensaje.
th:each="faq, iter : ${faqs}" → recorre la lista. iter.index es el índice (0, 1, 2...), lo usamos para que cada item del
acordeón tenga un id único.
th:text="${faq.question}" → reemplaza el texto del botón con la pregunta.
th:attr="data-bs-target='#collapse-' + ${iter.index}" → genera dinámicamente el atributo HTML que Bootstrap usa para
abrir/cerrar el panel.
El resultado es un acordeón clásico: pulsas una pregunta y se despliega su respuesta.
Paso 6 — Probar en el navegador
- Arranca la aplicación (
./mvnw spring-boot:run desde terminal, o el botón verde de IntelliJ).
- Abre
http://localhost:8080/faqs directamente en el navegador.
- Debes ver 5 preguntas con su categoría (Reservas, Anfitriones, Soporte, General).
- Pulsa en una pregunta y debe desplegarse la respuesta.
- Bonus: abre
http://localhost:8080/h2-console y haz SELECT * FROM faqs; para ver tus datos en la BD.
Criterios de aceptación
Errores comunes
| Error |
Causa |
Solución |
Whitelabel Error Page al ir a /faqs |
El return del controller no coincide con la ruta del HTML |
Verifica que devuelves |
exactamente "faq/faq-list" y que el HTML está en templates/faq/faq-list.html |
|
|
Cannot resolve symbol 'Faq' en el repositorio o controller |
Falta el import com.demo.model.Faq; |
Añádelo arriba del |
| archivo |
|
|
BeanCreationException al arrancar |
Probablemente el package del archivo no coincide con su carpeta |
El package del Java |
debe coincidir con la carpeta: model/Faq.java → package com.demo.model; |
|
|
La tabla faqs no aparece en h2-console |
El DataInitializer no se ejecuta porque falta @Component |
Asegúrate de tener |
@Component y @Profile("!test") arriba de la clase |
|
|
| El acordeón no se abre al pulsar |
Falta el script de Bootstrap |
Verifica que el `<script th:replace="~{layout/footer :: |
| scripts}"></script>` está al final del HTML |
|
|
Lo que NO tienes que hacer
- ❌ NO toques ningún archivo que ya existía antes. Solo creas archivos nuevos.
- ❌ NO añadas asociaciones a otras entidades (
@ManyToOne, @ManyToMany, etc.).
- ❌ NO modifiques el
navbar.html (el profesor añadirá el enlace después).
- ❌ NO crees formularios todavía (esa puede ser una tarea siguiente).
- ❌ NO escribas tests por ahora.
Si te quedas atascada
Mira estos archivos del proyecto como referencia (hacen lo mismo pero con otras entidades):
src/main/java/com/demo/model/Amenity.java → estructura de una entidad simple sin relaciones.
src/main/java/com/demo/repository/AmenityRepository.java → repositorio vacío.
src/main/java/com/demo/controller/ReviewController.java líneas 22-27 → patrón de listado.
src/main/resources/templates/review/review-list.html → patrón de plantilla con th:each.
El patrón se repite. Una vez entiendas esta cadena (entidad → repositorio → controller → plantilla) para FAQ, lo verás aplicado
igual en todas las demás partes del proyecto.
Objetivo
Crear una sección de preguntas frecuentes (FAQ) en la aplicación, accesible en
http://localhost:8080/faqs. Es una entidadnueva, completamente independiente del resto del proyecto: no se relaciona con casas, reservas, reseñas ni usuarios. Por eso
no necesitas coordinarte con nadie y no vas a tener conflictos de Git con tus compañeros.
¿Por qué esta tarea?
El objetivo es que practiques a tu ritmo el ciclo completo de Spring MVC con una pieza aislada y manejable:
@Entity) → cómo se define una tablaextends JpaRepository) → cómo se accede a la base de datosCommandLineRunner) → cómo se cargan datos de ejemplo@Controller,@GetMapping) → cómo se atiende una petición HTTPth:each,th:text) → cómo se pinta el HTMLCada paso está en un archivo distinto y los cinco son archivos nuevos. No tienes que modificar absolutamente nada de lo que
ya existe.
Lo que vas a crear
Faq.javasrc/main/java/com/demo/model/FaqRepository.javasrc/main/java/com/demo/repository/FaqDataInitializer.javasrc/main/java/com/demo/config/FaqController.javasrc/main/java/com/demo/controller/faq-list.htmlsrc/main/resources/templates/faq/(crea también la carpetafaq/)Paso 1 — Crear la entidad
FaqCrea
src/main/java/com/demo/model/Faq.java:¿Qué significa cada anotación?
@Entity→ le dice a JPA que esta clase representa una tabla.@Table(name = "faqs")→ la tabla se llamaráfaqsen la base de datos.@Id+@GeneratedValue(...IDENTITY)→ el campoides la clave primaria autoincremental.@Column(nullable = false, length = ...)→ restricciones de la columna en BD.@Getter,@Setter,@Builder,@NoArgsConstructor,@AllArgsConstructor→ Lombok te genera automáticamente losgetters/setters, el patrón builder y los dos constructores.
Paso 2 — Crear el repositorio
Crea
src/main/java/com/demo/repository/FaqRepository.java:Solo eso. Es una interfaz vacía. Heredando de
JpaRepository<Faq, Long>ya tienes gratis:findAll(),findById(),save(),deleteById(),count(), etc. Spring genera la implementación automáticamente.Paso 3 — Crear el DataInitializer
Crea
src/main/java/com/demo/config/FaqDataInitializer.java:¿Qué hace cada anotación?
@Component→ Spring registra esta clase como un bean (la instancia y la gestiona él).@Profile("!test")→ este código NO se ejecuta cuando se corren los tests (importante para no contaminar los tests).implements CommandLineRunner→ al arrancar la aplicación, Spring llama al métodorun()automáticamente.if (faqRepository.count() > 0) return;→ si ya hay datos, no inserta duplicados al reiniciar.Paso 4 — Crear el controller
Crea
src/main/java/com/demo/controller/FaqController.java:¿Qué hace cada línea?
@Controller→ Spring sabe que esta clase atiende peticiones HTTP y devuelve vistas Thymeleaf.FaqRepository→ Spring inyecta el repositorio automáticamente (inyección por constructor, no hace falta@Autowired).@GetMapping("/faqs")→ cuando alguien entra ahttp://localhost:8080/faqsse ejecuta este método.faqRepository.findAll()→ trae todos los registros de la tablafaqs.model.addAttribute("faqs", faqs)→ pasa la lista a la plantilla con el nombrefaqs.return "faq/faq-list"→ renderiza el archivotemplates/faq/faq-list.html.Paso 5 — Crear la plantilla HTML
Primero crea la carpeta
src/main/resources/templates/faq/, y dentro el archivofaq-list.html:¿Qué hace cada parte?
th:replace="~{layout/head :: head('Preguntas frecuentes')}"→ reutiliza el<head>común (con Bootstrap, FontAwesome,etc.).
th:replace="~{layout/navbar :: navbar}"→ mete el navbar arriba.th:if="${faqs.isEmpty()}"→ si no hay FAQs, muestra el mensaje.th:each="faq, iter : ${faqs}"→ recorre la lista.iter.indexes el índice (0, 1, 2...), lo usamos para que cada item delacordeón tenga un id único.
th:text="${faq.question}"→ reemplaza el texto del botón con la pregunta.th:attr="data-bs-target='#collapse-' + ${iter.index}"→ genera dinámicamente el atributo HTML que Bootstrap usa paraabrir/cerrar el panel.
El resultado es un acordeón clásico: pulsas una pregunta y se despliega su respuesta.
Paso 6 — Probar en el navegador
./mvnw spring-boot:rundesde terminal, o el botón verde de IntelliJ).http://localhost:8080/faqsdirectamente en el navegador.http://localhost:8080/h2-consoley hazSELECT * FROM faqs;para ver tus datos en la BD.Criterios de aceptación
./mvnw clean compilepasa)./faqsse ven las 5 preguntas en formato acordeón.h2-console,SELECT * FROM faqs;devuelve 5 filas.Errores comunes
Whitelabel Error Pageal ir a/faqsreturndel controller no coincide con la ruta del HTML"faq/faq-list"y que el HTML está entemplates/faq/faq-list.htmlCannot resolve symbol 'Faq'en el repositorio o controllerimport com.demo.model.Faq;BeanCreationExceptional arrancarmodel/Faq.java→package com.demo.model;faqsno aparece en h2-console@Component@Componenty@Profile("!test")arriba de la claseLo que NO tienes que hacer
@ManyToOne,@ManyToMany, etc.).navbar.html(el profesor añadirá el enlace después).Si te quedas atascada
Mira estos archivos del proyecto como referencia (hacen lo mismo pero con otras entidades):
src/main/java/com/demo/model/Amenity.java→ estructura de una entidad simple sin relaciones.src/main/java/com/demo/repository/AmenityRepository.java→ repositorio vacío.src/main/java/com/demo/controller/ReviewController.javalíneas 22-27 → patrón de listado.src/main/resources/templates/review/review-list.html→ patrón de plantilla conth:each.El patrón se repite. Una vez entiendas esta cadena (entidad → repositorio → controller → plantilla) para FAQ, lo verás aplicado
igual en todas las demás partes del proyecto.