# SQLAlchemy 2.0(09)Data Manipulation with the ORM
###### tags: `python` `sqlalchemy`
[官方文件](https://docs.sqlalchemy.org/en/20/tutorial/orm_data_manipulation.html)
如果你有注意到的話應該會發現到前面大多說明的是CORE的用法,那這邊開始就會來談談ORM的部份。官方建議在進入這邊之前可以先讀讀:
1. [Executing with an ORM Session](https://docs.sqlalchemy.org/en/20/tutorial/dbapi_transactions.html#tutorial-executing-orm-session)
2. [Using ORM Declarative Forms to Define Table Metadata](https://docs.sqlalchemy.org/en/20/tutorial/metadata.html#tutorial-orm-table-metadata)
3. [Selecting ORM Entities and Columns](https://docs.sqlalchemy.org/en/20/tutorial/data_select.html#tutorial-selecting-orm-entities)
這大致上就是官方教程的一部份,順著讀過來也有一定的觀念了,不用太擔心。
## Inserting Rows using the ORM Unit of Work pattern
當我們使用ORM的時候,[Session](https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session)就是做為建構Insert結構,然後在執行的交易記錄中把它們做為INSERT statements發送出去。我們要Session做的就是把object entires(就是要寫入的資料)加進去,然後要確保這些資料能夠在需要時候以**flush**的方式寫入資料庫。這樣的過程就是unit of work pattern。
### Instances of Classes represent Rows
在ORM的話,我們所建立的class、User與Address,不僅能夠拿來定義資料表,也可以直接做為操作使用,下面給出兩個範例:
```python=
squidward = User(name="squidward", fullname="Squidward Tentacles")
krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")
```
兩個object分別代表兩筆記錄,目前還沒有寫入資料庫,你可以發現鍵值(pk)的部份是None:
```python=
>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')
```
這時候的狀態是屬於『[transient](https://docs.sqlalchemy.org/en/20/glossary.html#term-transient)』,也就是非持久化或暫時的狀態,而且也跟Session八字沒一撇,毫無關聯。
下面我們就來利用`Session`處理後續的過程,這次的作法跟之前的`with`較不一樣:
```python=
session = Session(engine)
```
然後利用`Session.add`將預計寫入的資料[peding](https://docs.sqlalchemy.org/en/20/glossary.html#term-pending)進去:
```python=
session.add(squidward)
session.add(krabs)
```
這時候可以用`Session.new`來看一下這些pending進去的資料狀況:
```python=
>>> session.new
IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')])
```
可以發現,這時候跟資料庫還是沒有任何的關係,因為Session採用的是[unit of work](https://docs.sqlalchemy.org/en/20/glossary.html#term-unit-of-work)的方式,這意思就是異動會累積,然後在你有需要的時候才會實際跟資料庫有實際的交握,也就是flush:
```python=
session.flush()
```
```
BEGIN (implicit)
INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id
[... (insertmanyvalues) 1/2 (ordered; batch not supported)] ('squidward', 'Squidward Tentacles')
INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id
[insertmanyvalues 2/2 (ordered; batch not supported)] ('ehkrabs', 'Eugene H. Krabs')
```
要注意,這時候的transcation仍然是open的狀態,直到我們呼叫`Session.commit(), Session.rollback(), Session.close()`。不過也不是每次都要利用flush,直接的commit也是可以的。
在插入這兩個物件之後,它們的狀態就不再是transient,而是persistent。而且這時候這兩個物件的`id`也不再是None,有屬於它們的屬性值了:
```python=
>>> squidward.id
4
>>> krabs.id
5
```
### Getting Objects by Primary Key from the Identity Map
物件的primary key identity對於Session來說是非常重要的,因為物件就是用identity map跟記憶體中的這個識別值做連結的。那這個identity map指的就是把目前已經載入到記憶體的物件做個連結,靠的就是這個primary key identity。關於這部份我們可以用`Session.get()`來試一下:
```python=
>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')
```
值得注意的是,這個identity map所對應到的一定是一個唯一的物件:
```python=
>>> some_squidward is squidward
True
```
### Committing
完成整個交易記錄就可以把它提交:
```python=
>>> session.commit()
COMMIT
```
提交之後,我們處理的物件仍然還是在Session,這個狀態會一直到把這個Session關閉。
### Updating ORM Objects using the Unit of Work pattern
要怎麼利用ORM更新資料呢?我們可以利用`Session.exectue`來做一系列的搜尋:
```python=
sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one()
```
上面這個表達式說的是,從`User`去搜尋`name="sandy"`的資料:
```python=
BEGIN (implicit)
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('sandy',)
```
好,現在這個`sandy`代表的就是資料表中`id=2`的這筆記錄。
```python=
>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')
```
如果你去改變這個物件(`sandy`)的屬性,那Session就會追蹤這個變化,這個部份會在`Session.dirty`顯示:
```python=
>>> sandy.fullname = "Sandy Squirrel"
>>> sandy in session.dirty
True
```
現在,只要這個Session做一次flush,那就會更新資料庫的值。記得,在發出任何的SELECT之前,都會自動的發出flush,也就是autoflush。直接來搜尋一次,測試給你看:
```python=
>>> sandy_fullname = session.execute(select(User.fullname).where(User.id == 2)).scalar_one()
```
```python=
UPDATE user_account SET fullname=? WHERE user_account.id = ?
[...] ('Sandy Squirrel', 2)
SELECT user_account.fullname
FROM user_account
WHERE user_account.id = ?
[...] (2,)
```
我們沒有執行flush,只是重新的做一次搜尋,可以發現得到的sql statement存在update,你可以列印出來驗證:
```python=
>>> print(sandy_fullname)
Sandy Squirrel
```
因為已經flush了,自然不存在`Session.dirty`的清單中:
```python
>>> sandy in session.dirty
False
```
當然啦,我們的整個交易記錄還沒有確認,所以實際資料庫中,sandy的fullname仍然是`Cheeks`就是了。
### Deleting ORM Objects using the Unit of Work pattern
來吧,讓我們來完成整個演練吧,再來就是刪除,先從資料庫來筆資料:
```python=
>>> patrick = session.get(User, 3)
```
```python=
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 = ?
[...] (3,)
```
一樣的,你刪,但是沒有flush之前什麼事都是沒有發生的:
```python=
>>> session.delete(patrick)
```
這個物件仍然是存在Session中,一直到flush,一樣的,搜尋一下來觸發autoflush:
```python=
>>> session.execute(select(User).where(User.name == "patrick")).first()
```
```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
[...] (3,)
DELETE FROM user_account WHERE user_account.id = ?
[...] (3,)
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('patrick',)
```
應該可以注意到,這邊除了我們所執行的user_account之外,還有一個address的table也被執行,這是一個cascade(級聯),如果有對應的資料存在的話,也會一併的處理掉。
一樣的,目前的操作都是在Session,沒有commit就不會影響資料庫。
### Rolling Back
Session有一個方法`Session.rollback()`,總之就是把目前正進行中的處理都做一個ROLLBACK。像剛剛我們做的sandy的改名,只要你執行`Session.rollback()`,那這個改變就會會回滾了,不止它喔,目前所有Session相關的通通expire(失效):
```python=
>>> session.rollback()
ROLLBACK
```
從`sandy.__dict__`更仔細的來看這個失效過程:
```python=
>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}
```
這個`dict`裡面沒有東西,就只有一個SQLAlchemy的內部狀態,這就是失效的狀況,現在再來看一次`sandy.fullname`可以發現,一切都還原了:
```python=
>>> sandy.fullname
'Sandy Cheeks'
```
```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 = ?
[...] (2,)
```
可以發現,在確認`sandy.fullname`的時候重新做了一次select。這時候再來看一次`sandy.__dict__`:
```python=
>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>,
'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}
```
完整的資料庫記錄寫入`sandy.__dict__`了。那剛剛我們刪除掉已經不存在Session的物件`patrick`也回到session內:
```python=
>>> patrick in session
True
```
### Closing a Session
這次的操作我們並不是利用`with`讓整個session在一個上下文中執行,所以我們就必需要手動來關閉:
```python=
>>> session.close()
ROLLBACK
```
當我們執行`close`的時候會有幾件事發生:
* 把所有的連結資源(connection resources)通通釋放回到[連接池](https://terms.naer.edu.tw/detail/36ad5378edb6adc1f2f58ffcd5b8fc74/)(connection pool),然後會取消(rolling back)所有執行中的交易記錄,意思就是說,如果確定要釋放資源的話,那只要關閉就好,不用再特別的執行`Session.rollback()`
* 把所有Session內的物件通通刪光光,意思是說,剛剛上面我們所操作的物件,`sandy, partick, squidward`,通通都[detached](https://docs.sqlalchemy.org/en/20/glossary.html#term-detached)(啪,沒啦):
```python=
>>> squidward.name
Traceback (most recent call last):
...
sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x...> is not bound to a Session; attribute refresh operation cannot proceed
```
不過那些被分離(detached)的物件你還是可以用`Session.add()`來跟新的Session,或是原本的Session來重新關聯,這會重新建議它們跟資料庫之間的關聯:
```python=
>>> session.add(squidward)
>>> squidward.name
'squidward'
```
```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 = ?
[...] (4,)
```