Try   HackMD

Flask實作_ext_02_Flask-SQLAlchemy_關聯

tags: flask flask_ext python SQLAlchemy

Flask實作_ext_01_Flask-SQLAchemy_初探中,我們瞭解到如何透過類別的建置來產生表單,此篇主要討論資料表之間的關聯設置。
關聯式資料庫中,不外乎一對一、一對多、多對一與多對多的關聯方式,關聯的設置影響到我們對物件的操作,老話一句,需求推動功能,先瞭解基本,有進階需求的時候自然會尋找答案,另外,flask-sqlalchemy的官方文件對幾種關聯方式都有案例,如果臨時想不到如何設置的時候記得查詢官方文件如官方文件Declaring Models

範例

範例_一對多

我們利用上一篇的範例來做調整,如下修正:

from flask import Flask # from flask.ext.sqlalchemy import SQLAlchemy<--新版取消了 from flask_sqlalchemy import SQLAlchemy import os # 取得目前文件資料夾路徑 pjdir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) # 新版本的部份預設為none,會有異常,再設置True即可。 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True # 設置sqlite檔案路徑 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \ os.path.join(pjdir, 'data.sqlite') db = SQLAlchemy(app) class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) # 設置關聯,relationship設置於一對多的『一』 contacts = db.relationship('Contact', backref='user') def __repr__(self): return '<User %r>' % self.username class Contact(db.Model): __tablename__ = 'contacts' id = db.Column(db.Integer, primary_key=True) contact_style = db.Column(db.String) contact_context = db.Column(db.String) # 設置外來鍵,ForeignKey設置於一對多的『多』 user_id = db.Column(db.Integer, db.ForeignKey('users.id')) def __repr__(self): return 'contact_style:%s, contact_context:%s' % \ (self.contact_style, self.contact_context)

SQLAlchemy中,每個Model都要求要設置主鍵,名稱通常設置為id

範例中設置了兩個類別,一個使用者(User),一個聯絡方式(Contact),在寫程式的時候我會特別註記好設置關聯的那一方是『一對多的一或一對多的多』,透過兩邊的設置讓SQLAlchemy瞭解關聯的關係。

在Python Shell中執行db.create_all()之後,可以透過sqlite圖形介面查看,已經確實的產生了兩個Table,如下圖:

contacts = db.relationship('Contact', backref='user')

db.relationship,透過關聯設置讓人家知道ContactUser相愛,兩個人之間恩愛透過backref來連接關係。(也可以透過back_populates設置)不過實際上這是一個在雙方都設置監聽器的作法,透過db.relationshipSQLAlchemy知道ContactUser是有關聯的,但前提是必需設置ForeignKey,更深入的說明可參閱參考文件中Relationships-backref

user_id = db.Column(db.Integer, db.ForeignKey('users.id'))

db.ForeignKey可很以直觀的以資料庫的角度去看,這代表我們在保存聯絡方式的時候會在資料庫內存儲使用者的IDusers.id

透過Python Shell來improt設定的兩個資料類別,並新增使用者,範例如下:

>>> from model_login import User >>> from model_login import Contact >>> from model_login import db >>> admin=User(username='admin',email='admin@abc.com') >>> admin.contacts []

第4行:建立一個新的使用者,使用者名稱為admin
第5行:查詢關聯,回傳為空的list,這是因為我們尚未建置任何關聯資料

現在我們來建立聯絡方式,範例如下:

>>> contact = Contact(contact_style='email',contact_context='admin2@abc.com') >>> admin.contacts.append(contact) >>> admin.contacts [contact_style:email, contact_context:admin2@abc.com] >>> contact.user <User 'admin'>

第1行:建立新的聯絡方式
第2行:利用append的方式將該物件寫入list,也就是我們設置的關聯contacts
第3行:查詢目前使用者的聯絡方式
第5行:查詢這聯絡方式的使用者,注意到user即是我們在db.relationship中設置的backref=user

到這邊,應該可以理解相關的設置邏輯,也可能很驚訝就這樣就已經完成了一個表單的關聯,而且是雙向的設置,這代表如果我們要清除資料的話,只需要刪除一邊即可,如下:

>>> contact.user = None >>> admin.contacts []

上面的案例可以看的出來,將contact.user設置為None之後,admin.contacts也變空的list了,非常令人驚豔。

