第十二章 關注者
社交類web程序允許用戶與其它用戶聯絡。程序把這些關系稱之為關注者,朋友,人脈,熟人或者粉絲……不管名稱怎么變化,這一功能實際一樣,就是保持追蹤用戶之間的定向連接并在數據庫查詢中使用這些連接。
在本章,你將學到如何實現Flasky的關注功能,用戶將能夠關注其他用戶并在首頁選擇性顯示自己關注的用戶的博客列表。
重新審視數據庫關系
我們曾在第五章討論過,數據庫使用關系在數據記錄之間建立連接。一對多關系就是最常見的一種關系類型——一條記錄與多條記錄相關聯。為了實現這種關系類型,“多”側的元素擁有一個外鍵指向"一”側的那個元素。在當前狀態下的例程包含了兩個一對多關系:一個是連接了角色(一)和用戶(多)列表,而另一個則是連接著用戶(一)和他所寫的文章(多)列表。
其他大多數關系可以認為是一對多關系的演變。多對一是一對多關系的反向視圖,一對一關系就是簡化版的一對多,多側簡化成了只有一個元素。唯一不能這么理解的關系就是多對多關系,它兩側都是一個元素列表。這種關系我們將在下節進行描述。
多對多關系
一對多,多對一和一對一都是有一邊是一個實體,所以相關記錄通過一個外鍵來指向"一"這側。但是,怎么實現兩邊都是多的關系呢
讓我們來考慮下經典的多對多的例子:一個學生數據表和他們上的課程數據表。很明顯,你不能在學生表中添加一個外鍵指向課程表,因為一個學生可能選了多門課程——一個外鍵是不夠的。同樣的,也不能在課程表中添加指向學生的外鍵,因為該課程不止一個學生會選。兩邊都需要一個外鍵列表。
解決辦法就是在數據庫里添加第三個表,我們稱之為關聯表。現在多對多關系可以被分解到兩個一對多的關系,從原來兩個原始表分解出第三個關聯表。圖12-1展示了學生和課程間的多對多關系的表現。
本例中的關聯表被稱為registrations(注冊表)。表中的每一行對應一個獨立的注冊信息:描述了某1學生選擇了某1課程。
對多對多關系的查詢需要執行兩個步驟。為了獲取某個學生選課的課程列表,你得先從學生和注冊的一對多關系開始(1學生多注冊),獲取該學生的注冊列表。然后,反轉課程和注冊之間的一對多關系,從多對一的方向(1注冊多課程)來獲取與注冊列表對應的課程列表。同樣的,要列出某課程中的所有學生,你先從課程開始獲取該課程所有的注冊列表,然后的得到與這些注冊相關聯的所有學生列表。
橫跨兩種關系來獲取查詢結果聽起來很困難,但對于一個像上面例子中的簡單關系來說,SQLAlchemy能完成大部分的工作。下列代碼就實現了圖12-1中的多對多關系(
由于排版的關系,下列代碼可能需要你進行重新處理縮進換行
):
registrations = db.Table('registrations',
db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
db.Column('class_id', db.Integer, db.ForeignKey('classes.id'))
)
class Student(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
classes = db.relationship('Class',
secondary=registrations,
backref=db.backref('students', lazy='dynamic'),
lazy='dynamic')
class Class(db.Model):
id = db.Column(db.Integer, primary_key = True)
name = db.Column(db.String)
使用db.relationship()結構定義的關系是用于一對多關系的,但本例是多對多關系,所以附加的secondary參數必須設置成關聯表。這個關系也可以定成在這兩個類中的任意一個,只要使用backref參數從另外一邊明確指出這種關系即可。關聯表被定義成一個簡單表,而不是一個模型,因為SQLAlchemy會內部管理這個表。
classes關系使用語義列表,它使以這種方式處理多對多關系非常簡單。給定一個學生 S 和一個課程 C ,把學生注冊到課程的代碼就是:
>>> s.classes.append(c)
>>> db.session.add(s)
查詢學生S選擇的課程列表和課程C下面所有的學生同樣簡單:
>>> s.classes.all()
>>> c.students.all()
課程模型中有效的學生關系是在db.backref()參數中定義的,注意在這個關系中,backref參數也被擴展為含有lazy='dynamic'屬性,所以兩邊都會返回一個能接受過濾器的查詢。
如果學生 S 決定不選課程 C 了,你可以更新數據庫:
>>> s.classes.remove(c)
自引用關系
多對多關系可以被用在規范用戶關注其他用戶上,但這里有個問題。在學生和課程的例子中,有兩個非常明顯的被定義的實體(學生和課程)通過關聯表被鏈接在一起。但是,在我們描述用戶關注另外一個用戶的時候,并沒有第二個實體——兩邊都是指向用戶實體自己。
兩邊都屬于同一個表的關系我們稱之為自引用關系。本例中,關系的左側實體是被稱為“關注者(粉絲)”的用戶,右側的也是用戶,但應該稱之為“被關注者(博主)”。從概念上來說,自引用關系和普通的關系并沒有什么不同,但理解起來確實不容易。圖12-2顯示了數據庫中的自引用關系圖,它描述了用戶關注其他用戶。
這個關系中的中間關聯表稱之為follows。這個表中的每一行記錄著一個用戶關注另外一個用戶。圖中左側的一對多關系中,通過follows表關聯到的多側的用戶是關注者。而圖右側的一對多關系中,通過follows表關聯到的多側非人用戶則是被關注者。
高級多對多關系
利用像上文例子中自引用多對多關系配置,數據庫能描述關注者,但也有一個限制。當使用多對多關系時,一個常見要求是存儲附加數據用來在兩個實體間提供鏈接。對于關注者的關系,保存一個用戶開始關注另外一個的時間是很有用的,因為可以據此生成按時間排序的關注者列表。唯一能存儲這一信息的地方就是關聯表,但在類似于前面顯示的學生和課程的實現中,這個關聯表是一個完全由SQLAlchemy管理的內部表(不是模型,無法使用自定義數據)。
為了能夠在關系中使用自定義數據,關聯表必須提升為一個明確的模型以便于程序能訪問到。例子12-1展示了新的關聯表,由Follow模型定義。
Example 12-1. app/models/user.py: The follows association table as a model
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)
SQLAlchemy不能透明地使用關聯表,因此就無法讓程序訪問其中的自定義字段。多對多關系必須從左邊和右邊分解成兩個基本的一對多關系,且須定義成標準關系。如例子12-2:
Example 12-2. app/models/user.py: A many-to-many relationship implemented as two one-to-many relationships
class User(UserMixin, db.Model):
# ...
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附加參數來明確指出每個關系的外鍵,避免含混不清是十分有必要的。這些關系中的db.backref()參數并不是指向彼此,而是都回溯引用到Follow模型。
回溯引用的lazy參數指定為joined。lazy模式會立即從join查詢中加載相關對象。例如,如果一個用戶關注了100個其他用戶,調用user.followed.all()將返回一個100個follow實例的列表,其中每個實例的關注者(follower)
和被關注(followed)的回溯引用
都指向各自對應的一個用戶。lazy='joined'模式允許使用一個查詢完成所有上述操作。如果lazy被設置為默認的值'select',關注者和被關注的用戶就將會延遲加載:當第一次訪問follower或followed的時候,才會使用單獨查詢加載用戶——這就意味著要獲得完整的這100個被關注者的名單需要額外100次數據庫查詢才能完成。
這兩個關系中User端的lazy參數有不同的要求。在一這一側設置lazy并將返回多側的數據,這里lazy就使用了dynamic模式,所以關系的屬性返回一個查詢對象而不是直接返回記錄,這樣一來我們可以在查詢執行之前添加可選過濾器。
cascade參數配置了父類的一個動作如何傳播給其子對象的。cascade選項的一個例子就是,規定當一個對象被添加到數據庫會話時,任何通過關系與之相關的對象也應該被自動添加到會話中。默認的cascade選項足以應對大部分情況,但但在多對多的關系中這一默認選項就不能很好的工作了。默認的cascade會在一個對象被刪除時,把所有指向它的對象的外鍵統統設為空值(null)。但對以一個關聯表來說,正確的操作應該是在一條數據被刪除后,也刪除所有指向它的關聯數據實體(而不是外鍵設空值),以此來刪除關聯。這正好是delete-orphan傳播選項可以做的。
指定給cascade的是一個逗號分隔的選項列表,這有點繞,但實際是名為all的選項包含了除了delete-orphan外所有的級聯選項。使用all+delete-orphan將啟用默認傳播選項,并刪除孤兒數據(失聯的無效數據,無法被引用也沒有引用別的)。
現在程序需要運行兩個一對多關系來實現多對多關系功能。由于有需要經常重復使用的功能,在User模型中為所有的操作可能創建一個輔助方法是個不錯的主意。控制這個關系的四個新方法如例子12-3所示:
Example 12-3. app/models/user.py: Followers helper methods
class User(db.Model):
# ...
def follow(self, user):
if not self.is_following(user):
f = Follow(follower=self, followed=user)
db.session.add(f)
def unfollow(self, user):
f = self.followed.filter_by(followed_id=user.id).first()
if f:
db.session.delete(f)
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
follow()方法手動在關聯表中插入Follow實例,來鏈接關注者和被關注者,為程序提供設置自定義字段的機會。這兩個鏈接起來的用戶是在Follow實例的構造函數中被手動加入,然后把該實例對象添加到數據庫會話。注意并不需要手動設置timestamp字段因為它的默認值被定義為當前日期和時間。unfollow()方法使用followed關系定位鏈接了被關注者和關注者的follow實例。要解除兩個用戶之間的鏈接,只需簡單的刪除該follow對象即可。is_followed()和is_followd_by()方法在左側和右側的一對多關系中查找指定用戶,并在找到后返回True。
這個功能的數據庫部分已經完成了,你可以嘗試為它編寫一個測試單元。
屬性頁上的關注者
如果查看某用戶屬性頁的瀏覽者不是該用戶的關注者的話,該屬性頁面需要顯示一個"Follow"按鈕,或者如果是關注者就應該顯示一個"Unfollow"按鈕。在合適的時候,這里顯示關注數量和被關注數量,顯示關注者列表和粉絲列表,或顯示一個“Follows you”的標志,這都是不錯的。屬性頁的變化請參見例子12-4,圖12-3顯示了其樣式。
Example 12-4. app/templates/user.html: Follower enhancements to the user profile header
{% if current_user.can(Permission.FOLLOW) and user != current_user %}
{% if not current_user.is_following(user) %}
<a href="{{ url_for('.follow', username=user.username) }}"
class="btn btn-primary">Follow</a>
{% else %}
<a href="{{ url_for('.unfollow', username=user.username) }}"
class="btn btn-default">Unfollow</a>
{% endif %}
{% endif %}
<a href="{{ url_for('.followers', username=user.username) }}">
Followers: <span class="badge">{{ user.followers.count() }}</span>
</a>
<a href="{{ url_for('.followed_by', 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">Follows you</span>
{% endif %}
圖12-3
在模板的這些變化中,定義了四個新的端點(endpoint)。
/follow/<username>
路由將在一個用戶在別的用戶屬性頁上點擊"follow"按鈕時被調用。其實現如例子12-5所示:
Example 12-5. app/main/views.py: Follow route and view function
@main.route('/follow/<username>')
@login_required
@permission_required(Permission.FOLLOW)#注:注意導入引用
def follow(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('Invalid user.')
return redirect(url_for('.index'))
if current_user.is_following(user):
flash('You are already following this user.')
return redirect(url_for('.user', username=username))
current_user.follow(user)
flash('You are now following %s.' % username)
return redirect(url_for('.user', username=username))
這個視圖函數加載了要關注的用戶,確認其合法并尚未被當前的登錄用戶關注,然后調用User模型中的follow()輔助函數來實現鏈接。/unfollow/<username>
路由也是以類似的方式實現。(下面是自寫代碼,你也可以參考作者的github源代碼)
@main.route('/unfollow/<username>')#注:自寫,原無。
@login_required
@permission_required(Permission.FOLLOW)
def unfollow(username):
user=User.query.filter_by(username=username).first()
if user is None:
flash(u'無效的用戶!')
return redirect(url_for('.index'))
if current_user.is_following(user):
current_user.unfollow(user)
flash(u'你已取消對 %s 的關注' % username)
return redirect(url_for('.user',username=username))
當瀏覽者點擊關注者數量(followers)的時候,就會調用/followers/<username>
路由函數,這一實現如例子12-6所示(這里還需要在config.py中設置一下FLASKY_FOLLOWERS_PER_PAGE分頁變量
):
Example 12-6. app/main/views.py: Followers route and view function
@main.route('/followers/<username>')
@login_required
def followers(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('Invalid user.')
return redirect(url_for('.index'))
page = request.args.get('page', 1, type=int)
pagination = user.followers.paginate(page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],\
error_out=False)
follows = [{'user': item.follower, 'timestamp': item.timestamp}
for item in pagination.items]
return render_template('followers.html', user=user, title="Followers of",\
endpoint='.followers', pagination=pagination,follows=follows)
這個函數加載并驗證被請求的用戶,然后使用11章的分頁技術對該用戶的關注者進行分頁處理。因為對關注者的查詢將返回一個Follow實例列表,我們把它轉換成為便于顯示的follows列表,其中每個實體只擁有user和timestamp兩個字段。
用來顯示關注了xx列表的模板可以通用些,這樣它也可以被用來顯示被關注的oo列表。模板接收用戶、頁面標題、分頁鏈接使用的斷點(路由名稱)、分頁對象和結果列表。
foolowed_by斷點幾乎完全一樣。唯一區別就是用戶列表是從user.followed關系取得的。模板參數也需要相應進行調整。
followers.html模板包含兩列,分別在左側顯示用戶名稱和他們的頭像,在右側顯示flask-moment時間戳。你可以從github倉庫獲取源代碼查看詳細實現。
注下面是我自己的實現代碼:
#followed-by路由
@main.route('/followed-by/<username>')
@login_required
def followed_by(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('Invalid user.')
return redirect(url_for('.index'))
page = request.args.get('page', 1, type=int)
pagination = user.followed.paginate(page,\
per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],\
error_out=False)
follows = [{'user': item.followed, 'timestamp': item.timestamp}
for item in pagination.items]
return render_template('followers.html', user=user, title="Followed by ",\
endpoint='.followed_by', pagination=pagination,follows=follows)
followers.html模板參考如下注意:由于我訪問不了garava網站,所以把所有用戶頭像都硬編碼為一個固定值。你可以參考前面的代碼,調用user模型中的頭像生成函數來使用自定義頭像。
:
{% extends "base.html" %}
{% import "_macros.html" as macros %}
{% block title %}Flasky - {{ title }} {{ user.username }}{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>{{ title }} {{ user.username }}</h1>
</div>
<table class="table table-hover followers">
<thead><tr><th>User</th><th>Since</th></tr></thead>
{% for follow in follows %}
{% if follow.user != user %}
<tr>
<td>
<a href="{{ url_for('.user', username = follow.user.username) }}">
<img class="img-rounded profile-thumbnail" src="{{ url_for('static',filename='headimg.png') }}">
<div style="margin-left:40px;">{{ follow.user.username }}</div>
</a>
</td>
<td>{{ moment(follow.timestamp).format('L') }}</td>
</tr>
{% endif %}
{% endfor %}
</table>
<div class="pagination">
{{ macros.pagination_widget(pagination, endpoint, username = user.username) }}
</div>
{% endblock %}
使用數據庫join查詢關注的文章
目前,程序的首頁倒序(從最近到最久遠)顯示了數據庫里所有的文章。完成以下代碼功能后,就可以允許用戶選擇只顯示自己關注的那些用戶的文章。
加載關注用戶的所有文章的直觀思路是:首先獲取那些被關注的用戶列表,然后逐個獲取其文章并存入一個獨立列表中。當然這個方法擴展性并不好,獲取聯合列表的耗時會隨著數據量的增長同步變大,諸如分頁之類的操作就會變得沒有效率。提升性能的方法就是使用一個查詢完成獲取這個文章列表。
用來完成這個的數據庫操作被稱為“join”。一個join操作將根據指定條件在兩個或更多表中查找數據,生成一個聯合數據集,并將數據插入一個臨時表。(譯注:這個數據集包含指定的各表字段,而非單一表中的字段。你可以參看sql語法
)。你可以通過一個例子來更好的理解join的工作方式。
表12-1是一個帶有三個用戶的users表
Table 12-1. users table
id username
1 john
2 susan
3 david
表12-2 是帶有一些文章的posts表
Table 12-2. Posts table
id author_id body
1 2 Blog post by susan
2 1 Blog post by john
3 3 Blog post by david
4 1 Second blog post by john
最后表12-3是誰關注誰。這個表你可以看到john關注了david,susan關注john,david沒有關注任何人。
Table 12-3. Follows table
follower_id followed_id
1 3
2 1
2 3
要獲得susan關注的文章列表,必須合并posts和follows兩個表。首先過濾follows表只保留關注者susan的記錄,在表中就是最后兩行。然后,凡是posts表中author_id等于followed_id的數據被提取出來插入新創建的臨時join表中,這樣操作選擇了susan關注的用戶的文章就更有效率些。表12-4顯示了join操作的結果,用來進行join操作的列被標注了星號*。
Table12-4 合并后的表
id author_id* body follower_id followed_id*
2 1 Blog post by john 2 1
3 3 Blog post by david 2 3
4 1 Second blog post by john 2 1
這個表僅體現了susan觀注的用戶所撰寫的文章。Flask-SQLAlchemy要像表述那樣執行查詢操作是相當復雜的:
return db.session.query(Post).select_from(Follow).\
filter_by(follower_id=self.id).\
join(Post, Follow.followed_id == Post.author_id)
以前我們所見的查詢都是從模型的query屬性來執行,但在這里這個模式就不適合了,因為此處查詢需要返回posts數據,但我們要完成的第一個操作是對follows表進行過濾。所以我們使用了更基礎的模式來執行這個查詢。為了更好地理解,我們把它分成幾個獨立的部分來解釋下。
+ db.session.query(Post),指明這是一個將返回Post對象的查詢
+ select_from(Follow),從Follow模型開始一個查詢
+ filter_by(follower_id=self.id),使用follower用戶來過濾follows表(譯注:去除非susan的數據)
+ join(Post,follow,followed_id==Post.author_id),把filter_by()后的結果與Post對象進行join操作。
可以通過交換過濾器和join的順序把這個查詢簡化為:
return Post.query.join(Follow, Follow.followed_id == Post.author_id).filter \
(Follow.follower_id == self.id)
首先執行join操作,我們再從Post.query開始查詢——這樣就只需要添加兩個過濾器:join()和filter()。但這是一樣的嗎?看起來先執行join然后進行過濾操作可能會導致更多的操作。但實際上這兩個查詢是等價的。SQLAlchemy首先收集所有的過濾器然后用最有效率的方式生成查詢。這兩個查詢的原生SQL結構是一致的。添加到Post模型的最終版本查詢如例子12-7所示:
Example 12-7. app/models.py: Obtain followed posts
class User(UserMixin,db.Model):
# ...
@property
def followed_posts(self):
return Post.query.join(Follow, Follow.followed_id == Post.author_id)\
.filter(Follow.follower_id == self.id)
注意:followed_post()方法被定義成了一個屬性,所以調用時不需要加上()
。這樣一來所有關系的語法就統一起來了。
Join的相關知識可能難以理解,我想你需要在shell中多練習例子的代碼來幫助你加深理解。
在首頁顯示關注用戶的文章
首頁現在允許用戶選擇顯示所有用戶的文章還是僅顯示自己關注用戶的文章。例子12-8展示了它是如何實現的:
Example 12-8. app/main/views.py: Show all or followed posts
@app.route('/', methods = ['GET', 'POST'])
def index():
# ...
show_followed = False
if current_user.is_authenticated():
show_followed = bool(request.cookies.get('show_followed', ''))
if show_followed:
query = current_user.followed_posts
else:
query = Post.query
pagination = query.order_by(Post.timestamp.desc()).paginate(page,\
per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],\
error_out=False)
posts = pagination.items
return render_template('index.html',form=form,posts=posts,\
show_followed=show_followed, pagination=pagination)
在cookie中存儲了一個名為show_followed的字符串,如果非空,則只顯示關注的文章。Cookie以request.cookies的字典格式被存儲在請求對象中。這個cookie字符串可以被轉換成Boolean(布爾)類型,基于其值,程序將設置一個本地的query變量來查詢以獲取全部或篩選后的文章清單。要顯示所有的文章,就使用最頂層的查詢Post.query。當列表被限制到關注者的時候,就會調用最后添加的User.followed_posts屬性。存儲在query本地變量中的這個查詢隨后進行分頁,像以前那樣其結果被發送給模板。
在兩個新的路由中設置了show_followed的cookie,如例子12-9所示:
Example 12-9. app/main/views.py: Selection of all or followed posts
@main.route('/all')
@login_required
def show_all():
resp = make_response(redirect(url_for('.index')))
resp.set_cookie('show_followed', '', max_age=30*24*60*60)
return resp
@main.route('/followed')
@login_required
def show_followed():
resp = make_response(redirect(url_for('.index')))
resp.set_cookie('show_followed', '1', max_age=30*24*60*60)
return resp
在首頁模板中添加上這兩個路由的鏈接。當點擊時,將為show_followed cookie設置一個值,并重新定向回到首頁。
Cookies只能在response對象中設置,所以這兩個路由必須手動調用make_response來創建response對象,而不是讓Flask來做。
set_cookie()函數把cookie的名稱和值作為開頭的兩個參數。max_age選項參數則指定了cookie超時過期的秒數。如果沒有設置該參數,則cookie將在瀏覽器窗口關閉之后立即過期失效。在本例中,我們設置了過期時間是30天,這樣瀏覽器將“記住”該設置,即使用戶幾天都沒有登錄過系統。
修改模板中文章列表部分,在其頂部添加兩個導航標簽,用以調用/all
或者/followed
路由來設置session值。你可以從Github庫中找到詳細的代碼(譯注:此處未列出
)。圖12-4就是更改后的首頁。
譯注:我的模板修改代碼:
<ul class="nav nav-tabs" role="tablist">
<li role="presentation"><a href="{{url_for('main.show_all')}}">All Posts</a></li>
<li role="presentation"><a href="{{url_for('main.show_followed')}}">Followed Posts</a></li>
</ul>
如果你嘗試點擊切換到“關注的文章”,你會注意到你自己的文章并沒有出現這列表中。這當然是對的,因為不能關注自己嘛!
實際上即使是查詢代碼確實是按照我們的設計思路正常運行,大多數人還是希望能同時看到自己的文章。實際上這并不困難,只要在創建注冊用戶的時候,直接把他們標注為自己的關注者就可以了。這個小改動如例子12-10所示:
Example 12-10. app/models.py: Make users their own followers on construction
class User(UserMixin, db.Model):
# ...
def __init__(self, **kwargs):
# ...
self.follow(self)
不幸的是,你的數據庫里可能有寫用戶早已存在而當時并沒有關注自己。如果數據庫規模不大并且易于重建的話,最好是刪除并重建;但如果不行,那么正確的方法是添加一個函數來修復已存在的用戶比較好。請看例子12-11:
Example 12-11. app/models.py: Make users their own followers
class User(UserMixin, db.Model):
# ...
@staticmethod
def add_self_follows():
for user in User.query.all():
if not user.is_following(user):
user.follow(user)
db.session.add(user)
db.session.commit()
# ...
現在你就可以從shell中運行這一代碼進行數據庫升級了:
(venv) $ python manage.py shell
>>> User.add_self_follows()
創建一個函數來更新數據庫是一個常見的技術,通過腳本升級比手動修改數據庫更安全,所以這一技術在升級已經發布的程序是很常見。在第17章中你將看到這一函數和其他類似的被合并到一個發布腳本中。
這樣一來程序可用性更好了,但也同時導致了不良“并發癥”。在用戶屬性頁中顯示的關注者和被關注者計數都增長了“1”——直接的辦法就是在模板顯示的時候直接把計數減一即可:
修改user.html:
{{ user.followers.count() - 1 }}
和{{ user.followed.count() -1 }}
。
同樣的,關注和被關注者列表中也必須進行調整,清除掉“自己”——在模板中使用條件判斷即可完成。最后,檢查關注者計數的單元測試代碼也必須進行修改,以去除對自關注的統計。(譯注:如果你自己無法完成,可以去github參考作者提供的代碼)
ps: 這一段原文有幾處小瑕疵,作者在12-7后的幾個例子中修改user模型的文件位置標注錯誤,標成了
app/models/user.py
,12-7的User(dm.Model)缺少了UserMixin。實際代碼是沒有問題的。
在下一章,我們將實現用戶評論子系統——社交程序中的另一個重要功能。