Python Web Flask — Relational database 關聯式資料庫的建立

Sean Yeh
Python Everywhere -from Beginner to Advanced
13 min readDec 24, 2020
Quebec, Canada, photo by Sean Yeh

到目前為止我們已經知道在Flask裡面建立SQLLite資料庫,需要下面幾個步驟:匯入套件、建立資料庫路徑、建立Flask App、Configure 參數設定與傳到SQLAlchemy。

建立SQLLite資料庫後,要透過Model建立資料庫裡面的table資料表,包括裡面的各個欄位。

建立這Model需要下面幾個步驟:建立Class類別、類別繼承自db.Model、可選用是否提供資料表的名稱、建立資料表欄位與加上__init__與__repr__。

如果還不是很清楚上面的步驟,可以參看下面的文章。在這篇文章中,有較為詳細的說明與實作。

今天我們要再來深入一點探討,如何在資料庫中建立含有兩個以上關連的資料表。

關聯式資料庫

所謂關聯式資料庫(Relational database),根據維基百科上面的敘述,是建立在關聯模型基礎上的資料庫,藉助於集合代數等數學概念和方法來處理資料庫中的資料。我們等一下會使用ER Model來表示資料表之間的關係。

ER Model實體關係圖

我們先解釋一下什麼是ER Model實體關係圖?所謂的ER Model(全名Entity-Relation Model)是用來描述「實體與實體」之間「相互關係」的工具。由於採用圖形化的表示方式,使得這樣子的抽象關係可以被非技術人員所理解,可以作為系統設計者與使用者之間的溝通橋樑。

這裡的實體是指真實世界的物件是個名詞。例如:員工、客戶、會員等。在資料庫的實務上,我們可以把實體描述為各種資料表,例如:員工實體,就是員工資料表;客戶實體,就是客戶資料表;會員實體,就是會員資料表等。

至於關係是用來說明一個「實體」與另外一個「實體」之間的關聯方式。例如兩個實體間有著「一對一的關係」、「一對多的關係」或者是「多對多的關係」。

ER圖示

在ER Model實體關係圖中,我們可以使用矩形來表示實體,橢圓形表示它的屬性而菱形表示實體之間的關係。

假設我們有一個專案(Projects)實體,這個實體有專案的id(proj_id)、專案的名稱(proj_name)與專案進行的狀態(peoj_ststus)三個屬性。

這時候我們可以使用下面的圖形來表示。其中專案的id具有唯一的性質,我們在名稱下面加上了底線。

實體與屬性

我們可以發現一個實體在關聯式資料庫中視為一個「資料表」,它可能含有很多個可以用來描述該實體的屬性。這些屬性就是資料表裡面的「欄位」。因此,上面的ER圖可以被解釋為下面的Projects資料表的欄位:

資料表

前面我們提過,「關係」是用來說明「實體」與「實體」之間的關聯方式。關係有「一對一」、「一對多」、「多對一」或者是「多對多」的關聯方式。

在下面我們將原來的ER圖,加入另外一個Owners實體。

從下面的圖可以解讀出Projects實體與Owners實體之間具有「一對一」的關係。也就是說一個Owners實體,可以建立一個Project專案;一個Project專案被一個Owners實體所建立。

一對一關係

如果,我們認為一個Owners實體可以建立多個Projects實體的話,上面的圖形應該以下面的方式呈現:

一對多關係

以上就是ER Model實體關係圖的簡單解釋,對於接下來說明關聯式資料庫所需要的ER Model背景知識,已經足夠,因此這個議題就停在這裡。接著我們要回到關聯式資料庫,看看應該如何把兩個資料表關聯在一起?

談到關聯式資料庫,就需要提到主索引鍵(Primary Keys)和外部索引鍵(Foreign Keys)兩種重要的資料庫物件。

Primary Keys 與 Foreign Keys

所謂主索引鍵(Primary Keys)是指,該欄位內容具有獨一無二的值。在同一資料表裡面,不會出現重複。舉例來說,如果我們有一個會員的資料表,裡面有姓名、身分證字號、地址、住家電話等等欄位。試問這些欄位裡面哪一個欄位的值是獨一無二?

利用消去法刪除不可能的欄位。首先地址、與住家電話應被排除,因為同一個地方可能會有兩個以上(自己與其他家人)的人居住、同一支電話號碼也可能代表不同人的住家電話號碼,是以這兩個欄位無法作為主索引鍵的代表。接著再來看姓名欄位是否可以當作主索引鍵?不幸的是,這世界上有不少人的姓名是同名同姓的菜市場名,因此這個欄位也無法作為主索引鍵的代表。最後,最有資格的欄位就剩下身分證字號了。

在實務上,有時基於某些原因,使用者不想填寫這個私密性高的欄位,或者是使用者本身並非本國人而沒有身分證字號可以填寫。我們會使用一個id欄位來代表主索引鍵,而不是使用身分證字號。這樣可以保證每個會員都會被發到一個獨一無二的號碼。

而所謂的外部索引鍵(Foreign Keys),則是另外一張資料表中的Primary Keys。因為外部索引鍵可用來建立與強制兩資料表的資料之間連結的一個欄位,以控制外部索引鍵資料表中可儲存的資料。