現在我們來看將資料正式的寫入資料庫的情況,如下:

admin=User(username='admin',email='admin@abc.com') contact = Contact(contact_style='email',contact_context='admin2@abc.com') admin.contacts.append(contact) db.session.add(admin) db.session.commit()

執行之後會發現,雖然session.add只有將admin這個類寫入,但是在關聯設置之下contact也一併寫入資料庫。

另一種關聯寫入資料的方式

>>> from model_login import db,User,Contact >>> db.create_all() >>> user_admin = User(username='admin',email='admin@abc.com') >>> contact_admin = Contact(contact_style='phone', contact_context='3345678', user=user_admin) >>> user_admin.contacts [contact_style:phone, contact_context:3345678] >>> contact_admin.user <User 'admin'> >>> db.session.add_all([user_admin, contact_admin]) >>> db.session.commit()

第5行:實作中加入關聯物件

上面範例,在實作『多』的物件時直接利用backref屬性設置關聯之後寫入資料庫。

relationship_lazy

在建立好UserContact的關聯之後,我們嚐試著select一筆使用者資料,如下:

>>> user_query = User.query.filter_by(username='admin').first() >>> user_query.contacts [contact_style:phone, contact_context:3345678, contact_style:email, contact_context:admin2@abc.com]

透過user_query.contacts查詢會發現相關關聯資料已經幫你一起帶出了,很不錯吧,又一次的驚豔!

只是這種情況下會有一個問題,如果你想再透過查詢式query來查詢你的contacts就沒有辦法了,因為生米已成熟飯,它的type已經是list,我們必需保留它是物件的狀態,在有需要的時候我們再來對關聯資料做二次查詢。

這時候就需要調整db.relationship的參數,加入lazy,調整如下:

class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) # 設置關聯,relationship設置於一對多的『一』 contacts = db.relationship('Contact', backref='user', lazy='dynamic') # def __init__(self, username, email): # self.username = username # self.email = email def __repr__(self): return '<User %r>' % self.username

第7行:加入參數lazy='dynamic'

透過參數lazy='dynamic'可以讓SQLAlchemy在搜尋關聯資料的時候保留物件狀態,如下說明:

>>> user_query = User.query.filter_by(username='admin').first()
>>> user_query.contacts()
<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x0000000003ACFC50>

這時候回傳資料是一個SQLAlchemy物件類別,這樣就可以透過filter的方式再來做二次搜尋,或是透過order_by做排序,如下範例:

# 透過all做全部呈現 >>> user_query.contacts.all() [contact_style:phone, contact_context:3345678, contact_style:email, contact_context:admin2@abc.com] # 透過order by做排序 >>> user_query.contacts.order_by(Contact.contact_style).all() [contact_style:email, contact_context:admin2@abc.com, contact_style:phone, contact_context:3345678] >>> user_query.contacts.order_by(Contact.id).all() [contact_style:phone, contact_context:3345678, contact_style:email, contact_context:admin2@abc.com] # 透過filter做過濾條件 >>> user_query.contacts.filter_by(contact_style='email').all() [contact_style:email, contact_context:admin2@abc.com]

範例_一對一

兩個Model之間的關聯如果是一對一的話,宣告方式與一對多相同,只需要在主要Model的db.relationship中加入參數uselist=False就可以,相同於一對多,也可以利用backref在主要Model中設置uselist

class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) # 一對一,設置關聯於主要model contacts = db.relationship('Contact', backref=backref('user', uselist=False)) def __repr__(self): return '<User %r>' % self.username class Contact(db.Model): __tablename__ = 'contacts' id = db.Column(db.Integer, primary_key=True) contact_style = db.Column(db.String) contact_context = db.Column(db.String) # 於次要model設置foreign key user_id = db.Column(db.Integer, db.ForeignKey('users.id')) def __repr__(self): return 'contact_style:%s, contact_context:%s' % \ (self.contact_style, self.contact_context)

範例_多對多

多對多關聯使用情境,舉例來說:

  1. 一篇文章可以有多個分類,一個分類可以存在於多篇文章中,這是一個tag設置的問題
  2. 一個使用者可以有多個聯絡方式,而一個聯絡方式可能對應到多個人

