定義關系
在關系型數據庫中,我們可以通過關系讓不同表之間的字段建立聯系。一般來說,定義關系需要兩步,分別是創建外鍵和定義關系屬性。在更復雜的多對多關系中,我們還需要定義關聯表來管理關系。下面我們學習用SQLAlchemy在模型之間建立幾種基礎的關系模式。
配置python shell上下文
在上面的操作中,每一次使用flask shell命令啟動python shell后都要從app模塊里導入db對象和相應的模型類。為什么不能把他們自動集成到python shell上下文里呢?就像flask內置的app對象一樣。這當然可以實現,我們可以使用app.shell_context_processor裝飾器注冊一個shell上下文處理函數。它和模板上下文處理函數一樣,也需要返回包含變量和變量值字典。
app.py: 注冊shell上下文處理函數
@app.shell_context_processor def make_shell_context(): return dict(db=db, Note=Note) # 等同於{'db': db, 'Note': Note}
當你使用flask shell命令啟動python shell時,所有使用app.shell_context_processor裝飾器注冊的shell上下文處理函數都會被自動執行,這會將db和Note對象推送到python shell上下文里:
>>> db <SQLAlchemy engine=sqlite:///D:\flask\FLASK_PRACTICE\DataBase\data.db> >>> Note <class 'app.Note'>
在下面演示各種數據庫關系時,將編寫更多的模型類。在示例程序中,都使用shell上下文處理函數添加到shell上下文中,因此你可以直接在python shell使用,不用手動導入。
一對多
我們將以作者和文章來演示一對多關系:一個作者可以寫多篇文章。一對多關系如下圖:

在示例程序中,Author類用來表示作者,Article類用來表示文章。
app.py: 一對多關系示例
class Author(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) class Article(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(50), index=True) body = db.Column(db.Text)
我們將在這兩個模型之間建立一個一對對關系,建立這個一對多關系的目的是在表示作者的Author類中添加一個關系屬性articles,作為集合(collection)屬性,當我們對特定的Author對象調用articles屬性會返回所有的Article對象。下面來介紹如何一步步定義這個一對多關系。
1、定義外鍵
定義關系的第一步是創建外鍵。外鍵是(foreign key)用來在A表存儲B表的主鍵值以便和B表建立聯系的關聯字段。因為外鍵只能存儲單一數據(標量),所以外鍵總是在“多”這一側定義,多篇文章屬於同一個作者,所以我們需要為每篇文章添加外鍵存儲作者的主鍵值以指向對應的作者。在Article模型中,我們定義一個author_id字段作為外鍵:
class Article(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(50), index=True) body = db.Column(db.Text) author_id = db.Column(db.Integer, db.ForeignKey('authod.id')) #指定顯示內容,否則默認顯示<表名 主鍵id> def __repr__(self): return '<Article %r>' % self.title
這個字段使用db.ForeignKey類定義為外鍵,傳入關系另一側的表名和主鍵字段名,即author.id。實際的效果是將article表的authod_id的值限制為author表的id列的值。它將用來存儲author表中記錄的主鍵值,如下圖:

