Flask 數據庫多對多關系


社交Web程序允許用戶之間相互聯系,在程序中,這種關系成為關注者、好友、聯系人、聯絡人或伙伴,但不管使用哪個名字,其功能都是一樣的,而且都要記錄兩個用戶之間的定向聯系,在數據庫查詢中也要使用這種聯系

再論數據庫關系

之前我們說過,數據庫使用關系建立記錄之間的聯系,其中,一對多關系是最常用的關系類型,它把一個記錄和一組相關的記錄聯系在一起,實現這種關系時,要在“多”這個側加入一個外鍵,指向“一”這一側聯結的記錄,目前我們的程序現在包含兩個一對多關系:一個把用戶角色和一組用戶聯系起來,另一個把用戶和發布的博客文章聯系起來

大部分的其他關系類型都可以從一對多類型中衍生,多對一關系從“多”這一側看就是一對多關系,一對一關系類型是簡化版的一對多關系,限制多這一側最多只能有一個記錄,唯一不能從一對多關系中簡單演化出來的類型是多對多關系這種關系的兩側都有多個記錄

多對多關系

一對多關系、多對一關系和一對一關系至少都有一側是單個實體,所以記錄之間的聯系通過外鍵實現,讓外鍵指向這個實體,但是我們要怎么實現兩側都是“多”的關系呢

下面以一個典型的多對多關系為例,即一個記錄學生和他們所選課程的數據庫,很顯然,你不能在學生表中加入一個指向課程的外鍵,因為一個學生可以選擇多個課程,一個外鍵不夠用,同樣,你也不能在課程表中加入一個指向學生的外鍵,因為一個課程有多個學生的選擇,兩側都需要一組外鍵

這種問題的解決方法是添加第三張表,這個表稱為關聯表,現在,多對多的關系可以分解成原表和關聯表之間的兩個一對多關系,下圖描繪了學生和課程之間的多對多關系 
這里寫圖片描述

這個例子中的關聯表是registrations,表中每一行都表示一個學生注冊的一個課程

查詢多對多關系要分成兩步,若想知道某位學生選擇了哪些課程,要先從學生和注冊之間的一對多關系開始,獲取這位學生在registrations表中的所有記錄,然后再按照多到一的方向遍歷課程和注冊之間的一對多關系,找到這位學生在registrations表中個記錄所對應的課程,同樣,若想找到選擇了某門課程的所有學生,要先從課程表中開始,獲取其在registrations表中的記錄,再獲取這些記錄聯接的學生

通過遍歷兩個關系來獲取查詢結果的做法聽起來有難度,不過像前例這種簡單關系,SQLAlchemy就可以完成大部分操作,多對多關系使用的代碼如下:

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

多對多關系仍使用定義一對多關系的db.relationship()方法進行定義,但在多對多關系中,必須把secondary參數設為關聯表,多對多關系可以在任何一個類中定義,backref參數會處理好關系的另一側,關聯表就是一個簡單的表,不是模型,SQLAlchemy會自動接管這個表,classes關系使用列表語義,這樣處理多對多關系特別簡單,假設學生是s,課程是c,學生注冊課程的代碼是:

>>> s.classes.append(c)
>>> db.session.add(s)
  • 1
  • 2
  • 1
  • 2

列出學生s注冊的課程以及注冊了課程c的學生也很簡單:

>>>s.classes.all()
>>>c.students.all()
  • 1
  • 2
  • 1
  • 2

Class模型中的students關系由參數db.backref()定義,注意這個關系中還指定了lazy='dynamic'參數,所以關系兩側返回的查詢都可接受額外的過濾器

如果后來學生s決定不選課程c了,那么可使用下面的代碼更新數據庫:

>>> s.classes.remove(c)
 
 
  • 1
  • 1

自引用關系

多對多關系可用於實現用戶之間的關注,但存在一個問題,在學生和課程的例子中,關聯表聯接的是兩個明確的實體,但是表示用戶關注其他用戶時,只有用戶一個實體,沒有第二個實體