例如上面兩個資料表:Owners資料表與Projects資料表。兩個資料表分別有自己的主索引鍵(以PK表示),Owners資料表的主索引鍵為owner_id而Projects資料表的主索引鍵為proj_id。若要將兩個資料表連在一起的話,我們需要在兩者間加入一個外部索引鍵。在這個例子裡面,我們在Owners資料表中,加入一個proj_id的外部索引鍵(以FK表示)。

因此,一個table資料表中只能有一個主索引鍵,但是可以有很多外部索引鍵。主索引鍵與外部索引鍵的配對就產生了資料表之間的關聯性 ,所以我們稱它們為「關聯式資料庫」。

將兩個資料表關聯在一起

接下來,我們要設計關聯式資料庫。這個資料庫用來儲存專案中的代辦事項,以及專案中每個代辦事項清單的負責人。

下面是資料表的關係圖,包含「一對一」與「一對多」關係。

首先我們建立三個資料表,名稱分別為tasks、owners與projects,分別表示代辦清單資料表(tasks)、負責人資料表(owners)與專案資料表(projects)。並依照資料表,分別建立modle:

代辦清單Task Model:

class Task(db.Model):    """Task DB"""
__tablename__ = 'tasks'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
priority = db.Column(db.Integer)
date = db.Column(db.DateTime, default= datetime.utcnow)
remorks = db.Column(db.Text)
# 一對一
owner = db.relationship('Owner', backref='task', uselist= False)

# 一對多
project_id = db.Column(db.Integer,db.ForeignKey('projects.id'))
def __init__(self, name, priority, date, remorks): self.name = name
self.priority = priority
self.date = date
self.remorks = remorks

負責人Owner Model:

class Owner(db.Model):    """Owner DB"""
__tablename__ = 'owners'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
task_id = db.Column(db.Integer, db.ForeignKey('tasks.id'))
def __init__(self, name, task_id):
self.name = name
self.task_id = task_id

專案Project Model:

class Project(db.Model):    """Project DB"""
__tablename__ = 'projects'
id = db.Column(db.Integer, primary_key=True)
proj_name = db.Column(db.String(255))
proj_status = db.Column(db.String(100))
client_name = db.Column(db.String(100))
org_name = db.Column(db.String(255))
task = db.relationship('Task',backref='project', uselist=True)
def __init__(self, proj_name, proj_status, client_name, org_name, task_id):
self.proj_name = proj_name
self.proj_status = proj_status
self.client_name = client_name
self.org_name = org_name
self.task_id = task_id

從上面三個Model可以觀察到,Task與Owner間有一對一關係,而Task與Project間有一對多關係。

一對多關係

由於一個Project專案裡面可以有多個Task代辦事項,Project資料表與Task資料表之間的關係應該被設計為「一對多」的關聯性。

因此,我們建立如下的Project Model 中要加上db.relationship來關聯Project:

task = db.relationship('Task', backref='project', uselist= True, lazy= 'dynamic')

這一行讓Project Model 與 Task Model進行了一個連結。db.relationship用於兩個資料表間建立「一對多」關係。db.relationship放在「一對多」關係中「一」的部分,另外還要在「多」這一側資料表加入一個外部索引鍵(Foreign Keys),指向「一」這一側的記資料表錄。

雖然在大多數情況下, db.relationship能自行找到關係中的外部索引鍵(Foreign Keys),但有時若其中有兩個以上的欄位定義為外部索引鍵時,SQLAlchemy 就不知道該選擇哪一個欄位。在這時就需要替db.relationship提供額外的參數,用以指定應該使用的外部索引鍵。在Task Model我們指定project_id外部索引鍵。

project_id = db.Column(db.Integer, db.ForeignKey('projects.id'))

回到「一」的部分,db.relationship提供了 Project資料表對 Task資料表的查詢。而backref正好相反,提供了Task資料表對 Project資料表的查詢。

此外,設定db.relationship的lazy屬性可以指定SQLAlchemy資料庫什麼時候該載入資料,有下面幾種可以選擇:

  • select 選擇:為預設,會直接全部載入資料。
  • dynamic動態:只能用在「一對多」、「多對多」的狀況,不先載入紀錄,只保留物件狀態提供未來查詢。
  • joined參加:對關聯的兩個資料表聯接一起。

在這裡我們使用參數lazy='dynamic',讓SQLAlchemy在搜尋關聯資料的時候保留物件狀態。

一對一關係

由於我們假定一個代辦事項只有一個負責的承辦人,Task Model與Owner Model之間的關係是「一對一」的關聯性。因此,我們要在Task Model中要加上db.relationship來關聯Owner Model:

owner  = db.relationship('Owner', backref='task', uselist= False)

若兩個Model之間的關聯是一對一的,宣告方式與一對多相同,只需要在主要Model的db.relationship中加入參數uselist=False就可以。

完成上面的Model後,在終端機執行下面指令初始化(假定檔名為models.py):

$ export FLASK_APP=models.py$ flask db init$ flask db migrate -m "initial migration"$ flask db upgrade

使用軟體觀察結果

再次使用DB軟體打開data.sqlite,可以看到資料表結構產生下面的變化。

我們已經成功的建立owners、projects、tasks資料表,每個資料表的欄位與個別的Model一樣。

--

--

Sean Yeh
Python Everywhere -from Beginner to Advanced

# Taipei, Internet Digital Advertising,透過寫作讓我們回想過去、理解現在並思考未來。並樂於分享,這才是最大贏家。