記錄一次完整的flask小型應用開發(5)-- 高階多對多關係
使用者之間互相follow
接下來,我們實現,讓使用者可以關注其他使用者,並且在首頁只顯示所關注使用者釋出的部落格文章。
多對多關係建立的解決辦法是新增第三張表,這個表為關聯表,多對多關係可以分解為原表和關聯表之間的兩個一對多關係。多對多關係仍然使用定義一對多關係的db.relationship()方法來定義,但是在多對多關係中,必須把secondary引數定義為關聯表,並且,多對多關係可以在任意一個類中定義,backref引數會處理好關係的另一側!,關聯表就是一個簡單的表,不是模型。
那麼問題來了,學生和學生選課也是多對多關係,但是簡單的是這個例子中,關聯表連線的是學生和課程兩個明確的實體物件。
但是再想想我們的使用者互相關注的例子,只有使用者這一個實體。那麼這種關係就是自引用關係,即關係表的兩側都在同一個實體上面。
使用多對多關係的時候,往往需要儲存所聯絡的兩個實體之間的額外資訊,所以我們可以儲存使用者關注的時間,為了能夠在關係中處理自定義的資料,我們需要提升關聯表的地位,使其變成程式可訪問的模型。
class Follow(db.Model): __tablename__ = 'follows' follower_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True) followed_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True) timestamp = db.Column(db.DateTime, default=datetime.utcnow)
然後在user模型裡面定義關係:
followed = db.relationship('Follow', foreign_keys=[Follow.follower_id], backref=db.backref('follower', lazy='joined'), lazy='dynamic', cascade='all, delete-orphan') followers = db.relationship('Follow', foreign_keys=[Follow.followed_id], backref=db.backref('followed', lazy='joined'), lazy='dynamic', cascade='all, delete-orphan')
為了消除外來鍵之間的歧義,必須使用可選引數foreign_keys=來指定外來鍵
lazy引數: 這個lazy模式可以實現立即從聯結查詢中載入相關物件,比如某使用者關注了100位使用者,使用user.followed.all()會返回一個列表,包含了100個Follow例項,每一個例項的follower和followed回引屬性都指向相對應的使用者。當lazy設定為joined時,就一個一次性完成這些操作,如果是預設屬性select,那麼每一個屬性都需要單獨的查詢。lazy引數都在1的這一側,返回的結果是多的這一側中的記錄,使用dynamic的話,關係屬性不會直接返回記錄,而是返回查詢的物件。
cascade引數: 表示層疊選項,all表示除了delete-orphan之外的所有層疊選項,所以設為all,delete-orphan表示啟動所有的層疊選項,並且也刪除孤兒記錄。
接下來,我們在user模型中定義一些我們經常用到的方法:
# 從這裡開始是為了實現使用者關注功能的輔助方法
def is_following(self, user):
# 去查詢是不是關注了某一個使用者
return self.followed.filter_by(followed_id=user.id).first() is not None
def is_followed_by(self, user):
# 去查詢是不是被這個使用者關注了
return self.followers.filter_by(follower_id=user.id).first() is not None
def follow(self, user):
if not self.is_following(user):
# 這裡表示如果沒有關注那麼就關注他
f = Follow(follower=self, followed=user)
db.session.add(f)
db.session.commit()
def unfollow(self, user):
f = self.followed.filter_by(followed_id=user.id).first()
db.session.delete(f)
db.session.commit()
接下來我們要在資料頁實現如下功能:
- 未關注使用者,顯示follow按鈕
- 已關注使用者,顯示unfollow按鈕
- 顯示出關注者和被關注者的數量,以及列表
- 相應的使用者頁面顯示follows you標識
所以我們首先修改資料頁的模板:
{% if current_user.can(Permission.FOLLOW) and user != current_user %}
{% if not current_user.is_following(user) %}
<a href="{{ url_for('main.follow', username=user.username) }}" class="btn btn-primary">Follow</a>
{% else %}
<a href="{{ url_for('main.unfollow', username=user.username) }}" class="btn btn-default">UnFollow</a>
{% endbif %}
{% endif %}
<a href="{{ url_for('main.followers', username=user.username) }}">
Followers: <span class="badge">{{ user.followers.count() }}</span>
</a>
<a href="{{ url_for('main.following', username=user.username) }}">
Following: <span class="badge">{{ user.followed.count() }}</span>
</a>
{% if current_user.is_authenticated() and user != current_user and user.is_following(current_user) %}
| <span class="label label-default">He already followed you!</span>
{% endif %}
這裡面有四個路由需要我們去實現,下面我們開始實現路由:
# 使用者在其他使用者的頁面點選follow,呼叫follow/username路由
@main.route('/follow/<username>')
@login_required
def follow(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('invalid user')
return redirect(url_for('main.index'))
# 這裡其實不是很必要,因為如果已經關注了,頁面都不會顯示這個按鈕
if current_user.is_following(user):
flash('you already follow this user')
return redirect(url_for('main.user', username=username))
current_user.follow(user)
flash('ok, you followed this user now')
return redirect(url_for('main.user', username=username))
@main.route('/unfollow/<username>')
@login_required
def unfollow(username):
user = User.query.filter_by(username=username).first()
current_user.unfollow(user)
flash('ok, you have cancled to follow this user')
return redirect(url_for('main.user', username=username))
@main.route('/followers/<username>')
@login_required
def followers(username):
flash('not finished')
return redirect(url_for('main.user', username=username))
@main.route('/following/<username>')
@login_required
def following(username):
flash('not finished')
return redirect(url_for('main.user', username=username))