跟着學習(新版):https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vii-error-handling
回顧上一章:https://blog.csdn.net/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='[email protected]')
self.assertEqual(u.avatar(128), ('https://www.gravatar.com/avatar/'
'd4c74594d841139328695756648b6bd6'
'?d=identicon&s=128'))
def test_follow(self):
u1 = User(username='john', email='[email protected]')
u2 = User(username='susan', email='[email protected]')
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='[email protected]')
u2 = User(username='susan', email='[email protected]')
u3 = User(username='mary', email='[email protected]')
u4 = User(username='david', email='[email protected]')
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中顯示所關注用戶的貼子列表,但現在編寫貼子的功能還未實現,所以等到下一章再來把!