Práctica 8: Desarrollo de una aplicación Spring (IV)
Respondiendo a mensajes
Hasta ahora, tu aplicación no permite responder a mensajes. En este ejercicio implementaremos esta nueva función en la vista de mensaje. Para no complicar en exceso la aplicación, no se permitirá responder a mensajes que sean respuestas a otros mensajes. Esto es, las respuestas no podrán ser anidadas.
En primer lugar,
modifica la plantilla de la vista de mensaje
añadiendo un nuevo formulario que permita responder al mensaje mostrado.
Este formulario será casi igual
que el de publicar nuevos mensajes en la vista principal,
y también se enviará al mismo controlador,
con la diferencia de que incluirá
un control adicional de tipo hidden
que especifica el identificador del mensaje
al cual el usuario está respondiendo.
Los controles ocultos no se muestran en el navegador,
pero sus datos se envían junto
con los datos de los otros controles del formulario
cuando el usuario lo envía:
<input type="hidden" name="responseTo" th:value="${message.getId()}">
El código del controlador que programaste en el laboratorio anterior
ya es capaz de recoger automáticamente estos datos
al coincidir el nombre del control (responseTo
)
con la propiedad de la clase Message
que tiene el mismo nombre.
Por lo tanto,
si el mensaje que estás creando
no es una respuesta a otro mensaje,
su propiedad responseTo
tendrá un valor null
.
De lo contrario,
si es una respuesta a otro mensaje,
ese otro mensaje estará disponible
en su propiedad responseTo
.
Por último, debes modificar la redirección al final del controlador:
- Si el mensaje que has creado no es una respuesta, simplemente redirige a la vista de ese mensaje (tu código ya está haciendo eso).
- Si es una respuesta, redirige a la vista del mensaje original al cual el usuario está respondiendo. Modificarás esa vista en el próximo ejercicio para que muestre el mensaje original junto con las respuestas a él.
Para comprobar que las respuestas se están almacenando correctamente
en la base de datos,
conéctate a tu base de datos
y verifica que los mensajes de respuesta que creas estén allí
y tengan el identificador correcto en su columna response_to_id
.
Mostrando mensajes de respuesta
En este momento, la vista de mensaje muestra un mensaje sin sus respuestas. Modifícala para que las respuestas, si las hay, se muestren, ordenadas de más recientes a menos recientes.
Para obtener la lista ordenada de respuestas,
puedes declarar el siguiente método
en la interfaz MessageRepository
:
List<Message> findByResponseToOrderByTimestampAsc(Message message);
Nuevamente, el entorno de Spring Data JPA derivará automáticamente la implementación de este método a partir de su nombre.
Ocultando mensajes de respuesta en algunas vistas
Dado que los mensajes de respuesta son ciudadanos de segunda clase en nuestra aplicación, no queremos que se muestren en la vista principal ni en la vista de usuario. Además, no queremos mostrar la vista de mensaje para los mensajes de respuesta.
Aplica los siguientes cambios:
-
En la vista principal,
obtén los 10 mensajes más recientes
que no sean respuestas.
Puedes declarar y utilizar el siguiente método nuevo
en la interfaz
MessageRepository
:List<Message> findFirst10ByResponseToIsNullOrderByTimestampDesc();
-
En la vista del perfil de usuario,
obtén solo aquellos mensajes que no sean respuestas.
Puedes declarar y utilizar el siguiente método nuevo
en la interfaz
MessageRepository
:List<Message> findByUserAndResponseToIsNullOrderByTimestampDesc(User user);
- En la vista de mensaje, aborta con un error 403 (Prohibido) si el mensaje a mostrar es un mensaje de respuesta.
Los usuarios pueden seguir a otros usuarios
Implementemos ahora una nueva funcionalidad para que los usuarios puedan seguir a otros usuarios de la aplicación.
El primer paso consiste en actualizar el modelo de la base de datos.
Dado que un usuario puede seguir a muchos usuarios y ser seguido por muchos usuarios también,
hay una relación de muchos a muchos desde la tabla de usuarios hacia sí misma.
Las relaciones de muchos a muchos se modelan en JPA con la anotación @ManyToMany
.
Agrega los siguientes dos atributos a tu clase User
:
// Nuevas clases a importar:
import jakarta.persistence.ManyToMany;
import jakarta.persistence.JoinTable;
// Declaraciones de los nuevos atributos:
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name="following",
joinColumns=@JoinColumn(name="follower_id"),
inverseJoinColumns=@JoinColumn(name="followed_id"))
private List<User> following;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name="following",
joinColumns=@JoinColumn(name="followed_id"),
inverseJoinColumns=@JoinColumn(name="follower_id"))
private List<User> followers;
Recuerda también programar los métodos getter y setter para estos dos atributos.
El atributo following
almacenará
la lista de usuarios a los que este usuario sigue.
El atributo followers
almacenará
la lista de usuarios que siguen a este usuario.
Ambos atributos están anotados como lazy, lo que significa que estas listas solo se cargarán desde la base de datos si tu programa accede a ellas.
Las relaciones de muchos a muchos se modelan a través de una tabla intermedia. En este caso, esa tabla consta de dos columnas: el id del seguidor y el id del seguido. Ambos son claves ajenas que hacen referencia a filas en la tabla de usuarios. Además, la combinación de ambas columnas forma la clave primaria de la tabla.
La anotación @JoinTable
que estamos utilizando le indica al sistema de persistencia
que ambas relaciones se basan en esa tabla intermedia llamada following
.
Contendrá dos columnas,
una para almacenar el id del usuario seguido (llamada followed_id
)
y la otra para almacenar el id del usuario seguidor (llamada follower_id
).
La lista de usuarios a los que sigue este usuario se construye
haciendo coincidir el id del usuario con la columna follower_id
de la tabla intermedia
(es decir, este usuario es el seguidor de la relación).
Por esa razón, declaramos follower_id
como la columna de unión en este caso.
La lista de usuarios que siguen a este usuario se construye haciendo coincidir la otra columna,
y por eso declaramos followed_id
como la columna de unión en este otro caso.
Por último, debes permitir que Spring Data JPA actualice tu base de datos. Reinicia tu aplicación y la nueva tabla se creará automáticamente.
Comprueba que haya aparecido en la base de datos
una nueva tabla llamada following
.
Examina qué columnas contiene.
El modelo está completo ahora. En el próximo ejercicio aprenderás cómo utilizar estos dos nuevos atributos para que los usuarios sigan o dejen de seguir a otros usuarios.
Un servicio para seguir y dejar de seguir a usuarios
Dado que las operaciones de seguir y dejar de seguir
tienen cierta complejidad,
principalmente debido a las comprobaciones de datos que serán necesarias,
moveremos su implementación a un servicio.
En particular,
extenderemos la interfaz UserService
y su implementación UserServiceImpl
,
que ya tienes en tu proyecto.
Primero,
crea una nueva clase de excepción personalizada
llamada UserServiceException
en el paquete es.uc3m.microblog.services
:
package es.uc3m.microblog.services;
public class UserServiceException extends Exception {
private static final long serialVersionUID = 1L;
public UserServiceException() {
}
public UserServiceException(String message) {
super(message);
}
}
A continuación,
agrega los siguientes tres métodos
a la interfaz UserService
:
boolean follows(User follower, User followed);
void follow(User follower, User followed) throws UserServiceException;
void unfollow(User follower, User followed) throws UserServiceException;
Los utilizaremos para hacer que un usuario empiece a seguir a otro usuario, para que deje de seguir a otro usuario y para verificar si un usuario está siguiendo actualmente a otro.
Ahora, necesitamos programar estos métodos
en la clase UserServiceImpl
:
-
En el método
follows
, devuelvetrue
si el usuario seguido está contenido en la lista de usuarios que sigue el usuario seguidor, yfalse
en caso contrario. Recuerda que puedes usar el métodocontains
de la interfazList
. -
En el método
follow
, verifica primero que el usuario seguidor no esté intentando seguirse a sí mismo (puedes usar el métodoequals
que tienen o comparar sus identificadores), y que el usuario seguidor no esté siguiendo ya al usuario seguido. En cualquiera de esos casos, lanza una excepciónUserServiceException
. Después de esas comprobaciones, simplemente agrega el usuario seguido a la lista de usuarios que sigue el usuario seguidor, y luego guarda al usuario seguidor:follower.getFollowing().add(followed); userRepository.save(follower);
-
En el método
unfollow
, verifica que el usuario seguidor esté siguiendo al usuario seguido. Si no es así, lanza una excepciónUserServiceException
. El método debe eliminar al usuario seguido de la lista de usuarios que sigue el usuario seguidor. Puedes usar el métodoremove
de la interfazList
. Finalmente, guarda al usuario seguidor en la base de datos.
Comprobarás esta funcionalidad más adelante, cuando avances más con su implementación.
Controladores para seguir y dejar de seguir a usuarios
Ahora que la base de datos permite representar la relación de seguidor / seguido entre usuarios y que tenemos un servicio para seguir y dejar de seguir, terminemos la funcionalidad que permite a los usuarios, cuando visitan el perfil público de otro usuario, seguirlo o dejar de seguirlo.
Primero,
programa un nuevo método controlador en MainController
.
Hará que el usuario asociado a la sesión actual
siga a otro usuario.
Este último se recibirá como un parámetro en la URL:
@PostMapping(path = "/follow/{userId}")
public String follow(@PathVariable("userId") int followedUserId, Principal principal) {
(...)
}
Este controlador usará el servicio de usuario
para hacer que el usuario autenticado siga al usuario cuyo id recibe.
Necesita obtener este último
de la base de datos
(y devolver un error 404 No Encontrado si no está allí)
y usar el método follow
del servicio.
Si ese método lanza una excepción UserServiceException
,
el método del controlador debería devolver
un error 403 Prohibido.
Cuando todo esté hecho,
el controlador debería devolver una respuesta de redirección
a la página de perfil del usuario a seguir.
Programa otro controlador para dejar de seguir a un usuario. Será bastante similar al que acabas de programar.
Aún no puedes comprobar estos dos nuevos controladores. Para eso, necesitas añadir un formulario para seguir o dejar de seguir a un usuario en la vista del perfil de usuario. Harás eso en el próximo ejercicio.
Formularios para seguir y dejar de seguir a usuarios
En primer lugar,
modifica el controlador de la vista del perfil de usuario
para que envíe un nuevo parámetro llamado, por ejemplo,
followButton
a la plantilla.
Será una cadena que tomará los valores
"none" (cuando el usuario actual y el usuario mostrado son el mismo),
"follow" (cuando el usuario actual
todavía no está siguiendo al usuario mostrado en esa vista de perfil) y
"unfollow" (cuando el usuario actual
ya está siguiendo al usuario mostrado en esa vista de perfil).
Con ese parámetro, la vista sabe ahora qué formulario mostrar: un formulario para seguir, un formulario para dejar de seguir o ningún formulario. Esos formularios solo necesitan un control, que será un botón de envío con el texto apropiado. Se verían así (es posible que necesites ajustar cosas, como las URL de acción o el nombre de la variable que contiene el usuario cuyo perfil se está mostrando):
<form th:if="${followButton.equals('follow')}"
th:action="@{/follow/{user_id}(user_id=${user.getId()})}" method="post">
<input type="submit" value="Follow">
</form>
<form th:if="${followButton.equals('unfollow')}"
th:action="@{/unfollow/{user_id}(user_id=${user.getId()})}" method="post">
<input type="submit" value="Unfollow">
</form>
Observa cómo la URL del controlador de destino se construye
a partir del identificador del usuario cuyo perfil se está mostrando.
Observa también que el formulario se enviará con una solicitud POST
,
ya que es lo que esperan los métodos del controlador.
Ahora puedes probar las funciones de seguir y dejar de seguir.
Conéctate también a la base de datos para asegurarte
de que todas las acciones de seguir y dejar de seguir
que hagas en la aplicación
se guarden correctamente en la tabla following
.
Mostrar mensajes de los usuarios a quien se sigue en la vista principal
Para finalizar la funcionalidad de seguir / dejar de seguir, necesitamos modificar la consulta en el controlador de la vista principal para que obtenga, en lugar de los 10 mensajes más recientes de cualquier usuario, los 10 mensajes más recientes de los usuarios a los que el usuario actual está siguiendo.
Una forma de obtener estos mensajes habría sido
añadir la siguiente declaración de método
a MessageRepository
:
List<Message> findFirst10ByUserInOrderByTimestampDesc(List<User> users);
La cláusula byUserIn
hace que la consulta
solo seleccione mensajes de los usuarios proporcionados en la lista
que recibe el método.
Si esa lista contiene a los usuarios a los que sigue el usuario actual,
resuelve la tarea en cuestión.
Sin embargo,
con esta solución
estamos haciendo dos consultas a la base de datos:
una para obtener esa lista de usuarios seguidos
y otra para obtener los mensajes.
Una forma de resolver este problema con solo una consulta
sería combinar tres tablas con INNER JOIN
:
tabla de usuarios consigo misma (para obtener los usuarios seguidos)
y luego con mensajes (para obtener los mensajes en sí mismos).
Sin embargo,
no podemos expresar esto con la interpolación de métodos.
En su lugar,
podemos proporcionar la consulta
en Jakarta Persistence Query Language (JPQL),
anteriormente conocido como Java Persistence Query Language.
Este lenguaje está basado en SQL pero,
a diferencia de SQL,
es portable
a cualquier sistema de gestión de bases de datos relacionales compatible.
Resolvamos el problema de esta otra manera.
Primero, añade la siguiente declaración a MessageRepository
:
// Nuevas clases a importar:
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
// Nueva declaración de método:
@Query("SELECT messages "
+ "FROM User user "
+ "JOIN user.following followed "
+ "JOIN followed.messages messages "
+ "WHERE user=?1 AND messages.responseTo IS NULL "
+ "ORDER BY messages.timestamp DESC")
List<Message> messagesFromFollowedUsers(User user, Pageable pageable);
Fíjate en que:
-
La anotación
@Query
te permite especificar una consulta JPQL personalizada. El sistema de Spring Data JPA implementará automáticamente el método a partir de nuestra consulta JPQL. -
Primero,
la consulta hace una combinación de la tabla de usuarios
consigo misma
sobre la relación following.
Con eso,
se obtienen los usuarios seguidos por el usuario actual
(al cual llamamos
user
en la consulta). Se les llamaráfollowed
en la consulta. Luego, otra combinación obtiene los mensajes publicados por estos usuarios seguidos. Los mensajes de respuesta se descartan. -
El usuario que usamos en la consulta se obtiene del parámetro del método
a través del elemento
?1
de la consulta. Es un 1 porqueuser
es el primer parámetro del método (una consulta podría recibir más de un parámetro). -
Dado que JPQL no permite limitar el número de resultados,
pero necesitamos solo 10,
proporcionamos el parámetro
Pageable pageable
, que permitirá al llamante obtener el número de resultados que necesita.
Ahora, debes ir al método del controlador para la vista principal y cambiar la forma en que obtienes los mensajes a mostrar allí:
// Nuevas clases a importar:
import org.springframework.data.domain.PageRequest;
// Llamada para obtener la lista de mensajes:
List<Message> messages = messageRepository.messagesFromFollowedUsers(user, PageRequest.of(0, 10));
La llamada PageRequest.of(0, 10)
hace que la consulta devuelva los primeros 10 mensajes.
Su primer parámetro es el número de página que necesitas
(la primera página es la número 0)
y el segundo es el número de resultados por página
(10 en este caso).
Entonces,
obtendremos resultados del 1 al 10.
Por ejemplo,
si hubiéramos querido obtener resultados del 11 al 20
habríamos usado PageRequest.of(1, 10)
.
Comprueba ahora que la vista funciona y muestra los mensajes correctos.