【Flask/跟着学习】Flask大型教程项目#07:关注者

本文深入探讨了在Flask应用中实现多对多数据库关系的方法,特别是关注者与被关注者之间的关系。介绍了如何使用辅助表在用户间建立联系,以及如何在模型中声明这些关系。此外,还讲解了关注和取消关注的功能实现,包括数据库查询优化和单元测试。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

跟着学习(新版):https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vii-error-handling
回顾上一章:https://blog.youkuaiyun.com/weixin_41263513/article/details/85036999

本章内容

  • 多对多关系
  • 关注者与被关注者
  • 数据库模型
  • 关注和取消关注
  • 获取帖子
  • 单元测试
  • 让上述功能(关注,取消关注)可视化

多对多关系

我们之前说过,数据库使用关系建立记录之间的练习,其中,一对多关系是最常用的,它把一个记录和一组相关的记录联系在了一起,实现这个关系的时候,要在“多”的一侧加入一个外键,指向“一”这一侧连接的记录。一对多还是非常好理解的,接下来我们将实现多对多的关系。

多对多关系有点复杂。 例如,考虑一个拥有学生和教师的数据库。 我可以说学生有很多老师,老师有很多学生。 这就像是来自两端的两个重叠的一对多关系。对于这种类型的关系,我应该能够查询数据库并获得教授给定学生的教师列表,以及教师班级中的学生列表。 这在关系数据库中表示实际上并不重要,因为无法通过向现有表添加外键来完成。

多对多关系的表示需要使用称为关联表的辅助表。如下图
在这里插入图片描述

关注者与被关注者

根据上面的内容,很容能知道关注者与被关注者是多对多关系,因为用户可以关注很多用户,同时,用户也可以有许多关注者,例如:我是B站的一个up主,我有不多的粉丝,但同时,我也关注着其他优秀的up主。

所以,在我们的这个项目中,数据库的图可以如下画
followers是辅助表
在这里插入图片描述

数据库模型

现在可以开始了,先完成辅助表,非常简单:
文件:/app/models.py

followers = db.Table('followers',
    db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),
    db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))
)

注意,这里并没有把followers声明为一个类(class),因为这是一个不需要外部输入的一张辅助表
接下来,需要在users表中声明多对多的关系:
文件:/app/models.py

class User(UserMixin, db.Model):
    # ...
    followed = db.relationship(
        'User', secondary=followers,
        primaryjoin=(followers.c.follower_id == id),
        secondaryjoin=(followers.c.followed_id == id),
        backref=db.backref('followers', lazy='dynamic'), lazy='dynamic')

让我们先想象一下,通过辅助表,已经生成了两个User表,左边和右边
然后让我们逐个看看db.relationship()调用的所有参数:
“User”:生成两个User表嘛,非常容易理解
”secondary“:用来指明辅助表
”primaryjoin“:表示将左边用户(发起关注的用户)与辅助表连接的条件,followers.c.follower_id表达式引用辅助表的follower_id列
”secondaryjoin“:表示右边用户(被关注的用户)与辅助表连接的条件
”backref“:定义了如何从右边用户访问这个关系,当我们做出一个名为 followed 的查询的时候,将会返回所有跟左边用户联系的右边的用户。当我们做出一个名为 followers 的查询的时候,将会返回一个所有跟右边联系的左边的用户。简单来说就是打开粉丝列表(followed),可以看到里面有多少粉丝(关注自己的人),打开关注列表(follow),可以看到里面有多少我关注的人。
”lazy“:指示此查询的执行模式,dynamic模式表示直到有特定的请求才会运行查询,这是对性能有很好的考虑。
不理解没关系,后面会伴随着例子详细说明

关注和取消关注

以下是User模型中添加和删除关系的更改:
文件:/app/models.py

class User(UserMixin, db.Model):
    #...

    def follow(self, user):
        if not self.is_following(user):
            self.followed.append(user)

    def unfollow(self, user):
        if self.is_following(user):
            self.followed.remove(user)

    def is_following(self, user):
        return self.followed.filter(
            followers.c.followed_id == user.id).count() > 0

