映射在第五步,我們還是一步一步來哈
一. 關系介紹
舉一個比較經典的關系,部門與員工(以下是我的需求情況,算是把該有的關系都涉及到了)
1.每個部門會有很多成員(這里排除一個成員屬於多個部門的情況) ---> 一對多
2.每個部門都有一個負責人 ---> 多對一
3.每個部門可能有一個上級部門 ---> 自關聯多對一
4.每個員工都有一個主屬部門 ---> 多對一
5.每個員工可能有很多附屬部門 ---> 多對多
6.每個員工可能有很多上級員工 ---> 自關聯多對多
二. 設計部門與成員表模型
直接附上模型表代碼,但是不包含關系映射(單獨寫一步)
from sqlalchemy.ext.declarative import declarative_base BaseModel = declarative_base() # 創建模型對象的基類
# 部門
class Department(BaseModel): __tablename__ = 'dep' id = Column(Integer(), primary_key=True, autoincrement=True) name = Column(String(30),nullable=False,unique=True) # 部門名稱
staff_id = Column(Integer(),ForeignKey('staff.id')) # 負責人
up_dep_id = Column(Integer(),ForeignKey('dep.id')) # 上級部門----自關聯
def __repr__(self): # 方便打印查看
return '<Department %s>' % self.name # 員工
class Staff(BaseModel): __tablename__ = 'staff' id = Column(Integer(), primary_key=True, autoincrement=True) name = Column(String(30),nullable=False,unique=True) # 員工名稱
main_dep_id = Column(Integer(),ForeignKey('dep.id')) # 主要部門
def __repr__(self): return '<Staff %s>' % self.name
三. 設計第三方表模型--附屬部門與上級員工
建立多對多關系映射時secondary參數需要指定一個第三方表模型對象,但不是自己寫的Class哦,而是一個Table對象
from sqlalchemy import Table # 使用Table專門生成第三方表模型
# 第三方表--附屬部門
aux_dep_table = Table('staff_aux_dep',BaseModel.metadata, Column('id',Integer(),primary_key=True, autoincrement=True), Column('dep_id', Integer(), ForeignKey('dep.id',ondelete='CASCADE')), Column('staff_id', Integer(), ForeignKey('staff.id',ondelete='CASCADE')) ) # 第三方表--上級員工
up_obj_table = Table('staff_up_obj',BaseModel.metadata, Column('id',Integer(),primary_key=True, autoincrement=True), Column('up_staff_id', Integer(), ForeignKey('staff.id',ondelete='CASCADE')), Column('down_staff_id', Integer(), ForeignKey('staff.id',ondelete='CASCADE')) )
四. 生成表
from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker engine = create_engine('mysql+pymysql://root:123456@localhost:3306/test?charset=utf8') # 關聯數據庫
DBSession = sessionmaker(engine) # 創建DBSession類
session = DBSession() # 創建session對象
BaseModel.metadata.create_all(engine) # 數據庫生成表
五. relationship--關系映射
簡單來說, relationship函數是sqlalchemy對關系之間提供的一種便利的調用方式, relationship的返回值賦給的變量名為正向調用的屬性名,綁定給當前表模型類中,而backref參數則是指定反向調用的屬性名,綁定給關聯的表模型類中,如下部門表中Department.staffs就為正向,Staff.main_dep就為反向。
先導入relationship
from sqlalchemy.orm import relationship
先映射部門表,需要注意的是:
1. 是由於部門與員工之間有多重外鍵約束,在多對一或一對多關系相互映射時需要用foreign_keys指定映射的具體字段
2. 自關聯多對一或一對多時候,需要用remote_side參數指定‘一’的一方,值為一個Colmun對象(必須唯一)
# 部門
class Department(BaseModel): __tablename__ = 'dep' id = Column(Integer(), primary_key=True, autoincrement=True) name = Column(String(30), nullable=False, unique=True) # 部門名稱
staff_id = Column(Integer(), ForeignKey('staff.id')) # 負責人
up_dep_id = Column(Integer(), ForeignKey('dep.id')) # 上級部門----自關聯
# 主屬部門為此部門的所有員工,反向Staff實例.main_dep獲取員工的主屬部門
main_staffs = relationship('Staff', backref='main_dep', foreign_keys='Staff.main_dep_id') # 部門的直屬上級部門,反向Department實例.down_deps,獲取部門的所有直屬下級部門(自關聯多對一需用remote_side=id指定‘一’的一方)
up_dep = relationship('Department', backref='down_deps', remote_side=id)
重新生成數據(當調用屬性映射的為‘多’的一方,則代表的是一個InstrumentedList 類型的結果集,是List的子類,對這個集合的修改,就是修改外鍵關系)
staff_names = ['我','你','他','她','它'] dep_names = ['1部','2部','21部','22部'] staff_list = [Staff(name=name) for name in staff_names] dep_list = [Department(name=name) for name in dep_names] # 為所有員工初始分配一個主屬部門(按列表順序對應)
[dep.main_staffs.append(staff) for staff,dep in zip(staff_list,dep_list)] # 關聯上下級部門(2部的上級為1部,2部的下級為21、22部)
dep_list[1].up_dep = dep_list[0] dep_list[1].down_deps.extend(dep_list[2:]) session.add_all(staff_list + dep_list) session.commit() # 數據持久化到數據庫
部門表關系映射 查詢測試
dep_2 = session.query(Department).filter_by(name='2部').one() # 獲取 2部 部門
staff_me = session.query(Staff).filter(Staff.name=='我').one() # 獲取 ‘我’ 員工
print(dep_2.main_staffs) # 主屬部門為2部的所有員工
print(dep_2.up_dep) # 2部的上級部門
print(dep_2.down_deps) # 2部的所有直屬下級部門
print(staff_me.main_dep) # 員工‘我’ 的主屬部門
# [<Staff 你>] # <Department 1部> # [<Department 21部>, <Department 22部>] # <Department 1部>
映射員工表,需要注意的是:
1. 多對多關系中需要用secondary參數指定第三方表模型對象
2. 自關聯多對多需要用primaryjoin和secondaryjoin指定主副連接關系,查詢邏輯是根據主連接關系對應的第三方表的字段查詢(例,查詢id為10的員工的所有上級對象,就會在第三方表里查詢down_staff_id=10對應的所有數據,而把每條數據的up_staff_id值對應員工表的id查詢出來的對象集合,就為id為10的員工的所有上級對象了)
# 員工
class Staff(BaseModel): __tablename__ = 'staff' id = Column(Integer(), primary_key=True, autoincrement=True) name = Column(String(30), nullable=False, unique=True) # 員工名稱
main_dep_id = Column(Integer(), ForeignKey('dep.id')) # 主要部門
# 員工的所有附屬部門,反向為所有附屬部門為此部門的員工
aux_deps = relationship(Department, secondary=aux_dep_table,backref = 'aux_staffs') # 員工的所有直屬上級員工,反向為員工的所有直屬下級員工(自關聯多對多需要指定第三方表主連接關系與副連接關系,查詢邏輯是根據主連接關系對應的第三方表的字段查詢)
up_staffs = relationship('Staff', secondary=up_obj_table, primaryjoin = id == up_obj_table.c.down_staff_id, # 主連接關系 為 本表id字段對應第三方表的down_staff_id
secondaryjoin = id == up_obj_table.c.up_staff_id, # 副連接關系 為 本表id對應第三表的up_staff_id
backref = 'down_staffs')
建立數據映射關系
staff_all_list = session.query(Staff).all() # 獲取所有員工對象列表
dep_1 = session.query(Department).filter_by(name='1部').one() # 獲取 部門 1部
dep_2 = session.query(Department).filter_by(name='2部').one() # 獲取 部門 2部
staff_me = session.query(Staff).filter_by(name='我').one() # 獲取員工 ‘我’
dep_1.aux_staffs.extend(staff_all_list) # 分配所有員工的附屬部門都為1部
staff_me.aux_deps.append(dep_2) # 給員工‘我’額外分配附屬部門2部
staff_all_list.remove(staff_me) staff_me.down_staffs.extend(staff_all_list) # 將所有除員工‘我’以外的員工 都成為 員工‘我’的下級
session.commit()
員工表關系映射 查詢測試
staff_you = session.query(Staff).filter_by(name='你').one() # 獲取員工 你
print(dep_1.aux_staffs.all()) # 所有附屬部門為1部的員工
print(staff_me.aux_deps.all()) # 員工‘我’ 的所有附屬部門
print(staff_me.down_staffs.all()) # 員工‘我’ 的所有直屬下級員工
print(staff_you.up_staffs.all()) # 員工‘你’ 的所有直屬上級員工
# [<Staff 我>, <Staff 你>, <Staff 他>, <Staff 她>, <Staff 它>] # [<Department 1部>] # [<Staff 你>, <Staff 他>, <Staff 她>, <Staff 它>] # [<Staff 我>]
如果我們想根據調用屬性獲得的集合,再進行篩選,可以嗎?
模擬個場景,我已經知道了員工‘我’有一個附屬部門叫‘1部’了,我想知道除了‘1部’的其他附屬部門,如下:
from sqlalchemy import not_ staff_me.aux_deps.filter(not_(Department.name=='1部')) # AttributeError: 'InstrumentedList' object has no attribute 'filter'
報錯了,也是,之前都說了,調用屬性映射的為‘多’的一方代表的是一個InstrumentedList 對象,只有為Query對象才能有filter、all、one等的方法。
那么該怎么轉換成Query對象呢,通過查看relationship的參數,發現了lazy,通過改變它的值可以在查詢時可以得到不同的結果,他有很多值可以選擇:
1. 默認值為select, 他直接會導出所有的結果對象合成一個列表
aux_deps = relationship(Department, secondary=aux_dep_table, backref='aux_staffs', lazy='select') print(type(staff_me.aux_deps)) print(staff_me.aux_deps) # <class 'sqlalchemy.orm.collections.InstrumentedList'> # [<Department 1部>, <Department 2部>]
2. dynamic,他會生成一個繼承與Query的AppenderQuery對象,可以用於繼續做過濾操作,這就是我們模擬的場景想要的,需要注意的是,如果調用屬性映射的不是‘多’而是‘一’的一方,那么就會報錯(雖然為默認值select沒有影響,但本來就一個結果,就不要加lazy參數了)
aux_deps = relationship(Department, secondary=aux_dep_table, backref='aux_staffs', lazy='dynamic') print(type(staff_me.aux_deps)) print(staff_me.aux_deps.all()) # 獲取員工‘我’的所有附屬部門
print(staff_me.aux_deps.filter(not_(Department.name == '1部')).all()) # 獲取員工‘我’除1部以外的所有附屬部門
# <class 'sqlalchemy.orm.dynamic.AppenderQuery'> # [<Department 1部>, <Department 2部>] # [ <Department 2部>]
3. 其他的還有很多參數,例如joined,連接查詢,使用不當會有性能問題,以下為谷歌翻譯的,大家有興趣可以去看看原文https://docs.sqlalchemy.org/en/latest/orm/relationship_api.html,ctrl+f搜lazy
我們現在知道當需要對映射的結果集繼續篩選的時候,可以在relationship指定lazy參數為'dynamic',但是在這里加好像只是正向調用的時候有效,反向還是為InstrumentedList 對象
dep_1 = session.query(Department).filter_by(name='1部').one() # 獲取 部門 1部
print(dep_1.aux_staffs.all()) # 獲取所有附屬部門為1部的員工
# AttributeError: 'InstrumentedList' object has no attribute 'all'
如果反向的時候我們該加在哪里呢?其實backref參數也可以接受一個元祖,里面只能是兩個參數,一個跟之前一樣是個字符串,為反向調用的屬性名,另一個就是一個加關於反向調用時的參數(dict對象)
aux_deps = relationship(Department, secondary=aux_dep_table, backref=('aux_staffs',{'lazy':'dynamic'}), lazy='dynamic') print(type(dep_1.aux_staffs.all())) print(dep_1.aux_staffs.all()) # 獲取所有附屬部門為1部的員工
# <class 'sqlalchemy.orm.dynamic.AppenderQuery'> # [<Staff 我>, <Staff 你>, <Staff 他>, <Staff 她>, <Staff 它>]
掌握了如上,之后使用SQLALchemy在工作中遇到的奇葩關系應該都能處理了吧,我語言組織能力比較弱,有什么錯誤還請指出----1738268742@qq.com
