Try   HackMD

SQLAlchemy 2.0(09)Data Manipulation with the ORM

tags: python sqlalchemy

官方文件

如果你有注意到的話應該會發現到前面大多說明的是CORE的用法,那這邊開始就會來談談ORM的部份。官方建議在進入這邊之前可以先讀讀:

  1. Executing with an ORM Session
  2. Using ORM Declarative Forms to Define Table Metadata
  3. Selecting ORM Entities and Columns

這大致上就是官方教程的一部份,順著讀過來也有一定的觀念了,不用太擔心。

Inserting Rows using the ORM Unit of Work pattern

當我們使用ORM的時候,Session就是做為建構Insert結構,然後在執行的交易記錄中把它們做為INSERT statements發送出去。我們要Session做的就是把object entires(就是要寫入的資料)加進去,然後要確保這些資料能夠在需要時候以flush的方式寫入資料庫。這樣的過程就是unit of work pattern。

Instances of Classes represent Rows

在ORM的話,我們所建立的class、User與Address,不僅能夠拿來定義資料表,也可以直接做為操作使用,下面給出兩個範例:

squidward = User(name="squidward", fullname="Squidward Tentacles") krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")

兩個object分別代表兩筆記錄,目前還沒有寫入資料庫,你可以發現鍵值(pk)的部份是None:

>>> squidward User(id=None, name='squidward', fullname='Squidward Tentacles')

這時候的狀態是屬於『transient』,也就是非持久化或暫時的狀態,而且也跟Session八字沒一撇,毫無關聯。

下面我們就來利用Session處理後續的過程,這次的作法跟之前的with較不一樣:

session = Session(engine)

然後利用Session.add將預計寫入的資料peding進去:

session.add(squidward) session.add(krabs)

這時候可以用Session.new來看一下這些pending進去的資料狀況:

>>> session.new IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')])

可以發現,這時候跟資料庫還是沒有任何的關係,因為Session採用的是unit of work的方式,這意思就是異動會累積,然後在你有需要的時候才會實際跟資料庫有實際的交握,也就是flush:

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,有屬於它們的屬性值了:

>>> 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()來試一下:

>>> some_squidward = session.get(User, 4) >>> some_squidward User(id=4, name='squidward', fullname='Squidward Tentacles')

值得注意的是,這個identity map所對應到的一定是一個唯一的物件:

>>> some_squidward is squidward True

Committing

完成整個交易記錄就可以把它提交:

>>> session.commit() COMMIT

提交之後,我們處理的物件仍然還是在Session,這個狀態會一直到把這個Session關閉。

Updating ORM Objects using the Unit of Work pattern

要怎麼利用ORM更新資料呢?我們可以利用Session.exectue來做一系列的搜尋:

sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one()

上面這個表達式說的是,從User去搜尋name="sandy"的資料:

BEGIN (implicit) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [...] ('sandy',)

好,現在這個sandy代表的就是資料表中id=2的這筆記錄。

>>> sandy User(id=2, name='sandy', fullname='Sandy Cheeks')

如果你去改變這個物件(sandy)的屬性,那Session就會追蹤這個變化,這個部份會在Session.dirty顯示:

>>> sandy.fullname = "Sandy Squirrel" >>> sandy in session.dirty True

現在,只要這個Session做一次flush,那就會更新資料庫的值。記得,在發出任何的SELECT之前,都會自動的發出flush,也就是autoflush。直接來搜尋一次,測試給你看:

>>> sandy_fullname = session.execute(select(User.fullname).where(User.id == 2)).scalar_one()
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,你可以列印出來驗證:

>>> print(sandy_fullname) Sandy Squirrel

因為已經flush了,自然不存在Session.dirty的清單中:

>>> sandy in session.dirty
False

當然啦,我們的整個交易記錄還沒有確認,所以實際資料庫中,sandy的fullname仍然是Cheeks就是了。

Deleting ORM Objects using the Unit of Work pattern

來吧,讓我們來完成整個演練吧,再來就是刪除,先從資料庫來筆資料:

>>> patrick = session.get(User, 3)
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之前什麼事都是沒有發生的:

>>> session.delete(patrick)

這個物件仍然是存在Session中,一直到flush,一樣的,搜尋一下來觸發autoflush:

>>> session.execute(select(User).where(User.name == "patrick")).first()
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(失效):

>>> session.rollback() ROLLBACK

sandy.__dict__更仔細的來看這個失效過程:

>>> sandy.__dict__ {'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}

這個dict裡面沒有東西,就只有一個SQLAlchemy的內部狀態,這就是失效的狀況,現在再來看一次sandy.fullname可以發現,一切都還原了:

>>> sandy.fullname 'Sandy Cheeks'
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__

>>> sandy.__dict__ {'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>, 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}

完整的資料庫記錄寫入sandy.__dict__了。那剛剛我們刪除掉已經不存在Session的物件patrick也回到session內:

>>> patrick in session True

Closing a Session

這次的操作我們並不是利用with讓整個session在一個上下文中執行,所以我們就必需要手動來關閉:

>>> session.close() ROLLBACK

當我們執行close的時候會有幾件事發生:

  • 把所有的連結資源(connection resources)通通釋放回到連接池(connection pool),然後會取消(rolling back)所有執行中的交易記錄,意思就是說,如果確定要釋放資源的話,那只要關閉就好,不用再特別的執行Session.rollback()
  • 把所有Session內的物件通通刪光光,意思是說,剛剛上面我們所操作的物件,sandy, partick, squidward,通通都detached(啪,沒啦):
>>> 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來重新關聯,這會重新建議它們跟資料庫之間的關聯:

>>> session.add(squidward) >>> squidward.name 'squidward'
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,)