Computación Web (2025/26)

Práctica 7: Desarrollo de una aplicación Web con Spring MVC (III)

Guardar mensajes en la base de datos

En esta práctica implementarás la funcionalidad de la aplicación de microblogging que se encarga de la gestión de mensajes: publicar nuevos mensajes de usuario y mostrarlos en la aplicación.

En primer lugar, de la misma forma que hiciste con la clase User en el laboratorio anterior, necesitarás anotar la clase Message como una entidad, para que Spring te permita almacenar los mensajes en la base de datos. Reemplaza el código de la clase Message con el siguiente:

package es.uc3m.microblog.model;

import java.util.Date;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;


@Entity
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(nullable = false)
    @NotBlank
    @Size(max = 256)
    private String text;

    @ManyToOne(optional = false)
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "response_to_id")
    private Message responseTo;

    @OneToMany(mappedBy = "responseTo")
    private java.util.List<Message> responses;

    @Column(nullable = false)
    private Date timestamp;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public Message getResponseTo() {
        return responseTo;
    }

    public void setResponseTo(Message responseTo) {
        this.responseTo = responseTo;
    }

    public java.util.List<Message> getResponses() {
        return responses;
    }

    public void setResponses(java.util.List<Message> responses) {
        this.responses = responses;
    }

    public Date getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(Date timestamp) {
        this.timestamp = timestamp;
    }
}
Anotación que indica que esta clase es una entidad cuyas instancias serán almacenadas en la base de datos.
La propiedad identifica unívocamente a los objetos de esta clase, y será utilizada como clave primaria de la tabla en la base de datos. Esto es, dos objetos de la clase Message representan la misma entidad si el valor de esta propiedad es el mismo en ambos, y representan entidades distintas en caso contrario. Normalmente, todas las entidades que definas deben contar con una propiedad de tipo entero anotada con @Id.
La propiedad, que debe ser un entero que identifique al objeto, tomará un valor que se generará automáticamente por el sistema cada vez que se cree un nuevo objeto de la clase Message. El sistema asegurará que dicho valor sea único. Tus aplicaciones no deben inicializar esta propiedad al crear nuevos objetos, de tal forma que pueda hacerlo el sistema.
Esta propiedad representa una relación muchos a uno entre mensajes y usuarios. Es decir, cada mensaje es publicado por un usuario, y un usuario puede publicar muchos mensajes. La anotación @ManyToOne explicita esta relación. Además, el modificador optional = false indica que esta relación es obligatoria, es decir, que todo mensaje debe estar asociado a un usuario no nulo.
La relación entre mensajes y usuarios se implementa en la base de datos con una clave ajena. La anotación @JoinColumn se usa para indicar el nombre de la columna que se usará para esta clave ajena (en este caso, la columna user_id) en la tabla del lado del muchos (en este caso, la tabla de mensajes).
La relación entre mensajes y mensajes también se implementa en la base de datos con una clave ajena. En este caso, la anotación @JoinColumn indica que se usará la columna response_to_id como clave ajena para esta relación. Esta columna se añadirá a la tabla de mensajes.
Lado inverso de la relación entre mensajes y mensajes, representada por el atributo responseTo.
Esta propiedad representa una relación muchos a uno entre mensajes y mensajes. Es decir, cada mensaje puede ser una respuesta a otro mensaje, y un mensaje puede tener muchas respuestas. Sin embargo, esta relación es opcional (no todos los mensajes tienen por qué ser respuestas a otros mensajes), por lo que no se ha incluido el modificador optional = false. Los mensajes que no sean respuestas a ningún otro mensaje tendrán esta propiedad con valor null.
El tipo de datos java.util.Date se usa para representar una fecha y hora (día, mes, año, hora, minutos y segundos).

Observa en la declaración de esta clase que:

La relación entre usuarios y mensajes es bidireccional. Es decir, una relación muchos a uno de los mensajes a los usuarios corresponde a una relación uno a muchos de los usuarios a los mensajes. Modifica la clase User para que declare, además de las propiedades que ya tiene, esta relación:

// New classes to import:
import java.util.List;
import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;

// New attribute to add to the User class:
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
private List<Message> messages;

// Access methods for the new attribute:
public List<Message> getMessages() {
    return messages;
}

public void setMessages(List<Message> messages) {
    this.messages = messages;
}
Existe una relación uno a muchos entre usuarios y mensajes, lo que significa que un usuario puede publicar muchos mensajes y que un mensaje es publicado por un usuario.
Por razones de eficiencia, esta relación está marcada como perezosa con el modificador FetchType.LAZY. Esto significa que, cuando se cargue un usuario de la base de datos, sus mensajes no se cargarán automáticamente. Sin embargo, la lista de mensajes del usuario seguirá siendo accesible: si en algún momento tu aplicación invoca el método getMessages() de un usuario, el sistema irá automáticamente a la base de datos en ese momento para recuperarlos.
El atributo mappedBy de la anotación @OneToMany se usa para indicar que esta relación es bidireccional y que la parte inversa de esta relación (es decir, la parte de los mensajes) se declara en el atributo user de la clase Message.
El tipo de esta propiedad es List<Message>, lo que significa que representa una lista de objetos de la clase Message. Esto debe ser así porque un usuario puede publicar muchos mensajes.