可以明显看到,在follow和unfollow方法执行之前,我们都需要is_following来处理这两个简单功能的逻辑关系
is_following方法用来检查两个用户之间的关系是否存在,之前我们使用SQLAlchemy查询对象的filter_by()方法,例如查找给定用户名的用户。我在这里使用的filter()方法类似,但是更低级别,因为它可以包括任意过滤条件,不像filter_by(),它只能检查与常量值的相等性。查询以count()方法终止,该方法返回结果数。此查询的结果将为0或1,因此检查计数为1或大于0实际上是等效的。

获取帖子

正常来说,当我关注了一个人的时候,这个人的动态以及发过的贴子都应该出现在我的index页面中,因此需要一个返回贴子的数据库查询,所以:
文件:/app/models.py

class User(db.Model):
    #...
    def followed_posts(self):
        return Post.query.join(
            followers, (followers.c.followed_id == Post.user_id)).filter(
                followers.c.follower_id == self.id).order_by(
                    Post.timestamp.desc())

join主要是一个合并表的功能,上面看似复杂,但仔细看也不难琢磨出来

接下来要展现出来的不只是你关注的人写的贴子,你自己也会写帖子的嘛,所以需要把你和你关注的人的贴子整合在一起,有一个很简单的方法,就是自己关注自己,自己成为自己的关注者,不过一般来说,很多网站都是设置自己不能关注自己,因为这回影响关于followers的统计数据,反正就是不好啦!

所以可以使用“union”运算符将两个查询合并为一个查询,更新一下文件:
文件:/app/models.py

    def followed_posts(self):
        followed = Post.query.join(
            followers, (followers.c.followed_id == Post.user_id)).filter(
                followers.c.follower_id == self.id)
        own = Post.query.filter_by(user_id=self.id)
        return followed.union(own).order_by(Post.timestamp.desc())

单元测试

写了这么久,怎么知道我们写的都是对的呢?

Python包含一个非常有用的unittest包,可以轻松编写和执行单元测试。 让我们在tests.py模块中为User类中的现有方法编写一些单元测试:
文件:/tests.py

from datetime import datetime, timedelta
import unittest
from app import app, db
from app.models import User, Post

class UserModelCase(unittest.TestCase):
    def setUp(self):
        app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

    def test_password_hashing(self):
        u = User(username='susan')
        u.set_password('cat')
        self.assertFalse(u.check_password('dog'))
        self.assertTrue(u.check_password('cat'))

    def test_avatar(self):
        u = User(username='john', email='john@example.com')
        self.assertEqual(u.avatar(128), ('https://www.gravatar.com/avatar/'
                                         'd4c74594d841139328695756648b6bd6'
                                         '?d=identicon&s=128'))

    def test_follow(self):
        u1 = User(username='john', email='john@example.com')
        u2 = User(username='susan', email='susan@example.com')
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        self.assertEqual(u1.followed.all(), [])
        self.assertEqual(u1.followers.all(), [])

        u1.follow(u2)
        db.session.commit()
        self.assertTrue(u1.is_following(u2))
        self.assertEqual(u1.followed.count(), 1)
        self.assertEqual(u1.followed.first().username, 'susan')
        self.assertEqual(u2.followers.count(), 1)
        self.assertEqual(u2.followers.first().username, 'john')

        u1.unfollow(u2)
        db.session.commit()
        self.assertFalse(u1.is_following(u2))
        self.assertEqual(u1.followed.count(), 0)
        self.assertEqual(u2.followers.count(), 0)

    def test_follow_posts(self):
        # create four users
        u1 = User(username='john', email='john@example.com')
        u2 = User(username='susan', email='susan@example.com')
        u3 = User(username='mary', email='mary@example.com')
        u4 = User(username='david', email='david@example.com')
        db.session.add_all([u1, u2, u3, u4])

        # create four posts
        now = datetime.utcnow()
        p1 = Post(body="post from john", author=u1,
                  timestamp=now + timedelta(seconds=1))
        p2 = Post(body="post from susan", author=u2,
                  timestamp=now + timedelta(seconds=4))
        p3 = Post(body="post from mary", author=u3,
                  timestamp=now + timedelta(seconds=3))
        p4 = Post(body="post from david", author=u4,
                  timestamp=now + timedelta(seconds=2))
        db.session.add_all([p1, p2, p3, p4])
        db.session.commit()

        # setup the followers
        u1.follow(u2)  # john follows susan
        u1.follow(u4)  # john follows david
        u2.follow(u3)  # susan follows mary
        u3.follow(u4)  # mary follows david
        db.session.commit()

        # check the followed posts of each user
        f1 = u1.followed_posts().all()
        f2 = u2.followed_posts().all()
        f3 = u3.followed_posts().all()
        f4 = u4.followed_posts().all()
        self.assertEqual(f1, [p2, p4, p1])
        self.assertEqual(f2, [p2, p3])
        self.assertEqual(f3, [p3, p4])
        self.assertEqual(f4, [p4])

