FlaskBB閱讀筆記(三)
開篇
FlaskBB是用Flask框架實現的一個輕量級的論壇社群軟體,程式碼託管在GitHub上。本系列文章通過閱讀FlaskBB的原始碼來深入學習Flask框架,以及在一個產品級的Flask應用裡的一些最佳實踐規則。
本文是本系列文章的第三篇,將介紹ORM基礎知識,分析Flask-SQLAlchemy及sqlalchemy ORM引擎的一些常用方法,進而介紹FlaskBB使用者管理模組的資料庫設計。
什麼是 ORM
物件關係對映(英語:Object Relational Mapping,簡稱ORM,或O/RM,或O/R mapping),是一種程式技術,用於實現面向物件程式語言裡不同型別系統的資料之間的轉換。從效果上說,它其實是建立了一個可在程式語言裡使用的“虛擬物件資料庫”。-百度百科
簡單地說,使用 ORM 來操作資料庫,我們基本上不用跟 SQL 打交道了。直接用程式語言的物件來打交道即可。Flask-SQLAlchemy 是 ORM 引擎 sqlalchemy 針對 Flask 的擴充套件。
定義表
定義一個表只需要繼承自 db.Model
即可。
1 2 3 4 5 6 7 8 |
class User(db.Model, UserMixin): __tablename__ = "users" __searchable__ = ['username', 'email'] id = db.Column(db.Integer, primary_key= |
這樣我們就定義了一個叫 users
的表格,表格的名稱由 __tablename__
指定。這樣任何對錶格的操作,都可以轉化為對 User
類的操作。程式碼裡的 db
物件是什麼呢?在
extensions.py 裡建立了 db 物件 db = SQLAlchemy()
db.init_app(app)
。
定義一對多關係
一個論壇使用者會對應多個論壇主題。論壇主題由類 Topic
表達。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Topic(db.Model): __tablename__ = "topics" __searchable__ = ['title', 'username'] id = db.Column(db.Integer, primary_key=True) forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True, name="fk_topic_forum_id"), nullable=False) title = db.Column(db.String(255), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("users.id")) username = db.Column(db.String(200), nullable=False) date_created = db.Column(db.DateTime, default=datetime.utcnow()) last_updated = db.Column(db.DateTime, default=datetime.utcnow()) locked = db.Column(db.Boolean, default=False) important = db.Column(db.Boolean, default=False) views = db.Column(db.Integer, default=0) post_count = db.Column(db.Integer, default=0) |
User 類通過 db.relationship
來定義表 User 和 Topic 的一對多關係。
1 2 3 4 5 6 7 8 9 10 |
class User(db.Model, UserMixin): __tablename__ = "users" __searchable__ = ['username', 'email'] id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(200), unique=True, nullable=False) email = db.Column(db.String(200), unique=True, nullable=False) _password = db.Column('password', db.String(120), nullable=False) ... topics = db.relationship("Topic", backref="user", lazy="dynamic") |
關鍵程式碼在 LINE 10 。這一行程式碼會在 users
表裡建立一個列叫 topics
,這個列就儲存了這個使用者發起的所有論壇主題。然後在 topics
表裡建立一個列叫 user
,這是通過 backref
這個引數實現的,所以我們可以通過 Topic.user
來引用論壇主題的發起使用者。最後一個引數 lazy
可以有四個值:
select
這是預設值,表示 SQLAlchemy 會在必要的時候一次性把所有的資料從資料庫裡通過 SQL SELECT 語句讀取出來。當一對多的資料量比較小時可以用這個值,當資料量比較大時,用這個值會降低程式的效能。joined
告訴 SQLAlchemy 使用 JOIN 子句一次性地把關係資料從資料庫裡匯出來。關於 JOIN 可參閱這篇文章。subquery
類似joined
,但 SQLAlchemy 會使用子查詢來讀取資料庫。關於子查詢可參閱這篇文章。dynamic
針對一對多關係裡,資料量比較大時,這是個特殊且有用的型別。它不會一次性把所有的關係資料都從資料庫裡讀出來,相反它會返回一個查詢物件,在需要資料時,從這個查詢物件時進行二次查詢,才能獲得需要的資料。這種型別可以提高程式效能。
定義多對多關係
一個使用者可以屬於多個組,而一個組裡也會有多個使用者。針對這種多對多的關係,我們需要第三個表來儲存這種多對多關係。
1 2 3 4 |
groups_users = db.Table( 'groups_users', db.Column('user_id', db.Integer(), db.ForeignKey('users.id')), db.Column('group_id', db.Integer(), db.ForeignKey('groups.id'))) |
首先直接使用 db.Table
定義一個多對多的關係表 groups_users
。這裡要注意不要使用繼承db.Model
來定義這個多對多關係表。然後,在
User 類裡使用 db.relationship
來定義多對多關係:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class User(db.Model, UserMixin): __tablename__ = "users" __searchable__ = ['username', 'email'] id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(200), unique=True, nullable=False) email = db.Column(db.String(200), unique=True, nullable=False) _password = db.Column('password', db.String(120), nullable=False) ... secondary_groups = \ db.relationship('Group', secondary=groups_users, primaryjoin=(groups_users.c.user_id == id), backref=db.backref('users', lazy='dynamic'), lazy='dynamic') |
其中 LINE 10 - 15 使用 db.relationship
來定義多對多關係。第一個引數表示多對多關係的類為 Group,第二個引數 secondary=groups_users
表示需要從第三個叫 groups_users
的表裡獲取多對多關係,第三個引數 primaryjoin=(groups_users.c.user_id
== id)
表示連線查詢時的條件。第四個引數 backref=db.backref('users', lazy='dynamic')
會在 Group 類裡建立一個成員叫
users,其中 db.backref
的 lazy
引數為 dynamic
表示
Group.users 為一個查詢物件。第五個引數lazy='dynamic'
表示 User.secondary_groups 為一個查詢物件,其實這裡可以不要使用 dynamic
,因為一個使用者所屬的組是很有限的,不可能很多,可以一次性全部載入進來。
插入及修改記錄
插入記錄時,不用再寫 SQL 語句了,直接使用類物件來操作即可。使用者註冊成功後,需要向 users 表插入一條記錄。在 flaskbb.auth.RegisterForm.save()
裡實現:
1 2 3 4 5 6 7 |
def save(self): user = User(username=self.username.data, email=self.email.data, password=self.password.data, date_joined=datetime.utcnow(), primary_group_id=4) return user.save() |
建立一個 User 物件,然後呼叫物件的 save()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
def save(self, groups=None): """Saves a user. If a list with groups is provided, it will add those to the secondary groups from the user. :param groups: A list with groups that should be added to the secondary groups from user. """ if groups: # TODO: Only remove/add groups that are selected secondary_groups = self.secondary_groups.all() for group in secondary_groups: self.remove_from_group(group) db.session.commit() for group in groups: # Do not add the primary group to the secondary groups if group.id == self.primary_group_id: continue self.add_to_group(group) self.invalidate_cache() db.session.add(self) db.session.commit() return self |
其關鍵程式碼是 LINE 24 - 25。其中 db.session
物件是 Flask-SQLAlchemy 擴充套件為我們建立的一個事務物件,使用 db.session.add()
來插入記錄,使用 db.session.commit()
來提交事務,使操作生效。LINE
9 - LINE 22是當需要改變一個使用者所屬的組時的操作程式碼,這裡就不展開討論。
需要說明的是,修改記錄時也是使用 db.session.add()
方法。SQLAlchemy 會自動根據主鍵的值來判斷這是一個新加的記錄還是要修改的記錄。
關於db.session.commit()
User.save() 方法裡,當 groups 引數不為空時,會有兩個 db.session.commit() 的呼叫。把一個操作分成兩個事務,就達不到保證資料一致性的目的了。這裡的程式碼寫法應該可以再考量一下。
刪除記錄
當我們需要從 users 表裡刪除記錄裡,呼叫 User.delete()
方法即可,它的程式碼是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 |
def delete(self): """Deletes the User.""" # This isn't done automatically... PrivateMessage.query.filter_by(user_id=self.id).delete() ForumsRead.query.filter_by(user_id=self.id).delete() TopicsRead.query.filter_by(user_id=self.id).delete() db.session.delete(self) db.session.commit() return self |
LINE 9 - 10 是用來從 users 表裡刪除一條記錄。LINE 5-7 是用來在刪除使用者之前,把一些使用者相關的資料也一併刪除掉。
查詢記錄
繼承自 db.Model
的類會引入 query
屬性,這是個可查詢物件 Query
的例項。其常用的方法有query.filter()
,query.filter_by()
,query.order_by()
,query.limit()
,query.get()
等等。這些函式只是指定了查詢的條件,查詢真正開始是在呼叫 query.first()
,query.all()
等方法後才發生的。
例如,獲取使用者的主題個數 User.topic_count()
:
1 2 3 4 |
@property def topic_count(self): """Returns the thread count""" return Topic.query.filter(Topic.user_id == self.id).count() |
再如 User.delete()
的程式碼裡刪除使用者相關的資料的程式碼:
1 2 3 |
PrivateMessage.query.filter_by(user_id=self.id).delete() ForumsRead.query.filter_by(user_id=self.id).delete() TopicsRead.query.filter_by(user_id=self.id).delete() |
再如 User.save()
的程式碼裡關於群組的相關操作程式碼:
1 2 3 4 |
secondary_groups = self.secondary_groups.all() for group in secondary_groups: self.remove_from_group(group) db.session.commit() |
通過 self.secondary_groups.all()
獲取所有的群組,然後在這些群組裡把使用者移除。
filter() vs filter_by()
filter(*criterion)
使用 SQL 表示式,而 filter_by(**kwargs)
使用關鍵字表達式。從函式宣告可以看出來 filter()
接受的引數是一個元組表示式,而 filter_by()
接受的是一個
dict 表示式。所以,Topic.query.filter(Topic.user_id == self.id).count()
等價於Topic.query.filter_by(user_id
= self.id).count()
。關於這個區別,還可以進一步查閱StackOverFlow 及 SegmentFault 上的文章,還有官方的文件。順便吐槽一下,從這個對比可以看出來
StackOverFlow 和國內 SegmentFault 質量差異,順便再感慨一下,學 IT 的人英文不好你就等著受苦吧,永遠接觸不到第一手的權威資料。
關於查詢還需要說明的一點,Flask-SQLAlchemy 提供了便利的函式 get_or_404()
及first_or_404()
來替代 get()
和 first()
方法。這兩個方法在
view 裡特別有用,如找不到這個使用者時,直接丟擲 404 異常。而不是返回一個 None。
1 2 3 4 |
@user.route("/<username>") def profile(username): user = User.query.filter_by(username=username).first_or_404() return render_template("user/profile.html", user=user) |
MVC 程式碼結構
介紹完 ORM,我們可以看一下 FlaskBB 專案 flaskbb/flaskbb
目錄下的核心程式碼的 MVC 程式碼結構。它把每個模組封裝成一個獨立的 bluepoint,每個模組又分為
model,view,form 三個模組。這樣整體程式碼結構非常清晰。
flaskbb ├── __init__.py ├── _compat.py ├── app.py ├── email.py ├── extensions.py ├── auth │ ├── __init__.py │ ├── forms.py │ └── views.py ├── configs │ ├── __init__.py │ ├── default.py │ ├── development.py │ ├── development.py.example │ ├── production.py.example │ └── testing.py ├── fixtures │ ├── __init__.py │ ├── groups.py │ └── settings.py ├── forum │ ├── __init__.py │ ├── forms.py │ ├── models.py │ └── views.py ├── management │ ├── __init__.py │ ├── forms.py │ ├── models.py │ └── views.py ├── user │ ├── __init__.py │ ├── forms.py │ ├── models.py │ └── views.py └── utils ├── __init__.py ├── decorators.py ├── helpers.py ├── permissions.py ├── populate.py ├── settings.py └── widgets.py
結束語
本文簡單介紹了 ORM 操作資料庫的概念和一些基本的用法。可參考的資料很多,這裡強烈推薦官方文件,深入淺出。關於入門資料,可參閱 Flask-SQLAlchemy 官方文件。深入閱讀可以參考 sqlalchemy 官方文件。
相關推薦
FlaskBB閱讀筆記(三)
開篇 FlaskBB是用Flask框架實現的一個輕量級的論壇社群軟體,程式碼託管在GitHub上。本系列文章通過閱讀FlaskBB的原始碼來深入學習Flask框架,以及在一個產品級的Flask應用裡的一些最佳實踐規則。 本文是本系列文章的第三篇,將介紹ORM基礎知識,
Java集合框架閱讀筆記(三)ConcurrentHashMap
類繼承 only d+ nan next related ati null lur 預備知識 AQS(AbstractQueuedSynchronizer):提供了一個框架用來構造同步一些工具類比如ReentrantLock、 CopyOnWriteArrayList、
Software Testing 閱讀筆記(三)圖
無向圖和有向圖 無相同的相鄰矩陣是頂點和邊之間的關係,每列為2個1,代表邊和兩個頂點相連,這裡可以用作完整性檢查,類似對偶檢查。 下面是其上面無向圖的相鄰矩陣。 該表格為無向圖的關聯矩陣,當且僅當節點i是邊j的一個端點時,第i行第j列的元素為1 鄰接矩陣是指的是結點和
《好好學習》閱讀筆記(三)第二章:掌握臨界知識的方法
目錄 心態準備 具體方法 1.反思 心態準備 1.新舊知識建立聯絡 連線成網,從不同角度和領域對同一個知識進行分析,加深理解 [----知識結構 章魚狀 -》蜂窩狀] [----思維導圖試記憶-》殿堂式記憶(like sherlock)]
Sail.js官方文件閱讀筆記(三)——api/helpers/ 目錄
在1.0版本中,Sails推出了helper的內建支援,它是簡單的公用資源,可以用來共享Node程式碼。它可以減少重複,提高開發效率。和actions2一樣,它也可以使應用更容易生成文件。helper目錄位置如下所示: 3.1 總述 Sails中helper用來將重複的程式碼抽象到
深度學習論文閱讀筆記(三)之深度信念網路DBN
想要獲得更多深度學習在NLP方面應用的經典論文、實踐經驗和最新訊息,歡迎關注微信公眾號“DeepLearning_NLP” 或者掃描下方二維碼新增關注。 深度神經網路 12.《受限波爾茲曼機簡介》 (1)主要內容:主要介紹受限玻爾茲曼機(RBM)的基本模型、學習
《javascript設計模式與開發實踐》閱讀筆記(三)
this,call和apply 2.1 this this指標的用法,相信在很多場合都看到過,這裡也總結了幾點: 作為物件的方法呼叫 作為普通函式呼叫 構造器呼叫 Function.prototype.call或Function.prototype.ap
《人月神話》閱讀筆記(三)
現在 等待 不足 創建 我們 興趣 一句話 事物 般的 今天我閱讀了一部分《人月神話》,也是有一些感觸。 當閱讀到職業的樂趣這一節時有一句話:如果我們想解決問題,就必須試圖先去理解它職業的樂趣。而編程又何嘗不是呢,在編程中我們會遇到各種各樣的錯誤,越調試錯誤會越多
《好好學習》閱讀筆記(四)第三章:自己的臨界知識
目錄 對世界充滿好奇,獨立思考 自己的臨界知識來源 1.自己感興趣的領域的重要知識(針對已存在儲備理論規律) ----與後一章 興趣與天賦的關係一起看 瞭解事物背後的規律之後還需要進一步找到結論的原始出處,這樣會對結論的成立條件有更清楚的認識,
論文閱讀筆記(三十八):Dynamic Zoom-in Network for Fast Object Detection in Large Images
We introduce a generic framework that reduces the computational cost of object detection while retaining accuracy for scenarios whe
JAVA學習筆記(三)
byte repl efi ber 時間 clas 區分大小寫 增強for size @SuppressWarnings("resource")是用來壓制資源泄露警告的。比如使用io類,最後沒有關閉。Set集合特點:1)無序,不隨機2)元素唯一3)沒下標註意:Collect
《HTTP權威指南》--閱讀筆記(二)
cep ask 資源 phrase 格式 tel 位置 自動擴展 port URL的三部分: 1,方案 scheme 2,服務器位置 3,資源路徑 URL語法: <scheme>://<user>:<password>@&
5.28 周末筆記(三)
ifconf 地址 conf ges 筆記 mage 總結 config 查看 ifconfig一般須先安裝 查看已激活網卡 修改網卡地址(臨時): 永久修改網卡 查看運行狀態 查看網關 總結 梳理 5.28 周末筆記(三)
IDEA使用筆記(三)——小齒輪的顯示和隱藏(Autoscroll from Source)
當前 筆記 快捷鍵 什麽 編輯 osc .com log 文件 在玩快捷鍵的時候,不清楚自己操作了什麽,突然間發現——能直接定位到當前可編輯文件的哪個小齒輪,不見了,找了一會也沒弄出來,從網上搜索吧!也沒看到對應的方法,後來自己耐下心來復盤自己的操作,終於發現了,顯示或隱藏
thinkphp5.0學習筆記(三)獲取信息,變量,綁定參數
名稱 自動識別 參數順序 query images 報錯 oca nds arc 1.構造函數: 控制器類必須繼承了\think\Controller類,才能使用: 方法_initialize 代碼: <?php namespace app\lian\control
最優化學習筆記(三)最速下降法
tex track enter water pos 最優 content 分享 clas 最優化學習筆記(三)最速下降法
《Javascript高級程序設計》閱讀記錄(三):第五章 上
面向對象的語言 none 括號 數量 mas ie9 驗證 ive .so 這個系列以往文字地址: 《Javascript高級程序設計》閱讀記錄(一):第二、三章 《Javascript高級程序設計》閱讀記錄(二):第四章 這個系列,我會把閱讀《Jav
LINUX設備驅動程序筆記(三)字符設備驅動程序
準備 p s con 文件系統 write post container form nod <一>.主設備號和次設備號 對字符設備的訪問時通過文件系統內的設備名稱進行的。那些設備名稱簡單稱之為文件系統樹的節點,它們通常位於/dev文
python學習筆記(三)
list 內存 寫入 odin move 列表 付出 open ada 文件的操作一般分三步: 1、打開文件,獲取文件的指針(句柄) 2、通過指針(句柄)操作文件 3、關閉文件 現在有以下文件: 我們為愛還在學 學溝通的語言
Redis學習筆記(三)常用命令整理
mes ember nbsp end 插入 學習筆記 頻道 hash value Redis 常用命令 1.DEL key 刪除key2.EXISTS key 檢查key是否存在3.KEYS * 查看所有的key4.EXPIRE key seconds 設置key的過期時