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.ManyToOne;
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)
private User user;
@ManyToOne
private Message responseTo;
@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 Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
}
Observa en la declaración de esta clase que:
- Los mensajes tienen, de forma similar a los usuarios, un identificador numérico único, que será establecido automáticamente por el sistema cada vez que crees un nuevo mensaje.
- El texto de los mensajes está limitado a 256 caracteres, ya que se espera que los mensajes sean cortos en este tipo de redes sociales. El texto del mensaje debe contener al menos un carácter no blanco (esto es, que no sea espacio en blanco, fin de línea, tabulador, etc.).
- Existe una relación muchos a uno entre mensajes y usuarios, lo que significa que un usuario puede publicar muchos mensajes y que un mensaje es publicado por un usuario. Es bastante importante hacer explícita esta relación con esta anotación para que el sistema cree las claves ajenas apropiadas en la base de datos. Además, esta relación está marcada como no opcional, lo que significa que todo mensaje debe estar asociado a un usuario no nulo.
- También existe una relación muchos a uno entre mensajes y mensajes, lo que significa que muchos mensajes pueden responder a un mensaje. A diferencia de la relación entre mensajes y usuarios, esta relación está marcada como opcional porque no es obligatorio que un mensaje sea una respuesta a otro mensaje. Es decir, se permite a los usuarios crear mensajes que no respondan a ningún otro mensaje.
- Finalmente, la fecha de creación de un mensaje no puede ser nula.
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.JoinColumn;
import jakarta.persistence.OneToMany;
// New attribute to add to the User class:
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
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;
}
Observa que:
-
Por razones de eficiencia,
la relación está marcada como perezosa
con el modificador
FetchType.LAZY
. Esto significa que, cuando cargues un usuario de la base de datos, sus mensajes no se cargarán automáticamente. Imagina lo ineficiente que sería cargar, por ejemplo, un usuario que ha creado mil mensajes. Sin embargo, la lista de mensajes del usuario seguirá siendo accesible: si en algún momento tu aplicación invoca el métodogetMessages()
de un usuario, el sistema irá automáticamente a la base de datos en ese momento para recuperarlos. -
Es necesario proporcionar una pista al sistema
sobre cómo establecer la relación entre usuarios y mensajes.
Con la anotación
@JoinColumn
se le dice al sistema que utilice la propiedad con el identificador del usuario de cada mensaje siempre que necesite buscar los mensajes creados por un usuario dado.
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();
}
Observa en este fragmento de código que:
-
Como se hizo antes en el método del controlador de registro de usuarios,
el texto del objeto
message
se rellena automáticamente con los datos del formulario de creación de mensajes (crearás o adaptarás este formulario en breve en este ejercicio). Esto sucede debido a la anotación@ModelAttribute
. -
Se recibe un nuevo parámetro llamado
principal
de la clasePrincipal
. Te dará acceso a la identidad del usuario que ha iniciado sesión. La propiedadname
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 objetoUser
correspondiente. -
El texto del mensaje ya se ha establecido automáticamente,
pero todavía es necesario
rellenar las propiedades
user
ytimestamp
. El primero se ha obtenido de la base de datos al buscar por dirección de correo electrónico. - Por ahora, se asume que este mensaje no responde a ningún otro mensaje. La creación de mensajes de respuesta se implementará en la próxima práctica.
-
El nuevo mensaje se almacena en la base de datos
con el método
save
demessageRepository
. Al declarar este atributo con la etiqueta@Autowired
, el sistema de inyección de dependencias de Spring se encarga de su inicialización. -
Finalmente,
el control se pasa a la vista que mostrará
este nuevo mensaje.
Se asume en el código anterior que esta vista,
que ya programaste en una práctica previa,
pero que ajustarás en el próximo ejercicio,
responde a la ruta
/message
(podría ser diferente en tu caso, así que deberías poner aquí la ruta que le asignaste en dicha práctica). Además, añadiremos un nuevo componente a la ruta con el identificador del mensaje a mostrar. Por ejemplo, para mostrar el mensaje con identificador 768 en la base de datos la ruta de redirección serámessage/768
. En el próximo ejercicio aprenderás a procesar este identificador de mensaje para cargar el mensaje desde la base de datos.
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:
-
La ruta y el método
se especifican como
<form th:action="@{/post}" method="post">
, para que los datos se envíen al método del controlador que acabamos de programar. -
El atributo
name
del campo de texto donde los usuarios editan sus mensajes toma el valor"text"
. De esta forma, es el mismo nombre que el de la propiedad en la claseMessage
, y el valor se establecerá automáticamente sin que tengas que hacer nada más.
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";
}
Observa en el código anterior que:
-
El identificador del mensaje a mostrar
se recibe como una parte de la ruta.
Por ejemplo, la ruta
/message/42
se usa para mostrar el mensaje con identificador 42. A esto se le conoce en teminología de Spring como una variable de ruta. Como puedes ver, necesitas declarar el nombre de la variable entre llaves como parte de la ruta, y declarar un nuevo parámetro del método con el mismo nombre y la anotación@PathVariable
. El parámetro"messageId"
de la anotación@PathVariable
solo es necesario si no usas Gradle para compilar. Si usas Gradle tal y como se explica en los enunciados, puedes tanto dejarlo como omitirlo. -
El método
findById
de la interfazMessageRepository
se usa para localizar un mensaje dado su identificador. Aunque no declaraste este método en esa interfaz, está disponible porque se hereda deCrudRepository
. Los datos devueltos son de tipoOptional
. Los objetos de este tipo tienen un método llamadoisPresent
, que indica si hay o no un valor, y un métodoget
para acceder al valor si lo hay. -
Si no se encuentra el mensaje,
se devuelve un error 404 (no encontrado),
ya que este es el error estándar en HTTP
para contestar cuando la ruta proporcionada por el cliente
no coincide con ningún recurso en el servidor.
En Spring esto se puede hacer
simplemente lanzando una excepción de tipo
ResponseStatusException
. -
Si todo funciona, simplemente añades el mensaje recuperado
al objeto
Model
, para que esté disponible para la plantilla Thymeleaf. - De momento no cargamos las respuestas a este mensaje. Si ya tenías respuestas de ejemplo creadas en tu versión anterior de este método, puedes dejarlas por el momento, hasta que implementemos las funcionalidades necesarias para manejar mensajes de respuesta en la próxima práctica.
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>
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 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 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:
-
Deberías recibir el identificador del usuario a mostrar
como parte de la ruta.
Por ejemplo:
/user/23
. -
Para obtener todos los mensajes publicados por dicho usuario
podrías usar el método
getMessages
de la claseUser
. Sin embargo, queremos tener control sobre el orden de los mensajes. Por lo tanto, es mejor declarar un nuevo método enMessageRepository
que devuelva los mensajes del usuario que le pases como parámetro ordenados de más a menos reciente:List<Message> findByUserOrderByTimestampDesc(User user);
-
El método del controlador necesita proporcionar a la plantilla Thymeleaf
el objeto
User
del usuario a mostrar y la lista de mensajes, estableciéndolos como atributos del objetoModel
. - Para todas las menciones que se hagan de un usuario en las demás vistas de la aplicación como, por ejemplo, en la vista principal, donde se muestra el autor de cada mensaje, se debería proporcionar un hipervínculo que apunte a la vista de usuario del usuario mencionado.
- Análogamente, cada mensaje mostrado en la vista de usuario debería incluir un hipervínculo que apunte a su propia vista de mensaje.