SQL中的表關係一直是比較難理解的地方。一樣SQLAlchemy也對他們作了實現,若是對SQL中的表關係理解透徹的話,這裏也能夠更容易理解。html
在相關聯的表中,咱們能夠不建立表關聯的定義,而只是單純互相引用id便可。可是,查詢和使用起來就要麻煩不少:ios
#給定參數User.name,獲取該user的addresses # 參考知乎:https://www.zhihu.com/question/38456789/answer/90470689 def get_addresses_from_user(user_name): user = session.query(User).filter_by(name=user_name).first() addresses = session.query(Address).filter_by(user_id=user.id).all() return addresses
能夠看到,這樣的效率很是低。
好在原生的SQL就有relationship設置,SQLAlchemy將其引入到了ORM模型中。sql
它可讓咱們只在表中聲明表之間的關係,以後每次使用就徹底無需手動交叉搜索
,而是像對待一個表中的數據同樣直接使用。數據庫
通過實踐返回來加的這一節:實踐中的SQLAlchemy的"relationship"在必定程度上反而致使了總體表關聯關係的極大複雜化,還有效率的極其低下。session
若是你的數據庫只有兩個表的話,那麼relationship隨便定義隨便用。若是隻有幾百條數據的話,那麼也請隨便玩。app
可是,當數據庫中有數十個表以上,單個關聯層級就多過三個表以上層層關聯,並且各個數據量以萬爲單位。那麼,"relationship"會把整我的都搞垮,簡直還不如手寫SQL語句清晰好理解,而且效率也差在了秒級與毫秒級的區別上。函數
SQLAlchemy只能很輕鬆handleMany to Many
,可是若是是常見的Many to Many to Many
,或者是Many to Many to Many to Many
,那簡直就是噩夢。
可是,咱們都知道,項目作到必定程度,都會擺脫不了ORM。不管是本身造輪子仍是用別人的,不管起點是否是純SQL,終點都是ORM。
那麼該怎麼辦呢?ui
網友的建議是:
用SQLAlchemy創建各類ORM類對象,不要用內置的關聯,直接在查詢的時候手動SQL語句!spa
通過實踐,個人建議是:插件
query
參考官方文檔:Linking Relationships with Backref
SQLAlchemy建立表關聯時,使用的是relationshi()
這個函數。
它返回的是一個類的屬性,好比father類的children
屬性。可是,它實際上並無在father表中建立任何叫children的列,而是自動幫你到相關聯的children表中去找數據,讓你用起來感受沒有差異而已。
這是很是方便的!
relationship()
這個函數的參數很是多,每個參數都有不少內容須要理解。由於全部的表關聯的形態,都是在這個函數裏面定義的。
如下分別講解。
傳統的方法,是在父類中定義一個關係 relationship
或叫正向引用 Reference
,子類只需定義一個外鍵。好比:
class Father(..): id = Column(..) children = relationship('Child') class Child(..): father_id = Column( Integer, ForeignKey('father.id') ) # 添加數據 daddy = Father() jason = Child() emma = Child() # 將孩子掛到父親名下 daddy.children.append(jason) daddy.children.append(emma)
這樣當每次咱們使用father.children
的時候,就會自動返回與這個father相關聯的全部children了。
單純定義的relationship('子類名')
只是一個正向引用,也就是隻能讓父類調用子對象。反過來,若是要問children他們的父親是誰,就不行了。
因此,咱們還須要一個反向引用 (Back Reference)
的聲明,讓子對象可以知道父對象是誰。
定義方式是在父類的relationship(..)中加一個參數backref
:
class Father(..): children = relationship( 'Child', backref='parent' )
注意:
雙向性
的,意思是,只須要在父類中聲明一次,那麼父⇄子
的雙向關係就確立了,不用再去子類中寫一遍。這時候,咱們在添加就能夠這樣互相調用了:
>>> Jason = Child() >>> print( Jason.parent ) <__main__.Father object at 0x10222f860>
後來,SQLAlchemy發現這種只在一邊定義雙向性backref
的方法有點不太直觀,因此又添加了另外一個參數back_populates
參數,而這個back_populates參數是單向性的,也就是說:
你要確立雙方向關係就必須在兩邊的類中都聲明一遍。這樣比較直觀。
能夠把backref
和back_populates
都讀爲"as",這樣就好記憶了。
好比:
class Father(..): id = Column(..) children = relationship( 'Child', back_populates='parent' ) class Child(..): father_id = Column( Integer, ForeignKey('father.id') ) parent = relationship( 'Father', back_populates='children' )
注意:back_populates
要求父類子類的關係名稱必須嚴格「對稱」:
children
,必須對應子類的關係中的back_populates
中的值parent
,必須對應父類的關係中的back_populates
中的值這樣一來利用反向引用
參數建立的關係就確立了。可是注意,
不管用backref
仍是back_populates
建立的關聯,若是咱們必需要爲父子對象添加對象間的關聯才能引用,不然誰也不知道誰是誰的父親、兒子:
>>> daddy = Father() >>> son = Child() >>> daughter = Child() >>> daddy.children [] >>> son.parent None >>> daddy.children.append( son ) >>> daddy.children.append( daughter ) >>> daddy.children [ <Child ...>, <Child ...> ] >>> son.parent <Father ...>
另外:上面添加父子關係的時候,不光能夠用daddy.children.append
,
還能夠在聲明子對象的時候肯定:son = Child( parent=daddy )
反向引用
參數對比:
backref
參數:雙方向。在父類中定義便可。只能經過daddy.children.append()
方式添加子對象關聯。back_populates
參數:單方向。必須在父子類中都定義,且屬性名稱必須嚴格對稱。還能夠經過Child(parent=daddy)
的方式添加父對象關聯。對應關係:
創建一個One-to-Many
的多表關聯:
# ... class Person(Base): id = Column(...) name = Column(...) pets = relationship('Pet', backref='owner') # 上面這句是添加一關聯,而不是實際的列 # 注意:1. 'Pet'是大寫開頭,由於指向了Python類,而不是數據庫中表 # 2. backref是指創建一個不存在於數據庫的「假列」, # 用於添加數據時候指認關聯對象,代替傳統id指定 class Pet(Base): id = Column(...) name = Column(...) owner_id = Column(Integer, ForeignKey('person.id') # 上面這句添加了一個外鍵, # 注意外鍵的'person'是數據庫中的表名,而不是class類名,因此用小寫以區分
建立好關聯的表之後,咱們就能夠直接插入數據了。注意,插入帶關聯的數據也和SQL插入有些不一樣:
#... # 添加主人 andy = Person(name='Andrew') session.add( andy ) seession.commit() # 添加狗 pp01 = Pet(name='Puppy', owner=andy) pp02 = Pet(name='Puppy', owner=andy) # 注意這句話中,owner是剛纔主表中註冊relationship中的backref指定的參數名, # 傳給owner的是主表的一個Python實例化對象,而不是什麼id # 看起來複雜,實際上sqlalchemy能夠自動取出object的id而後匹配副表中的foreignkey。 session.add(pp01) session.add(pp02) session.commit() print( andy.pets ) # >>> [<Pet 1>, <Pet, 2>] # 返回的是兩個Pet對象 print( pp01.owner ) # >>> <Person 'Andrew'> # 一樣,副表中利用owner這個backref定義的假列,返回的是Person對象。
好比職工和公司的關係就是多對一。這和公司與職工對一對多有什麼區別?
區別實際上是在SQL語句中的:多對一的關聯關係,是在多的一方的表中定義,一的一方表中沒有任何關係定義:
class Company(...): id = Column(...) class Employee(..): id = Column(...) company_id = Column( ..., ForeignKey('company.id') ) company = relationship("Company")
多對多的關係也很常見,好比User和Radio的關係:
一個Radio能夠有多個用戶能夠訂閱,一個用戶能夠訂閱多個Radio。
SQL中處理多對多的關係時,是把多對多分拆成兩個一對多關係。作法是:新建立一個表,專門存儲映射關係。本來的兩個表無需設置任何外鍵。
SQLAlchemy的實踐中,也和SQL中的作法同樣。
注意:既然有了專門的Mapping映射表,那麼兩個表各自就不須要註冊任何ForeignKey外鍵了。
示例:
# 作出一個專門的表,存儲映射關係 # 注意:1. 這個表中兩個"id"都不是主鍵,由於是多對多的關係,因此兩者均可以有多條數據。 # 2. 映射表必須在前面定義,不然後面的類引用時,編譯器會找不到 radio_users = Table('radio_users', Base.metadata, Column('whatever_name1', Integer, ForeignKey('radios.id')), Column('whatever_name2', Integer, ForeignKey('users.id')) ) # 定義兩個ORM對象: class Radio(Base): __tablename__ = 'radios' rid = Column('id', Integer, primary_key=True) followers = relationship('User', secondary=radio_users, # `secondary`是專門用來指明映射表的 back_populates='subscriptions' # 這個值要對應另外一個類的屬性名 ) class User(Base): __tablename__ = 'users' uid = Column('id', Integer, primary_key=True) subscriptions = relationship('Radio', secondary=radio_users, back_populates='followers' # 這個值要對應另外一個類的屬性名 )
其中,secondary
是專門用來指明映射表的。
注意:多對多的時候咱們也能夠用backref
參數來添加互相引用。可是這種方法太不直觀了,容易產生混亂。因此這裏建議用back_populates
參數,在兩方都添加引用,表現一種平行地位,方便理解。
而後插入數據時候是這麼用:
r1 = Radio() r2 = Radio() r3 = Radio() u1 = User() u2 = User() u3 = User() # 添加對象間的關聯 r1.followers += [u1, u2, u3] # 反過來添加也同樣 u1.subscriptions += [r2, r3]
深層關聯,爲了不理解困難,最笨的方法就是簡單的使用外鍵ID,而後手動搜索另外一個表的對應ID。
(本篇未完待續)