Blog
Vamos a seguir trabajando con las relaciones de las bases de datos, es importante practicar bastante las relaciones ya que es muy importante para manejar bien las bases de datos y por ende hacer aplicaciones más sólidas en cuanto a los registros de datos. Anteriormente hice una relación de uno a muchos en el la lección 4 de este curso.
Relación de uno a muchos
Este tipo de relación es cuando tenemos 1 campo de 1 tabla que hace relación con todos los campos de otra tabla, como anteriormente dije, en la lección 4 hicimos este tipo de relación cuando relacionamos el campo id de la tabla usuarios con todos los campos de la tabla pubs
Esto da a entender que un usuario tiene muchas publicaciones y las publicaciones tienen muchos usuarios, el campo id_usuario vincula al usuario con la tabla pubs al momento de realizar una publicación.
Muchos a muchos
No hemos creado este tipo de relación, pero de igual forma es bueno explicarla para tener en cuenta de que existen. Este tipo de relación se da cuando tenemos que relacionar los campos 1 tabla (que para este caso se conoce como tabla auxiliar) con todos los campos de otras tablas, para este caso puedo darte el ejemplo de una tabla actores y una tabla películas:
Si miras la imagen, quizás no le veas mucha lógica. Pero la verdad es que si tuviésemos este tipo de relación e hiciéramos las consultas correspondientes, estas relaciones responderían correctamente. Voy a tratar de explicar esta relación un poco mejor ya que viste la imagen, supongamos que un actor quiere consultar el total de películas en las que ha participado, entonces la tabla actor hace la consulta a través de la tabla actores_películas porque esta relacionado con esta tabla gracias a actores_id, aquí es donde esta la clave para que todo funcione correctamente. Como las tablas se encuentran relacionadas, la tabla actores puede utilizar el campo peliculas_id y utilizar este campo como una especie de túnel para que su consulta viaje hasta la tabla películas donde la tabla películas obtendrá el id del actor y a raíz de la consulta devolverá todas las películas en las que dicho actor aparece.
Representando los seguidores en nuestra aplicación Flask
Pensando en como vamos a tomar la estructura relacional para representar nuestros seguidores, la que mejor se adapta es esta última que nombramos, la relación muchos a muchos es la que utilizan hoy en día las aplicaciones como instagram y twitter, así es como estos dos gigantes de las redes sociales hacen que sus usuarios puedan seguirse entre si y bueno dejarse de seguir también.
Para nuestro caso, el diagrama de nuestra relación quedaría de la siguiente manera:
Las claves foráneas en esta tabla apuntan a las entradas en la tabla de usuario, ya que vincula a los usuarios con los usuarios. Cada registro en esta tabla representa un enlace entre un usuario seguidor y un usuario seguido, en este caso para las consultas se aplica lo mismo que hablamos en el ejemplo de los actores y las películas ya que es la misma relación muchos a muchos.
Creando el modelo en nuestra base de datos
Vamos a dirigirnos a nuestro archivo modelos.py y vamos a crear la tabla seguidores, esta tabla debemos agregarla antes de los modelos de clase Usuario y la clase Pubs y lo haremos de la siguiente forma:
seguidores = bdd.Table('seguidores', bdd.Column('seguidor_id', bdd.Integer, bdd.ForeignKey('usuario.id')), bdd.Column('seguido_id', bdd.Integer, bdd.ForeignKey('usuario.id')) )
Una vez agregada la tabla seguidores debemos agregar la relación que va a tener esta tabla seguidores con la tabla usuarios, para ello vamos a al modelo de la clase Usuarios y agregamos dicha relación y nuestra clase Usuario ahora debería de lucir así:
class Usuario(UserMixin, bdd.Model): id = bdd.Column(bdd.Integer, primary_key=True) username = bdd.Column(bdd.String(64), index=True, unique=True) email = bdd.Column(bdd.String(120), index=True, unique=True) hash_clave = bdd.Column(bdd.String(128)) sobre_mi = bdd.Column(bdd.String(140)) ultima_sesion = bdd.Column(bdd.DateTime, default=datetime.utcnow) # Relación a la tabla Pubs pubs = bdd.relationship('Pubs', backref='autor', lazy='dynamic') # Relación a la tabla seguidores seguido = bdd.relationship( 'Usuario', secondary=seguidores, primaryjoin=(seguidores.c.seguidor_id == id), secondaryjoin=(seguidores.c.seguido_id == id), backref=bdd.backref('seguidores', lazy='dynamic'), lazy='dynamic' )
Esta relación que acabamos hacer no es igual a la relación que hicimos con la tabla usuarios y la tabla pubs. Las razones son obvias, porque para los usuarios y las publicaciones es una relación de uno a muchos y para los usuarios y los seguidores es una relación de muchos a muchos, teniendo en cuenta esto podemos continuar.
Vamos a explicar cada uno de los parámetros que tiene el método bdd.relationship()
'Usuario'
es la asociación del lado derecho de la relación (la asociación del lado izquierdo es la clase padre). Como se trata de una relación autorreferencial, tenemos que usar la misma clase en ambos lados.secondary
configura la tabla de asociación que se usa para esta relación, que definí justo encima de esta clase.primaryjoin
Indica la condición que vincula la asociación del lado izquierdo (el usuario seguidor) con la tabla auxiliar. La condición de unión para el lado izquierdo de la relación es el id de usuario que coincide con elseguidor_id
campo de la tabla auxiliar. La expresiónseguidores.c.seguidor_id
hace referencia alseguidor_id
columna de la tabla de auxiliar.secondaryjoin
indica la condición que vincula la asociación del lado derecho (el usuario seguido) con la tabla auxiliar. Esta condición es similar a la deprimaryjoin
, con la única diferencia que ahora estoy usandoseguido_id
, que es la otra clave externa en la tabla auxiliar.backref
define cómo se accederá a esta relación desde la asociación del lado derecho. Desde el lado izquierdo, se nombra la relaciónseguido
, por lo que desde el lado derecho voy a usar el nombreseguidores
para representar a todos los usuarios del lado izquierdo que están vinculados al usuario objetivo en el lado derecho. Ellazy
es un argumento adicional indica el modo de ejecución para esta consulta. Un mododynamic
configura la consulta para que no se ejecute hasta que se solicite específicamente, que también es cómo configuro la relación de uno a muchos, al momento de crear la relación para los mensajes entre usuarios.lazy
es similar al parámetro del mismo nombre en elbackref
, pero este se aplica a la consulta del lado izquierdo en lugar del lado derecho.
Ahora se debe proceder a realizar los cambios en la base de datos, para ello volver a dar uso a flask migrate:
(env) λ flask db migrate -m "seguidores" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'seguidores' Generating C:\Users\gamersnautas\Desktop\miBlog\migrations\versions\d1a43e7b6d25_seguidores.py ... done (env) λ flask db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade 2c60a75433ff -> d1a43e7b6d25, seguidores
Como siempre, recomiendo ir a verificar los cambios en la base de datos.
Comenzar a seguir y dejar de seguir
Para agregar estas opciones, solo falta hacer una especie de lógica con el campo seguido_id
como si fuera una especie de lista y esto es posible gracias al ORM de SQLAlchemy, entonces vamos a implementarlo en nuestra aplicación, nos ubicamos en nuestro archivo de modelos.py y agregamos a la clase Usuario lo siguiente:
class Usuario(UserMixin, bdd.Model): # ... def seguir(self, usuario): if not self.siguiendo(usuario): self.seguido.append(usuario) def dejar_seguir(self, usuario): if self.siguiendo(usuario): self.seguido.remove(usuario) def siguiendo(self, usuario): return self.seguido.filter(seguidores.c.seguido_id == usuario.id).count() > 0
Los métodos de la clase Usuario que hemos agregado seguir
y dejar de seguir
usan el métdo .append() a estas alturas de programación en python ya deberías de saber para que funciona, entonces lo que hace es realizar una consulta a la base de datos y agregarlo, pero antes de realizar esta acción primero hace uso de otro método el cual es siguiendo
este método se encarga de verificar si el usuario ya sigue a dicho usuario, hacemos esto por razones obvias la cual es que debe tener sentido nuestro código ya que no vamos a permitir que el usuario A siga a un usuario B que ya anteriormente seguía.
¿Pero exactamente como valida la función siguiendo
todo esto?, pues lo que hace esta función es emitir una consulta a nuestra base de datos a través de la relación seguido
y verifica si ya existe un enlace entre los dos usuarios, esto lo hace gracias al método .filter()
que en sus argumentos le estamos diciendo que primero verifique si el usuario ya sigue a ese otro usuario y luego con el método .count().
le decimos que devuelva ese resultado como un 1 si es cierto o como un 0 si es falso lo hacemos de esta manera para que la sentencia if que se encuentra dentro de los métodos seguir
y dejar de seguir
puedan hacer su trabajo, ya se que anteriormente expliqué todo esto con la relación de las tablas actores y películas, pero es que me gusta dejar las cosas bien claras. Otro de los métodos que usamos es la función .filter()
anteriormente usamos la función .filter_by()
la diferencia entre estas dos es que la primera me permite pasarle dentro de sus argumentos operadores de igualdad, mientras que la segunda solo permite buscar consultas más simples.
Si no los has entendido bastante bien acá te dejo este ejemplo:
bdd.users.filter_by(name='Jimena')
bdd.users.filter(db.users.name=='Joe')
En conclusión .filter()
podría decirse que nos permite hacer consultas más avanzadas, que es lo que necesitamos para este caso.
Agregando la función a nuestra app para obtener publicaciones de usuarios seguidos
Esto lo logramos haciendo una consulta a nuestra base de datos, la consulta es la más completa que realizaremos hasta ahora, vamos a ir a nuestro archivo modelos.py y agreguemos al modelo de la clase Usuario lo siguiente:
class Usuario(UserMixin, bdd.Model): #... def pubs_seguidores(self): return Pubs.query.join( seguidores, (seguidores.c.seguido_id == Pubs.id_usuario)).filter( seguidores.c.seguidor_id == self.id).order_by( Pubs.timestamp.desc())
A partir de ahora, como la consulta es muy completa vamos a analizar el código parte por parte.
Concepto de uniones
Digamos que la tabla auxiliar de seguidor_id
dice que el usuario Juan
está siguiendo a los usuarios Samuel
y Diego
, el usuario Samuel
está siguiendo al usuario Maribel
y el usuario Maribel
está siguiendo a Diego
. Los datos que representan todo esto a través de los enlaces son los siguientes:
seguidor_id | seguido_id |
1 | 2 |
1 | 4 4 |
2 | 3 |
3 | 4 4 |
Finalmente la tabla de las publicaciones se entendería de la siguiente manera:
id | cuerpo | id_usuario |
1 | Pubs de Samuel | 2 |
2 | Pubs de Maribel | 3 |
3 | Pubs de Diego | 4 |
4 | Pubs de Juan | 1 |
La columna cuerpo es el cuerpo del mensaje o la publicación que hace el usuario.
Luego estas tablas se unen, y se unen a gracias al método .join()
al que llamamos al momento de hacer la consulta, este método lo definimos en la linea 6 y es una consulta a nuestra base de datos que lo que hace es unir ambas tablas. La condición que utilicé dice que el campo seguido_id
de la tabla de seguidores debe ser igual al id_usuario la tabla de publicaciones. Para realizar esta fusión, la base de datos tomará cada registro de la tabla de publicaciones (el lado izquierdo de la unión) y agregará todos los registros de la tabla seguidores
(el lado derecho de la unión) que coincidan con la condición. Cuando se produce la unión la tabla resultante sería algo como esta:
id | cuerpo | id_usuario | seguidor_id | seguido_id |
1 | Pubs Samuel | 2 | 1 | 2 |
2 | Mensaje de Maribel | 3 | 2 | 3 |
3 | Mensaje de Diego | 4 4 | 1 | 4 4 |
3 | Mensaje de Juan | 4 4 | 3 | 4 4 |
Filtros
Al momento en que se unen las tablas, esto me arroja muchos datos de lo que en realidad quiero, es por eso que hice el llamado de la función .filter()
nuevamente este código se encuentra en la linea 7 y la linea 8, pero te lo dejo acá de igual forma:
filter(followers.c.follower_id == self.id)
Este fue el código que usamos para la consulta, dicha expresión selecciona los elementos en la tabla unida que tiene a la columna seguidor_id
establecida para este usuario, lo que en otras palabras significa que solo mantengo las entradas que tiene este usuario como seguidor.
Digamos que el usuario que me interesa es Juan
, que tiene su campo id
establecido en 1. Así es como se ve la tabla unida después del filtrado:
id | cuerpo | id_usuario | seguidor_id | seguido_id |
1 | Pubs de Samuel | 2 | 1 | 2 |
3 | mensaje de Diego | 4 4 | 1 | 4 4 |
Con esto logramos conseguir las publicaciones que queríamos exactamente, también es importante resaltar que las tablas que se generan a través de esta consulta son tablas temporales, no se guardan en nuestra base de datos.
Clasificación
Este sería nuestro último paso, nos queda solo ordenar los resultados, este código se encuentra al finales de la linea 8 que ya es donde termina nuestro código, el código usado es el siguiente:
order_by( Pubs.timestamp.desc())
Acá simplemente le estados diciendo que ordene los resultados por el tiempo en el que fueron publicados, por eso hacemos uso el campo timestamp que es parte de nuestra tabla Pubs para que nos devuelva los resultados en un origen cronológico por así decirlo.
Esta consulta sirve para nuestra aplicación, pero tiene un detalle; y es que las personas esperan ver sus propias publicaciones también, entonces con este código no estaría sucediendo eso. Para poder solucionarlo solamente requerimos hacer una ligera modificación dentro de la función antes de que retorne un resultado, entonces haciendo dichos cambios el código de nuestra función debería de verse así:
def pubs_seguidores(self): seguido = Pubs.query.join( seguidores, (seguidores.c.seguido_id == Pubs.id_usuario)).filter( seguidores.c.seguidor_id == self.id) pubs_propias = Pubs.query.filter_by(id_usuario=self.id) return seguido.union(pubs_propias).order_by(Pubs.timestamp.desc())
Debemos tener en cuenta que la consulta seguido
y pubs_propias
se combinan en una sola antes de retornar el resultado.
Integrando seguidores con al aplicación
Ya tenemos completa toda la lógica dentro de nuestra base de datos, ahora solo falta integrarla en nuestra app, en pocas palabras nos falta esa lógica en nuestro archivo rutas.py así que vamos allí y agregaremos lo siguiente:
#... @app.route('/seguir/<username>') @login_required def seguir(username): usuario = Usuario.query.filter_by(username=username).first() if usuario is None: flash('Usuario {} no encontrado.'.format(username)) return redirect(url_for('index')) if usuario == current_user: flash('¡No puedes realizar esta accion contigo mismo!') return redirect(url_for('perfil_usuario', username=username)) current_user.seguir(usuario) bdd.session.commit() flash('¡Ahora estas siguiendo a {}!'.format(username)) return redirect(url_for('perfil_usuario', username=username)) @app.route('/dejar_seguir/<username>') @login_required def dejar_seguir(username): usuario = Usuario.query.filter_by(username=username).first() if usuario is None: flash('Usuario {} no encontrado.'.format(username)) return redirect(url_for('index')) if usuario == current_user: flash('¡No puedes realizar esta accion contigo mismo!') return redirect(url_for('perfil_usuario', username=username)) current_user.dejar_seguir(usuario) bdd.session.commit() flash('Dejaste de seguir a {}.'.format(username)) return redirect(url_for('perfil_usuario', username=username))
Hemos agregado la vista que contiene la lógica cuando se siga a un usuario y también hemos agregado la vista que procesa la lógica en caso de que dejemos de seguir a alguien.
Ahora solo nos falta implementar un botón en nuestra plantilla usuario.html que nos permita seguir al usuario (si no se esta siguiendo) y dejar de seguir (si el usuario se está siguiendo), para lograr la plantilla usuarios.html debe verse así:
{% extends "base.html" %} {% block contenido %} <table> <tr valign="top"> <td><img src="{{ usuario.imagen_perfil(128) }}"></td> <!-- recordar que usuario es la consulta que hicimos con la clase Usuario a la base de datos y es por eso podemos usar su método image_perfil() --> <td> <h1>Usuario: {{ usuario.username }}</h1> {% if usuario.sobre_mi %}<p>{{ usuario.sobre_mi }}</p>{% endif %} {% if usuario.ultima_sesion %}<p>Ultima vez activo: {{ usuario.ultima_sesion }}</p>{% endif %} {% if usuario == current_user %} <p><a href="{{ url_for('editar_perfil') }}">Editar perfil</a></p> {% elif not current_user.siguiendo(usuario) %} <p><a href="{{ url_for('seguir', username=usuario.username) }}">Seguir</a></p> {% else %} <p><a href="{{ url_for('dejar_seguir', username=usuario.username) }}">Dejar de seguir</a></p> {% endif %} </td> </tr> </table> <hr> {% for post in posts %} {% include '$post.html' %} {% endfor %} {% endblock %}
Es hora de probarlo, crea dos usuarios y prueba esta función. Como no tenemos una forma de buscar un usuario dentro de nuestra app, lo que debes hacer es escribir la ruta del otro usuario que hayas registrado, por ejemplo si registraste a dos usuarios uno con el nombre jose19
y otro con el nombre juan05
inicia sesión con cualquiera de los dos y luego introduces manualmente la url del usuario, para el caso de iniciar sesión con el usuario juan05
debes dirigirte al perfil del usuario jose19
y seria introduciendo la ruta /usuario/jose19
y veras que ya podrás seguir y dejar de seguir a esta usuario porque la función de seguidores ya está integrada completamente.
➡ Continúa aprendiendo con nuestro Curso de Flask – Python: