1. 程式人生 > >FlaskBB閱讀筆記(三)

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=
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)

這樣我們就定義了一個叫 users 的表格,表格的名稱由 __tablename__ 指定。這樣任何對錶格的操作,都可以轉化為對 User 類的操作。程式碼裡的 db 物件是什麼呢?在 extensions.py 裡建立了 db 物件 db = SQLAlchemy()

。然後在 app.py 裡初始化這個 db 物件 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的過期時