if __name__ == '__main__':
    unittest.main(verbosity=2)

setUp()和tearDown()方法是单元测试框架分别在每个测试之前和之后执行的特殊方法。我在setUp()中实现了一点hack,以防止单元测试使用我用于开发的常规数据库。通过将应用程序配置更改为sqlite://我在测试期间让SQLAlchemy使用内存中的SQLite数据库。 db.create_all()调用将创建所有数据库表。这是从头开始创建数据库的快速方法,可用于测试

尝试运行一下嘛~
在这里插入图片描述

从现在开始,每次对应用程序进行更改时,您都可以重新运行测试以确保正在测试的功能未受到影响。此外,每次向应用程序添加另一个功能时,都应为其编写单元测试

让上述功能(关注,取消关注)可视化

让我们在应用程序中添加两个新路由以关注和取消关注用户:
文件:/app/routes.py

@app.route('/follow/<username>')
@login_required
def follow(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash('User {} not found.'.format(username))
        return redirect(url_for('index'))
    if user == current_user:
        flash('You cannot follow yourself!')
        return redirect(url_for('user', username=username))
    current_user.follow(user)
    db.session.commit()
    flash('You are following {}!'.format(username))
    return redirect(url_for('user', username=username))

@app.route('/unfollow/<username>')
@login_required
def unfollow(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash('User {} not found.'.format(username))
        return redirect(url_for('index'))
    if user == current_user:
        flash('You cannot unfollow yourself!')
        return redirect(url_for('user', username=username))
    current_user.unfollow(user)
    db.session.commit()
    flash('You are not following {}.'.format(username))
    return redirect(url_for('user', username=username))

一定一定一定不要忘了 db.session.commit()
有了视图函数的功能,现在要创建相应的页面了:
文件:/app/templates/user.html

   ...
        <h1>User: {{ user.username }}</h1>
        {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
        {% if user.last_seen %}<p>Last seen on: {{ user.last_seen }}</p>{% endif %}
        <p>{{ user.followers.count() }} followers, {{ user.followed.count() }} following.</p>
        {% if user == current_user %}
        <p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
        {% elif not current_user.is_following(user) %}
        <p><a href="{{ url_for('follow', username=user.username) }}">Follow</a></p>
        {% else %}
        <p><a href="{{ url_for('unfollow', username=user.username) }}">Unfollow</a></p>
        {% endif %}
        ...

可以很容易理解的逻辑是:如果用户正在查看当前未关注的用户,则会显示“关注”链接。
如果用户正在查看当前关注的用户,则会显示“取消关注”链接。

效果图效果图
在这里插入图片描述
在这里插入图片描述

注意,我将进入用户资料界面是直接通过http://127.0.0.1:5000/user/123进去的,本来应该在index.html中显示所关注用户的贴子列表,但现在编写贴子的功能还未实现,所以等到下一章再来把!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值