SQLAlchemy中設置多對多我們並不需要去特別的定義一個Model來做中繼,而是透過Table,設罝MetaData記錄兩個Model的ForeignKey,再於db.relationship加入參數secondary來設置關聯表,因為我們是利用flask-sqlalchemy來操作資料庫,所以設置上可以參考flask-sqlalchemy的範例

flask-sqlalchemy強烈建議,以真正的資料表來記錄多對多的關聯

相關建置說明如下註解:

# 設置中繼的關聯表 # flask-sqlalchemy會自動的在資料庫中產生相對應的table relations = db.Table('relations', db.Column('user_id', db.Integer, db.ForeignKey('users.id')), db.Column('contact_id', db.Integer, db.ForeignKey('contacts.id')) ) class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) # 利用secondary設置關聯中繼表 # lazy的部份可以依需求設置為動態與否 contacts = db.relationship('Contact', secondary=relations, lazy='subquery', backref=db.backref('user', lazy=True)) def __repr__(self): return '<User %r>' % self.username class Contact(db.Model): __tablename__ = 'contacts' id = db.Column(db.Integer, primary_key=True) contact_style = db.Column(db.String) contact_context = db.Column(db.String) # 不需要設置foreign key # user_id = db.Column(db.Integer, db.ForeignKey('users.id')) def __repr__(self): return 'contact_style:%s, contact_context:%s' % \ (self.contact_style, self.contact_context)

在利用Python Shell產生資料庫之後,我們來看一下資料庫的狀態,如下圖:

可以看到,雖然我們沒有利用Model來設置table,但SQLAlchemy會依db.Table的設置來產生中繼表,接著產生資料看看它的存取狀態為何,如下:

# 我們先import設置的model from app import User from app import Contact # 增加一個使用者為admin user = User(username='admin',email='admin@abc.com') # 增加一個聯絡人 contact = Contact(contact_style='mobile', contact_context='3345678') # 將聯絡人寫入使用者 user.contacts.append(contact) # 將資料寫入 db.session.add(user) db.session.commit()

首先確認資料庫的記錄狀態,可以發現資料庫的中繼表確實的記錄的雙邊的關聯,如下圖:

現在,手動再新增一個使用者,並且套用相同的聯絡人看看,如下:

user = User(username='Cust', email='Cust@abc.com') contact = Contact.query.filter_by(id=1).first() user.contacts.append(contact) db.session.add(user) db.session.commit()

資料庫的變化狀況如下:

接著測試搜尋Contact,結果如下,直接取下文字貼上:

>>> contact = Contact.query.filter_by(id=1).first() >>> contact contact_style:mobile, contact_context:3345678 >>> contact.user [<User 'admin'>, <User 'Cust'>]

利用Contact回查user會依我們設置的relation lazy參數來回傳,因為我們設置lazy=True,意義等同於lazy=select,如果我們希望在必要時候再透過filter來取值的話,那就記得使用lazy=dynamic,這時候就會如下方一般的回傳物件:

>>> contact.user <sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x030DF790>

最後,測試一下刪除,如果我們將聯絡人刪除,這時候的資料庫狀況為何?

>>> db.session.delete(contact) >>> db.session.commit()


這時候的中繼表相關資料也被刪除了,如果不想要這樣連動刪除資料的話可以透過參數設置來避免。

sqlalchemy的官方說明,雖然說我們使用的是flask-sqlalchemy,但本質上還是小調整之後可以完成的。

其它

多筆寫入

多筆寫入的情況下可以透過db.session.add_all([...list...])的方式來寫入,如果是大量資料要寫入的話,可以利用bulk,這部份也可以查詢SQLAlchemy官方文件

總結

幾個關聯方式練習下來相信對SQLAlchemy都有了初步的瞭解,這已經足夠我們建置系統使用了,不足的在有需求的時候查詢官方文件相信是會有相對應的答案。

Flask-SQLAlchemy_初探Flask實作_ext_01_Flask-SQLAlchemy_初探
Flask-SQLAlchemy_QueryFlask實作_ext_18_Flask-SQLAlchemy_Query

延伸閱讀

tag設置並非本篇的主旨,但是tag並非一定要設置為多對多,也可以設置一個字串欄位來保存,這邊提供二篇用於mysql中的設置說明,讓人驚訝的是,在tag愈見增長的時候,多對多的效能反而是較不好的。
設置說明
效能測試