深究SQLAlchemy中的表關係 Table Relationships

SQL中的表關係一直是比較難理解的地方。一樣SQLAlchemy也對他們作了實現,若是對SQL中的表關係理解透徹的話,這裏也能夠更容易理解。html

爲何須要定義Relationships

在相關聯的表中,咱們能夠不建立表關聯的定義,而只是單純互相引用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

它可讓咱們只在表中聲明表之間的關係,以後每次使用就徹底無需手動交叉搜索,而是像對待一個表中的數據同樣直接使用。數據庫

爲何不須要定義relationships?

通過實踐返回來加的這一節:實踐中的SQLAlchemy的"relationship"在必定程度上反而致使了總體表關聯關係的極大複雜化,還有效率的極其低下。session

若是你的數據庫只有兩個表的話,那麼relationship隨便定義隨便用。若是隻有幾百條數據的話,那麼也請隨便玩。app

可是,當數據庫中有數十個表以上,單個關聯層級就多過三個表以上層層關聯,並且各個數據量以萬爲單位。那麼,"relationship"會把整我的都搞垮,簡直還不如手寫SQL語句清晰好理解,而且效率也差在了秒級與毫秒級的區別上。函數

SQLAlchemy只能很輕鬆handle Many to Many,可是若是是常見的 Many to Many to Many,或者是 Many to Many to Many to Many,那簡直就是噩夢。

可是,咱們都知道,項目作到必定程度,都會擺脫不了ORM。不管是本身造輪子仍是用別人的,不管起點是否是純SQL,終點都是ORM。
那麼該怎麼辦呢?ui

網友的建議是:
用SQLAlchemy創建各類ORM類對象,不要用內置的關聯,直接在查詢的時候手動SQL語句!spa

通過實踐,個人建議是:插件

  • 容易SQL-Injection注入的地方,用SQLAlchemy的query
  • 建立ORM對象時候,用SQLAlchemy
  • 多層關聯的時候,不要用SQLAlchemy
  • 查詢的時候,用SQL
  • 插入數據的時候,不要用SQLAlchemy。(官方都說明了插入百萬級的時候,和SQL插件是秒級的)

relationship() 函數

參考官方文檔:Linking Relationships with Backref

SQLAlchemy建立表關聯時,使用的是relationshi()這個函數。
它返回的是一個類的屬性,好比father類的children屬性。可是,它實際上並無在father表中建立任何叫children的列,而是自動幫你到相關聯的children表中去找數據,讓你用起來感受沒有差異而已。
這是很是方便的!

relationship()這個函數的參數很是多,每個參數都有不少內容須要理解。由於全部的表關聯的形態,都是在這個函數裏面定義的。
如下分別講解。

Reference 正向引用

傳統的方法,是在父類中定義一個關係 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了。

Back Reference 反向引用

單純定義的relationship('子類名')只是一個正向引用,也就是隻能讓父類調用子對象。反過來,若是要問children他們的父親是誰,就不行了。

因此,咱們還須要一個反向引用 (Back Reference)的聲明,讓子對象可以知道父對象是誰。

定義方式是在父類的relationship(..)中加一個參數backref

class Father(..): 
    children = relationship( 'Child', backref='parent' )

注意:

  1. backref參數裏面使用的隨便寫,主要用於以後子類的引用。
  2. backref參數是雙向性的,意思是,只須要在父類中聲明一次,那麼父⇄子的雙向關係就確立了,不用再去子類中寫一遍。

這時候,咱們在添加就能夠這樣互相調用了:

>>> Jason = Child()
>>> print( Jason.parent )
 <__main__.Father object at 0x10222f860>

Bidirectional & Unidirectional Back Reference 雙向和單向的反向引用

後來,SQLAlchemy發現這種只在一邊定義雙向性backref的方法有點不太直觀,因此又添加了另外一個參數back_populates參數,而這個back_populates參數是單向性的,也就是說:
你要確立雙方向關係就必須在兩邊的類中都聲明一遍。這樣比較直觀。

能夠把 backrefback_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要求父類子類的關係名稱必須嚴格「對稱」:

  • 父類的relationship屬性名children,必須對應子類的關係中的back_populates中的值
  • 子類的relationship屬性名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)的方式添加父對象關聯。

SQL中的表關係

對應關係:

  • One to Many 一對多:
  • Many to One 多對一:
  • Many to Many 多對多:

One to Many 一對多

創建一個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對象。

Many to One 多對一

好比職工和公司的關係就是多對一。這和公司與職工對一對多有什麼區別?
區別實際上是在SQL語句中的:多對一的關聯關係,是在多的一方的表中定義,一的一方表中沒有任何關係定義:

class Company(...):
    id = Column(...)

class Employee(..):
    id = Column(...)
    company_id = Column( ..., ForeignKey('company.id') )
    company = relationship("Company")

Many to Many 多對多

多對多的關係也很常見,好比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]

Many to Many to Many 多對多對多 (深層關聯)

深層關聯,爲了不理解困難,最笨的方法就是簡單的使用外鍵ID,而後手動搜索另外一個表的對應ID。
image

(本篇未完待續)

相關文章
相關標籤/搜索