Flask Vue.js全栈开发|第7章:粉丝关注大神
Synopsis: 要实现粉丝机制,需要使用 SQLAlchemy ORM 的多对多关系,需要额外定义一个用于多对多关系的关联表,强烈建议不使用模型,而是采用一个实际的表。粉丝机制的多对多还比较特殊,它只有一张用户表,所以是自引用关系。前端使用 Vue.js 的 vue-router 嵌套路由来实现在用户个人主页,切换页签分别查看用户的已关注的用户列表、用户的粉丝列表、用户的文章列表、用户关注的大神们的文章列表
代码已上传到 https://github.com/wangy8961/flask-vuejs-madblog/tree/v0.7 ,欢迎star
1. 深入 SQLAlchemy 关系模式
1.1 一对多 (one-to-many) 关系
上一篇文章已经讲了,User 与 Post 就是一对多关系,详情请参考:http://www.madmalls.com/blog/post/post-curd-and-markdown/#12-one-to-many
1.2 多对一 (many-to-one) 关系
多对一关系也可使用一对多表示, 对调两个表即可,或者把 外键
和 db.relationship()
都放在 "多" 这一侧。举个例子,多个轮胎属于一辆汽车:
class Tire(db.Model): __tablename__ = 'tires' id = db.Column(db.Integer, primary_key=True) car_id = db.Column(db.Integer, db.ForeignKey('cars.id')) car = db.relationship('Car') class Car(db.Model): __tablename__ = 'cars' id = db.Column(db.Integer, primary_key=True)
1.3 一对一 (one-to-one) 关系
一对一关系指的是特定类的实例可能只与另一个类的一个实例相关联的关系,反之亦然。它也可以使用一对多关系表示,但调用 db.relationship()
时要把 uselist
设为 False
,把 "多" 变成 "一"
通常,一个人拥有一部手机,这部手机只属于这个人:
class Person(db.Model): __tablename__ = 'person' id = db.Column(db.Integer, primary_key=True) mobile_phone = db.relationship('MobilePhone', uselist=False, backref='person') class MobilePhone(db.Model): __tablename__ = 'mobile_phones' id = db.Column(db.Integer, primary_key=True) person_id = db.Column(db.Integer, ForeignKey('person.id'))
1.4 多对多 (many-to-many) 关系
最复杂的关系类型是多对多,需要定义一个用于关系的辅助表(称为 关联表
)。对于这个辅助表, 强烈建议 不使用模型
,而是采用一个 实际的表
:
例如,许多学生可以参加许多课程:
students_classes = db.Table('students_classes', db.Column('student_id', db.Integer, db.ForeignKey('students.id')), db.Column('class_id', db.Integer, db.ForeignKey('classes.id')) ) class Student(db.Model): __tablename__ = 'students' id = db.Column(db.Integer, primary_key=True) classes = db.relationship('Class', secondary=students_classes, backref=db.backref('students', lazy='dynamic')) class Class(db.Model): __tablename__ = 'classes' id = db.Column(db.Integer, primary_key=True)
2. 实现粉丝机制
通过上面的介绍,很容易确定维护粉丝关系的正确数据模型是 多对多
关系,因为用户可以关注多个其他用户,并且用户可以拥有多个粉丝
不过,在学生和课程的例子中,多对多关系关联了两个实体。 但在粉丝关系中,用户关注其他用户,只有一个用户实体。 那么,多对多关系的第二个实体是什么呢?该关系的第二个实体也是用户。 一个类的实例被关联到同一个类的其他实例的关系,被称为 自引用关系
使用 自引用多对多关系
来实现粉丝机制的表结构示意图如下:
followers
表是关系的 关联表
,此表中的 外键(follower_id 和 followed_id)
都指向 用户表(users)
中的数据行。 该表中的每个记录代表关注者和被关注者的一个关系,因为它将用户关联到用户
2.1 数据库模型
修改 back-end/app/models.py
:
followers = db.Table( 'followers', db.Column('follower_id', db.Integer, db.ForeignKey('users.id')), db.Column('followed_id', db.Integer, db.ForeignKey('users.id')), db.Column('timestamp', db.DateTime, default=datetime.utcnow) ) class User(PaginatedAPIMixin, db.Model): ... # followeds 是该用户关注了哪些用户列表 # followers 是该用户的粉丝列表 followeds = 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')
说明:
followeds
: 代表左侧用户实体(即关注者/粉丝)已关注的右侧用户列表(即被关注者/大神们)db.relationship('User')
:自引用多对多关系
中右侧的 User 实体secondary=followers
: 指明用于该关系的关联表
primaryjoin=(followers.c.follower_id == id)
: 指明通过关联表
关联到左侧实体(关注者)的条件,即关联表
中的follower_id
字段与这个关注者的用户 id 相等时(followers.c.follower_id
表达式引用了该关联表中的follower_id
列)。要查询用户已关注的用户列表,user.followeds
会使用这个条件!secondaryjoin=(followers.c.followed_id == id)
: 指明通过关联表
关联到右侧实体(被关注者)的条件,即关联表
中的followed_id
字段与这个关注者的用户 id 相等时。要查询用户的粉丝列表,user.followers
会使用这个条件!backref=db.backref('followers', lazy='dynamic')
: 反向引用的属性,即 User 实体中存在followers
属性,表示用户的粉丝集合lazy='dynamic'
: 表明查询的模式为动态模式,不会立即执行
由于修改了数据模型,需要执行一次数据库迁移:
(venv) D:\python-code\flask-vuejs-madblog\back-end>flask db migrate -m "add followers" (venv) D:\python-code\flask-vuejs-madblog\back-end>flask db upgrade
2.2 API: 关注与取消关注
修改 back-end/app/models.py
:
class User(PaginatedAPIMixin, db.Model): ... def is_following(self, user): '''判断当前用户是否已经关注了 user 这个用户对象,如果关注了,下面表达式左边是1,否则是0''' return self.followeds.filter( followers.c.followed_id == user.id).count() > 0 def follow(self, user): '''当前用户开始关注 user 这个用户对象''' if not self.is_following(user): self.followeds.append(user) def unfollow(self, user): '''当前用户取消关注 user 这个用户对象''' if self.is_following(user): self.followeds.remove(user)
修改 back-end/app/api/users.py
:
@bp.route('/follow/<int:id>', methods=['GET']) @token_auth.login_required def follow(id): '''开始关注一个用户''' user = User.query.get_or_404(id) if g.current_user == user: return bad_request('You cannot follow yourself.') if g.current_user.is_following(user): return bad_request('You have already followed that user.') g.current_user.follow(user) db.session.commit() return jsonify({ 'status': 'success', 'message': 'You are now following %d.' % id }) @bp.route('/unfollow/<int:id>', methods=['GET']) @token_auth.login_required def unfollow(id): '''取消关注一个用户''' user = User.query.get_or_404(id) if g.current_user == user: return bad_request('You cannot unfollow yourself.') if not g.current_user.is_following(user): return bad_request('You are not following this user.') g.current_user.unfollow(user) db.session.commit() return jsonify({ 'status': 'success', 'message': 'You are not following %d anymore.' % id })
2.3 API: 返回被关注者列表和粉丝列表
修改 back-end/app/models.py
:
class User(PaginatedAPIMixin, db.Model): ... @property def followed_posts(self): '''获取当前用户的关注者的所有文章列表''' followed = Post.query.join( followers, (followers.c.followed_id == Post.author_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()) return followed.order_by(Post.timestamp.desc())
修改 back-end/app/api/users.py
:
2 条评论
评论者的用户名
评论时间shishijia
2020-05-10T12:50:49Z请问,文章中表示粉丝机制的表结构图使用什么软件画的?
Madman shishijia Author
2020-05-10T14:05:52Zhttps://www.processon.com/