如果關系中的兩側都在同一個表中,這種關系成為自引用關系,在關注中,關系的左側是用戶實體,可以稱為“關注者”,關系的右側也是用戶實體,但這些是”被關注者“,從概念上來看,自引用關系和普通關系沒什么區別,只是不易理解,下圖是自引用關系的數據庫圖解,表示用戶之間的關注: 
這里寫圖片描述

本例的關聯表是follows,其中每一行都表示一個用戶關注了另一個用戶,圖中左邊表示的一對多關系把用戶和follows表中的一組記錄聯系起來,用戶是關注者,圖中右邊表示的一對多關系把用戶和follows表中的一組記錄聯系起來,用戶是被關注者

高級多對多關系

自引用多對多關系可在數據庫中表示用戶之間的關注,但卻有個限制,使用多對多關系時,往往需要存儲所聯兩個實體之間的額外信息,對用戶之間的關注來說,可以存儲用戶關注另一個用戶的日期,這樣就能按照時間順序列出所有的關注者,這種信息只能存儲在關聯表中,但是在之前實現的學生和課程之間的關系中,關聯表完全是由SQLAlchemy掌控的內部表

為了能在關系中處理自定義的數據,我們必須提升關聯表的地位,使其變成程序可訪問的模型,新的關聯表如下,使用Follow模型表示:

# app/main/models.py
# ...
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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

SQLAlchemy不能直接使用這個關聯表,因為如果這么做程序就無法訪問其中的自定義字段,相反地,要把這個多對多關系的左右兩側拆分成兩個基本的一對多關系,而且要定義成標准的關系,代碼如下:

# app/models.py
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')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在這段代碼中,followedfollowers關系都定義為單獨的一對多關系,為了消除外鍵間的歧異,定義關系時必須使用可選參數foreign_keys指定的外鍵,而且,db.backref()參數並不是指定這兩個關系之間的引用關系,而是回引Follow模型

回引中的lazy參數指定為joined,這個lazy模式可以實現立即從聯結查詢中加載相關對象,例如如果某個用戶關注了100個用戶,調用user.followed.all()后會返回一個列表,其中包含100個Follow實例,每一個實例的follower和followed回引屬性都指向相應的用戶,設定為lazy='joined'模式,就可在一次數據庫查詢中完成這些操作,如果把lazy設為默認值select,那么首次訪問followerfollowed屬性時才會加載對應的用戶,而且每個屬性都需要一個單獨的查詢,這就意味着獲取全部被關注用戶時需要增加100次額外的數據庫查詢

cascade參數配置在父對象上執行的操作對相關對象的影響,比如層疊選項可設定為:將用戶添加到數據庫會話后,要自動把所有關系的對象都添加到會話中,層疊選項的默認值能滿足大多數情況的需求,但對這個多對多關系來說卻不合用,刪除對象時,默認的層疊行為是把對象聯接的所有相關對象的外鍵設為空值,但在關聯表中,刪除記錄后正確的行為應該是把指向該記錄的實體也刪除,因為這樣能有效銷毀聯接,這就是層疊選項值delete-orphan的作用

cascade參數的值是一組由逗號分隔的層疊選項,這看起來可能讓人有點困惑,但all表示除了delete-orphan之外的所有層疊選項,設為all,delete-orphan的意思是啟用所有默認層疊選項,還要刪除孤兒記錄

程序現在要處理兩個一對多關系,以便實現多對多關系,由於這些操作經常需要重復執行,所以最好在User模型中為所有可能的操作定義輔助方法,用於控制關系的4個新方法如下:

# app/models.py
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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

follow()方法手動把Follow實例插入關聯表,從而把關注者和被關注者聯接起來,並讓程序有機會設定自定義字段的值,聯接在一起的兩個用戶被手動傳入Follow類的構造器,創建一個Follow新實例,然后像往常一樣,把這個實例對象添加到數據庫會話中,注意,這里無需手動設定timestamp字段,因為定義字段時已經指定了默認值,即當前日期和時間,unfollow()方法使用followed關系找到聯接用戶和被關注用戶的Follow實例,若要銷毀這兩個用戶之間的聯接,只需刪除這個Follow對象即可,is_following()方法和is_followed_by()方法分別在左右兩邊的一對多關系中搜索指定用戶,如果找到了就返回True


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com