# 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,) ```