外鍵字段的命名沒有限制,因為要連接的目標字段是author表的id列,所以為了便於區別而將這個外鍵字段的名稱命名為author_id。
傳入ForeignKey類的參數author.id,其中author指的是Author模型對應的表名稱,而id指的是字段名,即“表名.字段名”。模型類對應的表名由Flask-SQLAlchemy生成,默認為類名稱的小寫形式,多個單詞通過下划線分隔,你也可以顯示地通過__tablename__屬性自己指定。
2、定義關系屬性
定義關系的第二步是使用關系函數定義關系屬性。關系屬性在關系的出發側定義,即一對多關系的“一”這一側。一個作者擁有多篇文章,在Author模型中,定義一個articles屬性來表示對應的多篇文章:
class Author(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) articles = db.relationShip('Article') #指定顯示內容 def __repr__(self): return '<Author %r>' %self.name
關系屬性的名稱沒有限制,你可以自由修改。它相當於一個快捷查詢,不會作為字段寫入數據庫中。
這個屬性並沒有使用column類聲明為列,而是使用了db.relationship()關系函數定義為關系屬性,因為這個關系屬性返回多個記錄,我們稱之為集合關系屬性。relationship()函數的第一個參數為關系另一側的模型名稱,它會告訴SQLAlchemy將Author類與Article類建立關系。當這個關系屬性被調用時,SQLAlchemy會找到關系的另一側(即article表)的外鍵字段(即author_id),然后反向查詢article表中所有author_id值為當前表主鍵值(即author.id)的記錄,返回包含這些記錄的列表,也就是返回某個作者對應的多篇文章記錄。
>>> from app import Author, Article >>> from app import db >>> foo = Author(name = 'Foo') >>> spam = Article(title = 'Spam') >>> ham = Article(title = 'Ham') >>> db.session.add(foo) >>> db.session.add(spam) >>> db.session.add(ham)
3、建立關系
建立關系有兩種方式,第一種方式為外鍵字段賦值,比如:
>>> spam.author_id = 1 >>> db.session.commit()
我們將spam對象的author_id字段的值設為1,這會和id值為1的Author對象建立關系。提交數據庫改動后,如果我們隊id為1的foo對象調用articles關系屬性,會看到spam對象包括在返回的Article對象列表中:
>>> foo=Author.query.first() >>> foo <Author u'F00'> >>> foo.articles [<Article u'Spam'>, <Article u'Ham'>]
另一種方式是通過操作關系屬性,將關系屬性付給實際的對象即可建立關系。集合關系屬性可以像列表一樣操作,調用append()方法來與一個Article對象建立關系:
>>> foo.articles [<Article u'Spam'>, <Article u'Ham'>] >>> A1 = Article.query.get(3) >>> A1 <Article u'A1'> >>> foo.articles.append(A1) >>> db.session.commit() >>> foo.articles [<Article u'Spam'>, <Article u'Ham'>, <Article u'A1'>]
我們也可以直接將關系屬性賦值給一個包含Article對象的列表。
>>> foo.articles [<Article u'Spam'>, <Article u'Ham'>, <Article u'A1'>] >>> foo.articles = foo.articles[1:2] >>> foo.articles [<Article u'Ham'>]
和前面的第一種方式類似,為了讓改動生效,我們需要調用db.session.commit()方法提交數據庫會話。
建立關系后,存儲外鍵的author_id字段會自動獲得正確的值,而調用Author實例的關系屬性articles時,會獲得所有建立關系的Article對象:
>>> foo.id 2 >>> A1.author_id 2 >>> foo.articles [<Article u'Spam'>, <Article u'Ham'>, <Article u'A1'>]
和主鍵類似,外鍵字段有SQLAlchemy管理,我們不需要手動設置。當通過關系屬性建立關系后,外鍵字段會自動獲得正確的值。
和append()相對,對關系屬性調用remove()方法可以與對應的Article對象接觸關系:
>>> soo.articles [<Article u'Ham'>, <Article u'A1'>] >>> soo.articles.remove(A1) >>> soo.articles [<Article u'Ham'>] >>> db.session.commit() >>> soo.articles [<Article u'Ham'>]
你也可以使用pop()方法操作關系屬性,它會與關系屬性對應的列表的最后一個Article對象接觸關系並返回改對象。
不要忘記在操作結束后需要調用commit()方法提交數據庫會話,這樣才可以把數據寫入數據庫。
是用關系函數定義的屬性不是數據庫字段,而是類似於特定的查詢函數。當某個Article對象被刪除時,在對應Author對象的articles屬性調用時返回的列表也不會包含該對象。
在關系函數中,有很多參數可以用來設置調用關系屬性進行查詢時的具體行為。常用的SQLAlchemy關系函數參數如下所示:

當關系屬性被調用時,關系函數會加載相應的記錄,下表列出了控制關系記錄加載方式的lazy參數的常用選項。
常用的SQLAlchemy關系記錄加載方式(lazy參數可選值):

