# SQLAlchemy 2.0(10)Working with ORM Related Objects(01) ###### tags: `python` `sqlalchemy` [官方文件](https://docs.sqlalchemy.org/en/20/tutorial/orm_related_objects.html) ## Working with ORM Related Objects 這邊要介紹的是ORM中很重要的概念,也就是跟其它物件之間的關聯。在[Declaring Mapped Classes](https://docs.sqlalchemy.org/en/20/tutorial/metadata.html#tutorial-declaring-mapped-classes)有個範例是用[relationship()](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship)來完成映射。這個構造定義兩個mapped classes之間的連接,或者是跟自己本身的連接(又稱為self-referential relationship)。 用一個簡單的範例來說說`relationship`的基本概念: ```python= from sqlalchemy.orm import Mapped from sqlalchemy.orm import relationship class User(Base): __tablename__ = "user_account" # ... mapped_column() mappings addresses: Mapped[List["Address"]] = relationship(back_populates="user") class Address(Base): __tablename__ = "address" # ... mapped_column() mappings user: Mapped["User"] = relationship(back_populates="addresses") ``` 根據上面的範例,現在`User`有一個`addresses`的屬性,而`Address`有一個`user`的屬性。`relationship()`做的就是把`User`、`Address`兩個類別所建構出來的的table關聯起來,不過前題是它們兩之間有設置好ForeignKey,而且從定義來看我們也可以清楚確定`User->Address`是一種一對多(`User.addresses`)的關聯。 一對多的關聯反過來就是多對一,所以`Address.user`就是那個『一』。參數[relationship.back_populates](https://docs.sqlalchemy.org/en/20/orm/relationship_api.html#sqlalchemy.orm.relationship.params.back_populates)則是指向互向的屬性名稱: ![](https://hackmd.io/_uploads/BJboHLWK3.png) 後面會有說明,不急。 ## Persisting and Loading Relationships 現在我們來看看剛剛的程式碼發生了什麼事,首先當我們建立一個新的`User`物件,當我們去查看屬性`.address`的時候會得到一個python list。: ```python= >>> u1 = User(name="pkrabs", fullname="Pearl Krabs") >>> u1.addresses [] ``` 這是一個SQLAlchemy特別訂製的list,可以跟蹤與響應其變更。即使裡面沒有東西也不會拋出異常。 目前的`u1`還是處於transient(暫時的狀態),那`u1.addresses`目前還沒有任何的變化,因為還沒有跟任何東西有關聯。下面來生成一個新的`Address`物件: ```python= >>> a1 = Address(email_address="pearl.krabs@gmail.com") ``` 然後用`list.append()`來寫入`u1.addresses`: ```python= >>> u1.addresses.append(a1) ``` 現在,裡面就有一筆資料了,雖然是廢話,還是要看一下: ```python= >>> u1.addresses [Address(id=None, email_address='pearl.krabs@gmail.com')] ``` 有趣的是,當我們把`a1`寫入`u1.addresses`跟`u1`做關聯之後,`a1.user`也有了反應了: ```python= >>> a1.user User(id=None, name='pkrabs', fullname='Pearl Krabs') ``` 這種同步的狀況當然就是因為我們設置的`relationship`裡面的參數`back_populates`的功勞。再來一次也是一樣: ```python= >>> a2 = Address(email_address="pearl@aol.com", user=u1) >>> u1.addresses [Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')] ``` 當然,因為是雙向的,所以我們也可以用assign的方式來設置: ```python= # equivalent effect as a2 = Address(user=u1) >>> a2.user = u1 ``` ## Cascading Objects into the Session 現在我們已經有`u1, a1, a2`,這在記憶體中是呈現雙向結構的關聯,不過還記得吧,現在的狀態是transient,跟資料庫是八字沒一撇的,該是Session出場的時候了: ```python= >>> session.add(u1) >>> u1 in session True >>> a1 in session True >>> a2 in session True ``` 我們嚐試`session.add(u1)`,但同時`u1`所關聯的`a1, a2`也存在於session之中,這是一種Cascading,這稱為save-update cascade。 現在這三個物件都是pending的狀態,已經準備好寫入資料庫了,只是準備好,所以相關的鍵值目前都是沒東西的: ```python= >>> print(u1.id) None >>> print(a1.user_id) None ``` 如果是傳統的sql statement,我們還要想著先寫入`u1`,然後再來處理`a1, a2`,因為我們必需先得到`u1`的pk value才能處理`a1, a2`,不過ORM的話不用想那麼多,只要跟花媽一樣Do-Re-Mi-So: ```python= >>> session.commit() ``` ```python= INSERT INTO user_account (name, fullname) VALUES (?, ?) [...] ('pkrabs', 'Pearl Krabs') INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id [... (insertmanyvalues) 1/2 (ordered; batch not supported)] ('pearl.krabs@gmail.com', 6) INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id [insertmanyvalues 2/2 (ordered; batch not supported)] ('pearl@aol.com', 6) COMMIT ``` ## Loading Relationships 記得,當我們執行commit之後,Session內的所有物件通通expire(失效)。當我們確認寫入的id時,就會先執行個SELECT: ```python= >>> u1.id 6 ``` ```python= BEGIN (implicit) SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname FROM user_account WHERE user_account.id = ? [...] (6,) ``` 根據我們剛剛的建立,這資料裡面應該是有著兩筆的住址記錄,當我們去訪問這個屬性的時候會看到ORM在這時候才會再去SELECT這兩筆的資料,這是一種[lazy load](https://docs.sqlalchemy.org/en/20/glossary.html#term-lazy-load): ```python= >>> u1.addresses [Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')] ``` ```python= SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id FROM address WHERE ? = address.user_id [...] (6,) ``` 在SELECT之後,這資料存在記憶體中已經是持久性的了,所以再去訪問該屬性也不會觸發SELECT: ```python= >>> u1.addresses [Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')] ``` 一樣的,這種lazy loading在download `u1.addresses`的兩筆資料之後,根據映射的設置,兩筆資料也被載入`a1, a2`: ```python= >>> a1 Address(id=4, email_address='pearl.krabs@gmail.com') >>> a2 Address(id=5, email_address='pearl@aol.com') ```