# Flask實作_ext_02_Flask-SQLAlchemy_關聯 ###### tags: `flask` `flask_ext` `python` `SQLAlchemy` :::danger 官方文件: * [Declaring Models](http://flask-sqlalchemy.pocoo.org/2.3/models/) * [Relationships-backref](http://docs.sqlalchemy.org/en/latest/orm/backref.html#relationships-backref) ::: 在[Flask實作_ext_01_Flask-SQLAchemy_初探](https://hackmd.io/s/SJ9x3N9zz)中,我們瞭解到如何透過類別的建置來產生表單,此篇主要討論資料表之間的關聯設置。 關聯式資料庫中,不外乎一對一、一對多、多對一與多對多的關聯方式,關聯的設置影響到我們對物件的操作,老話一句,需求推動功能,先瞭解基本,有進階需求的時候自然會尋找答案,另外,`flask-sqlalchemy`的官方文件對幾種關聯方式都有案例,如果臨時想不到如何設置的時候記得查詢官方文件<sub>如官方文件Declaring Models</sub>。 ## 範例 ### 範例_一對多 我們利用上一篇的範例來做調整,如下修正: ```python= 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) ``` :::info `SQLAlchemy`中,每個Model都要求要設置主鍵,名稱通常設置為id ::: 範例中設置了兩個類別,一個使用者(User),一個聯絡方式(Contact),在寫程式的時候我會特別註記好設置關聯的那一方是『一對多的一或一對多的多』,透過兩邊的設置讓`SQLAlchemy`瞭解關聯的關係。 在Python Shell中執行`db.create_all()`之後,可以透過`sqlite`圖形介面查看,已經確實的產生了兩個Table,如下圖: ![](https://i.imgur.com/YS7dhTH.png) ```python=24 contacts = db.relationship('Contact', backref='user') ``` `db.relationship`,透過關聯設置讓人家知道`Contact`跟`User`相愛,兩個人之間恩愛透過`backref`來連接關係。<sub>(也可以透過`back_populates`設置)</sub>不過實際上這是一個在雙方都設置監聽器的作法,透過`db.relationship`讓`SQLAlchemy`知道`Contact`跟`User`是有關聯的,但前提是必需設置`ForeignKey`,更深入的說明可參閱參考文件中`Relationships-backref` ```python=35 user_id = db.Column(db.Integer, db.ForeignKey('users.id')) ``` `db.ForeignKey`可很以直觀的以資料庫的角度去看,這代表我們在保存聯絡方式的時候會在資料庫內存儲使用者的ID`users.id` 透過Python Shell來`improt`設定的兩個資料類別,並新增使用者,範例如下: ```python= >>> 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,這是因為我們尚未建置任何關聯資料 現在我們來建立聯絡方式,範例如下: ```python= >>> 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` 到這邊,應該可以理解相關的設置邏輯,也可能很驚訝就這樣就已經完成了一個表單的關聯,而且是雙向的設置,這代表如果我們要清除資料的話,只需要刪除一邊即可,如下: ```python= >>> contact.user = None >>> admin.contacts [] ``` 上面的案例可以看的出來,將`contact.user`設置為`None`之後,`admin.contacts`也變空的`list`了,非常令人驚豔。 現在我們來看將資料正式的寫入資料庫的情況,如下: ```python= 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`也一併寫入資料庫。 #### 另一種關聯寫入資料的方式 ```python= >>> 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 在建立好`User`跟`Contact`的關聯之後,我們嚐試著`select`一筆使用者資料,如下: ```python= >>> 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`,調整如下: ```python= 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`在搜尋關聯資料的時候保留物件狀態,如下說明: ```python >>> user_query = User.query.filter_by(username='admin').first() >>> user_query.contacts() <sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x0000000003ACFC50> ``` 這時候回傳資料是一個`SQLAlchemy`物件類別,這樣就可以透過`filter`的方式再來做二次搜尋,或是透過`order_by`做排序,如下範例: ```python= # 透過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`。 ```python= 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的範例](http://flask-sqlalchemy.pocoo.org/2.3/models/#many-to-many-relationships)。 :::danger flask-sqlalchemy強烈建議,以真正的資料表來記錄多對多的關聯 ::: 相關建置說明如下註解: ```python= # 設置中繼的關聯表 # 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產生資料庫之後,我們來看一下資料庫的狀態,如下圖: ![](https://i.imgur.com/Fz2nTHZ.png) 可以看到,雖然我們沒有利用Model來設置`table`,但`SQLAlchemy`會依`db.Table`的設置來產生中繼表,接著產生資料看看它的存取狀態為何,如下: ```python= # 我們先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() ``` 首先確認資料庫的記錄狀態,可以發現資料庫的中繼表確實的記錄的雙邊的關聯,如下圖: ![](https://i.imgur.com/DS4NKPt.png) 現在,手動再新增一個使用者,並且套用相同的聯絡人看看,如下: ```python= 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() ``` 資料庫的變化狀況如下: ![](https://i.imgur.com/cAnxTwN.png) 接著測試搜尋`Contact`,結果如下,直接取下文字貼上: ```python= >>> 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`,這時候就會如下方一般的回傳物件: ```python= >>> contact.user <sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x030DF790> ``` 最後,測試一下刪除,如果我們將聯絡人刪除,這時候的資料庫狀況為何? ```python= >>> db.session.delete(contact) >>> db.session.commit() ``` ![](https://i.imgur.com/M02Km3p.png) 這時候的中繼表相關資料也被刪除了,如果不想要這樣連動刪除資料的話可以透過參數設置來避免。 [sqlalchemy的官方說明](http://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#association-object),雖然說我們使用的是`flask-sqlalchemy`,但本質上還是小調整之後可以完成的。 ## 其它 ### 多筆寫入 多筆寫入的情況下可以透過`db.session.add_all([...list...])`的方式來寫入,如果是大量資料要寫入的話,可以利用`bulk`,這部份也可以查詢`SQLAlchemy`官方文件 ## 總結 幾個關聯方式練習下來相信對`SQLAlchemy`都有了初步的瞭解,這已經足夠我們建置系統使用了,不足的在有需求的時候查詢官方文件相信是會有相對應的答案。 **Flask-SQLAlchemy_初探**:[Flask實作_ext_01_Flask-SQLAlchemy_初探](https://hackmd.io/s/SJ9x3N9zz) **Flask-SQLAlchemy_Query**:[Flask實作_ext_18_Flask-SQLAlchemy_Query](https://hackmd.io/s/rkFtMQg47) ## 延伸閱讀 tag設置並非本篇的主旨,但是tag並非一定要設置為多對多,也可以設置一個字串欄位來保存,這邊提供二篇用於mysql中的設置說明,讓人驚訝的是,在tag愈見增長的時候,多對多的效能反而是較不好的。 [設置說明](http://howto.philippkeller.com/2005/04/24/Tags-Database-schemas/) [效能測試](http://howto.philippkeller.com/2005/06/19/Tagsystems-performance-tests/)