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
,透過關聯設置讓人家知道Contact
跟User
相愛,兩個人之間恩愛透過backref
來連接關係。(也可以透過back_populates
設置)不過實際上這是一個在雙方都設置監聽器的作法,透過db.relationship
讓SQLAlchemy
知道Contact
跟User
是有關聯的,但前提是必需設置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
屬性設置關聯之後寫入資料庫。
在建立好User
跟Contact
的關聯之後,我們嚐試著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)
多對多關聯使用情境,舉例來說:
在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_Query:Flask實作_ext_18_Flask-SQLAlchemy_Query
tag設置並非本篇的主旨,但是tag並非一定要設置為多對多,也可以設置一個字串欄位來保存,這邊提供二篇用於mysql中的設置說明,讓人驚訝的是,在tag愈見增長的時候,多對多的效能反而是較不好的。
設置說明
效能測試