dynamic選項僅用於集合關系屬性,不可用於多對一、一對一或是在關系函數中將uselist參數設為False的情況。
要避免使用dynamic來動態加載所有集合關系屬性對應的記錄,使用dynamic加載方式以為這每次操作關系都會執行一次SQL查詢,這會造成潛在的性能問題。大多數情況下我們只需要使用默認值(select),只有在調用關系屬性會返回大量記錄,並且總是需要對關系屬性返回的結果附加額外的查詢時才需要動態加載(lazy=’dynamic’)。
4、建立雙向關系
我們在Author類中定義了集合關系屬性articles,用來獲取某個作者擁有的多篇文章記錄。在某些情況下,你也許希望能在Article類中定義一個類似的author關系屬性,當被調用時返回對應的作者記錄,這類返回單個值的關系屬性被稱為標量關系屬性。而這種兩側都添加關系屬性獲取對方記錄的關系我們稱之為雙向關系(bidirectional relationship)
雙向關系並不是必須的,但在某些情況下會非常方便。雙向關系的建立很簡單,通過在關系的另一側也創建一個relationship()函數,我們就可以在兩個表之間建立雙向關系。我們使用作家(Writer)和書(Book)的一對多關系來進行演示,建立雙向關系后的Writer和Book類如下所示:
app.py: 基於一對多關系的雙向關系
class Writer(db.Model): id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(70), unique = True) #back_populates, 定義雙向關系 # back_populates參數的值需要設為關系另一側的關系屬性名 books = db.relationship('Book', back_populates='writer') def __repr__(self): return '<Writer %r>' % self.name class Book(db.Model): id = db.Column(db.Integer, primary_key = True) title = db.Column(db.String(50), index = True) writer_id = db.Column(db.Integer, db.ForeignKey('writer.id')) writer = db.relationship('Writer', back_populates = 'books') def __repr__(self): return '<Book %r>' % self.title
在“多”這一側的Book(書)類中,我們新創建了一個writer關系屬性,這是一個標量關系屬性,調用它會獲取對應的Writer(作者)記錄;而在Writer(作者)類中的books屬性則用來獲取對應的多Book(書)記錄。在關系函數中,我們使用back_populates參數來連接對方,back_populates參數的值需要設為關系另一側的關系屬性名。
我們先創建1個Writer和2個Book記錄,並添加到數據庫中:
>>> from app import Writer >>> from app import Book, db >>> king = Writer(name = 'Stephen KKing') >>> carrie = Book(title = 'Carrie') >>> it = Book(title = 'IT') >>> db.session.add(king) >>> db.session.add(carrie) >>> db.session.add(it) >>> db.session.commit() #當一個book對象修改了writer屬性,對應的writer對象的books屬性會跟着修改(刪去),#修改后的writer屬性對應的writer對象的books屬性會增加 >>> king.books [<Book u'IT'>, <Book 'Marry'>] >>> job = Writer(name = 'job') >>> db.session.add(job) >>> db.session.commit() >>> job.id 2 >>> marry = Book(title = 'Marry') >>> marry.writer = job >>> marry.writer <Writer u'job'> >>> king.books [<Book u'IT'>] >>> job.books [<Book u'Marry'>]
設置雙向關系后,除了通過集合屬性books(多個)來操作關系,我們也可以使用標量屬性writer來進行關系操作。比如,將一個Writer對象賦值給某個Book對象的writer屬性,就會和這個Book對象建立關系:
>>> king <Writer u'Stephen KKing'> >>> carrie <Book u'Carrie'> >>> carrie.writer = king >>> carrie.writer <Writer u'Stephen KKing'> #carrie這條數據的關系writer指向king,同時king這條數據的books屬性也指向了carrie,這是雙向的 >>> king.books [<Book u'Carrie'>] >>> it <Book u'IT'> >>> it.writer = king #同上 >>> king.books [<Book u'Carrie'>, <Book u'IT'>]
相對的,將某個book的writer屬性設為None,就會解除與對應Writer對象的關系:
>>> carrie.writer = None >>> king.books [<Book u'IT'>] >>> db.session.commit()
需要注意的是,我們只需要在關系的一側操作關系。當為Book對象的writer屬性賦值后,對應Writer對象的books屬性的返回值也會自動包含這個Book對象。反之,當某個Writer對象被刪除時,對應的Book對象的writer屬性被調用時的返回值也會被置為空(即NULL,會返回None)。
其他關系模式建立雙向關系的方式完全相同。
5、使用backref簡化關系定義
在介紹關系函數的參數時,我們曾提到過,使用關系函數中的backref參數可以簡化雙向關系的定義。以一對多關系為例,backref參數用來自動為關系另一側添加關系屬性,作為反向引用(back reference),賦予的值會作為關系另一側的關系屬性名稱。比如,我們在Author一側的關系函數中將backref參數設為author,SQLAlchemy會自動為Article類添加一個author屬性。為了避免和前面的實例命名沖突,我們使用歌手(Singer)和歌曲(Song)的一對多關系作為演示,分別創建Singer和Song類,如下所示:
class Singer(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) songs = db.relationship('Song', backref='singer') def __repr__(self): return "<Singer %r>" % self.name class Song(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), index=True) singer_id = db.Column(db.Integer, db.ForeignKey('singer.id')) def __repr__(self): return '<Song %r>' % self.name
在定義集合屬性songs的關系函數中,我們將backref參數設置singer,這會同時在Song類中添加一個singer標量屬性。我們僅需要定義這一個關系函數,雖然singer是一個“看不見的關系屬性”,但是使用上和定義兩個關系函數並使用back_poplulates參數的效果完全相同。
需要注意的是,使用backref允許我們僅在關系一側定義另一側的關系屬性,但是在某些情況下,我們希望可以對在關系另一側的關系屬性進行設置,這時就需要使用backref()函數。backref()函數接收第一個參數作為在關系另一側添加的關系屬性名,其他關鍵字參數會作為關系另一側關系函數的參數傳入。比如,我們要在關系另一側“看不見的relationship()函數”中將uselist參數設為False,可以這樣:
class Singer(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(70), unique=True) songs = relationship('Song', backref=backref('singer', uselist=False)) >>> from app import Singer >>> from app import Song >>> singer1 = Singer(name = 'Huyanbin') >>> song1 = Song(name = 'HongYan') >>> singer1 <Singer 'Huyanbin'> >>> song1 <Song 'HongYan'> >>> song1.singer = singer1 >>> song1.singer <Singer 'Huyanbin'> >>> singer1.songs [<Song 'HongYan'>] >>> song2 = Song(name = 'manman') >>> song2.singer = singer1 >>> song2 <Song 'manman'> >>> singer1.songs [<Song 'HongYan'>, <Song 'manman'>] #提交數據庫會話,落表 >>> db.session.add(singer1) >>> db.session.add(song1) >>> db.session.add(song2) >>> singer1.id >>> db.session.commit() >>> singer1.id 2 >>> song1.id 1 >>> song2.id 2
盡管使用backref非常方便,但通常來說“顯示好過隱式”,所以我們應該盡量使用back_populates定義雙向關系。