Observa que:

Para comprobar que este código se ha aplicado correctamente, inicia el servidor y, una vez iniciado, detenlo de nuevo. El esquema de la base de datos debería haber sido actualizado. Puedes comprobar esto conectándote a tu base de datos y ejecutando las siguientes sentencias SQL:

SHOW TABLES;
DESCRIBE message;
DESCRIBE user;

Comprueba que ambas tablas incluyen las columnas que esperabas de la declaración de las clases User y Message.

Creación de mensajes

Crea una nueva interfaz MessageRepository, análoga a UserRepository pero, de momento, sin métodos.

Añade a MainController un nuevo método que recibirá los datos del formulario de creación de mensajes y los publicará. Asignaremos a este controlador la ruta /post:

// New classes to import:
import java.security.Principal;
import es.uc3m.microblog.model.MessageRepository;

// New attribute:
@Autowired
private MessageRepository messageRepository;

// New method:
@PostMapping(path = "/post")
public String postMessage(@ModelAttribute Message message, Principal principal) {
    User user = userRepository.findByEmail(principal.getName());
    message.setUser(user);
    message.setTimestamp(new Date());
    messageRepository.save(message);
    return "redirect:message/" + message.getId();
}
Este método se encargará de manejar las peticiones POST a la ruta /post.
Debido a la anotación @ModelAttribute, el objeto message se crea y rellena automáticamente con los datos del formulario de creación de mensajes. Para que esto funcione, el formulario de creación de mensajes debe tener un campo de texto con el atributo name igual a "text", que es el mismo nombre que el de la propiedad en la clase Message.
El parámetro principal de la clase Principal da acceso a la identidad del usuario que ha iniciado sesión. La propiedad name de este objeto contiene su dirección de correo electrónico, que se utiliza en la primera línea del método para recuperar el objeto User correspondiente.
Se establece el usuario que ha creado el mensaje, porque este no viene del formulario.
Se establece la fecha de creación del mensaje con la fecha y hora actuales. Esto tampoco viene del formulario.
El nuevo mensaje se almacena en la base de datos.
Se redirige al usuario a la vista del mensaje que acaba de crear.

Observa en este fragmento de código que:

A continuación, necesitas ajustar el formulario de creación de mensajes que deberías tener en la vista principal. En particular, debes asegurarte de que:

Para probar este código, accede a la vista principal y crea un nuevo mensaje. La redirección a la vista de mensajes fallará porque se necesitan algunos ajustes en esa vista, que harás en el próximo ejercicio. Sin embargo, deberías ver el mensaje en la base de datos si listas el contenido de la tabla message.

Vista de mensaje

En una práctica anterior preparaste una versión preliminar del método de controlador para la vista de mensaje. Este simplemente creaba un mensaje de prueba y algunas respuestas al mismo, y los mostraba.

En este ejercicio cambiarás ese método para que, en lugar de mostrar un mensaje de prueba especificado directamente en el código, reciba el identificador del mensaje a mostrar como un parámetro de la petición, recupere ese mensaje de la base de datos y se lo pase a la plantilla para mostrarlo.

Se muestra a continuación un ejemplo de cómo sería este método. Necesitas reemplazar tu método antiguo con este, pero ten en cuenta que es posible que necesites adaptar algunas cosas para integrarlo correctamente. En particular, este ejemplo asume que la ruta de esta vista es /message y que la plantilla de Thymeleaf se llama message_view.html. Cámbialos si en tu código son diferentes.

// New classes to import:
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.bind.annotation.PathVariable;

// New code for the controller method of the message view:
@GetMapping(path = "/message/{messageId}")
public String messageView(@PathVariable("messageId") int messageId, Model model) {
    Optional<Message> messageOpt = messageRepository.findById(messageId);
    if (!messageOpt.isPresent()) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Message not found");
    }
    model.addAttribute("message", messageOpt.get());
    return "message_view";
}
Este método se encargará de manejar las peticiones GET a la ruta /message/{messageId}. El fragmento {messageId} de la ruta es una variable de ruta que se usará para recibir el identificador del mensaje a mostrar.
El valor de la variable de ruta messageId se inyecta en el parámetro del método con la anotación @PathVariable.
Los objetos de tipo Optional pueden contener un valor de tipo Message o no contener ningún valor. El método isPresent() devuelve true en caso afirmativo, y false en caso negativo. El método get() devuelve el valor contenido si lo hay, o lanza una excepción si no lo hay.
Si no se encuentra el mensaje, se lanza una excepción de tipo ResponseStatusException con el código de error HTTP 404 (no encontrado). Esto hace que el servidor responda con una respuesta de error de HTTP 404 NOT FOUND.
Se pasa el mensaje como dato a la plantilla Thymeleaf para esta que pueda mostrarlo.

