# 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)則是指向互向的屬性名稱:

後面會有說明,不急。
## 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')
```