Observa en el código anterior que:

Debido a que ahora añadimos el identificador del mensaje a la ruta de este controlador, las rutas relativas en tu plantilla dejarán de funcionar correctamente (estarás un nivel más bajo que antes). En este ejercicio y en los próximos necesitarás cambiar las rutas relativas que uses en plantillas Thymeleaf tal y como se muestra en los siguientes ejemplos:

<link rel="stylesheet" th:href="@{/example.css}">
<script th:src="@{/public/example.js}"></script>
<a th:href="@{/}">Volver al inicio</a>
<a th:href="@{/user/453}">@mary</a>
<form th:action="@{/delete}" method="post">...</form>
La notación @{...} crea rutas relativas a la raíz de la aplicación. Esto hace que las rutas funcionen correctamente independientemente de la ruta del controlador que esté mostrando la plantilla.
La ejecución de la plantilla resultará en un atributo href con el valor de la ruta calculada.

Observa el uso de th:href, th:src y th:action, y la notación @{...} que usamos en los ejemplos anteriores.

Reinicia la aplicación web y crea un nuevo mensaje. Si todo funciona, inmediatamente después de crear el nuevo mensaje se debería presentar la vista del mensaje con los datos de ese nuevo mensaje.

Mostrar mensajes en la vista principal

Dado que de momento no tenemos la funcionalidad para que un usuario siga a otros, simplemente mostraremos en la vista principal los diez mensajes más recientes que se hayan publicado, independientemente de quién sea su creador. En este ejercicio recuperaremos estos mensajes desde la base de datos y los mostraremos en la vista principal, en lugar de los mensajes fijos que están mostrando ahora.

Podemos hacer que la interfaz MessageRepository devuelva los diez mensajes más recientes simplemente declarando el siguiente método en MessageRepository:

// New import statement
import java.util.List;

// New method declaration for MessageRepository
List<Message> findFirst10ByOrderByTimestampDesc();
El nombre de este método sigue una convención que Spring Data JPA reconoce para generar automáticamente su implementación. Recupera los diez mensajes más recientes de la base de datos.

El nombre findFirst10ByOrderByTimestampDesc le está diciendo a Spring Data JPA que queremos obtener los primeros diez mensajes, ordenados por su propiedad timestamp, que es de tipo Date, en orden descendente (es decir, las fechas más recientes, primero). Como sucedió con los métodos de los párrafos anteriores, Spring nos proporcionará automáticamente una implementación para este método.

Modifica el método del controlador que maneja la vista principal para que, en lugar de crear los mensajes de ejemplo fijos, obtenga, invocando este nuevo método de MessageRepository, los diez mensajes más recientes. Almacénalos en Model para que la plantilla los muestre.

Modifica la plantilla Thymeleaf de la vista principal para que, como consideres oportuno, cada mensaje enlace a su propia vista de mensaje. Es decir, queremos que si los usuarios pinchan en cualquier mensaje (tú decides exactamente dónde hacerlo, ya sea en el texto del mensaje, en un enlace adicional, etc.), se les muestre la vista de dicho mensaje.

Para hacer esto debes construir un hipervínculo cuyo camino incluya el identificador del mensaje (por ejemplo, message/42 cuando el usuario pincha en el mensaje con identificador 42). Thymeleaf proporciona soporte para esto, como se muestra en el siguiente ejemplo:

<a th:href="@{/message/{id}(id=${message.getId()})}">Go to the message</a>
El fragmento {id} de la ruta es una variable de ruta que se usará para recibir el identificador del mensaje a mostrar. Entre paréntesis después de la ruta se indican los valores de las variables de ruta, con el formato nombreVariable=valor. En este caso, el valor de la variable de ruta id se obtiene del atributo id del mensaje que se está mostrando.

El código del ejemplo le dice a Thymeleaf que inyecte la variable id al final del camino, donde id toma el resultado de evaluar la expresión ${message.getId()}, es decir, el identificador del mensaje que se está mostrando.

Reinicia la aplicación y comprueba que todo funciona como se espera.

Vista de perfil de usuario

El último paso de esta práctica es actualizar la vista del perfil de usuario para que, en vez de mostrar un usuario de prueba fijo como hasta ahora, muestre el perfil del usuario cuyo identificador reciba como parte de la ruta. Deben mostrarse, al menos, el nombre del usuario y todos los mensajes publicados por este, ordenados de más a menos reciente.

Deberías hacer algo similar a lo que hemos hecho con la vista de mensajes en ejercicio anteriores. En particular: