SQLAlchemyでSession.commit()とSession.refresh()の使い方と挙動について



# Hello World!!
皆さま、はじめまして。初投稿になりますmasa-asaです。よろしくお願いいたします。

# なぜQiitaを始めたの？
私の経歴から説明します。
新卒でNECソリューションイノベータに入社し3年弱勤務→2024年1月から某消費財・化学メーカに転職し、主にクラウドに関するエンジニアとして勤務しています。もともとは現職の会社にエンジニアがいることをアピールする＋自身の知識のアウトプットをするという目的で始めました。よろしくお願いします！

※許可がいただけたら現職の会社名も出したいと思います！

# SQLAlchemyとは？
SQLAlchemyとはPythonでSQLを扱うためのツールです。一般的にORMapperと呼ばれるツールの1つで、Pythonでよく使われているものです。

# 本題

### SQlAlchemyでSession.commit()をする
以下のようなコードがあるとします。内容はデータベースにデータをinsertする単純なコードです。

```python
import sys
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine

from app.routers.chat_mode import Chat



engine=create_async_engine("postgresql+asyncpg://{ユーザ名}:{パスワード}@localhost:5432/postgres")
 
Session = sessionmaker(engine,class_=AsyncSession)

async def main():
    async with Session() as session:
        user = Chat()
        user.name = "Sato Taro"
        user.age = 30
        session.add(user)
        
        await session.commit()

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())
```

このコードを実行するとどのような結果になるでしょうか。以下に結果を示します。
データベースにレコードが追加されているのが分かります。ユーザーID、名前、年齢のレコードが1つ追加されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942439/39b638f1-c6f3-84f8-5a8d-e207993ec85c.png)


### SQlAlchemyでSession.commit()とSession.refresh()をする
次に以下のようなコードに修正します。`session.refresh()`の処理を追加し、
新しく追加したユーザーのIDを`print`しています。
```python
import sys
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine

from app.routers.chat_mode import Chat



engine=create_async_engine("postgresql+asyncpg://{ユーザ名}:{パスワード}@localhost:5432/postgres")
 
Session = sessionmaker(engine,class_=AsyncSession)

async def main():
    async with Session() as session:
        user = Chat()
        user.name = "Sato Taro"
        user.age = 30
        session.add(user)
        
        await session.commit()
        await session.refresh(user)

        print(user.user_id)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())
```
`print`の結果として以下のようなuuidが出力されます。
```
02e64f78-02ff-46e6-b09e-d6ca8763541f
```

### あれ、この場合は失敗するぞ？
次にコードを少し変えて、以下のようにしてみます。`print`を`session.commit()`と`session.refresh(user)`の間に挿入します。


```python
import sys
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine

from app.routers.chat_mode import Chat



engine=create_async_engine("postgresql+asyncpg://{ユーザ名}:{パスワード}@localhost:5432/postgres")
 
Session = sessionmaker(engine,class_=AsyncSession)

async def main():
    async with Session() as session:
        user = Chat()
        user.name = "Sato Taro"
        user.age = 30
        session.add(user)
        
        await session.commit()
        print(user.user_id)
        await session.refresh(user)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())
```
実行はエラーになってしましました。
```
～～～～～～～～
raise exc.MissingGreenlet(
sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called; can't call await_only() here. Was IO attempted in an unexpected place? (Background on this error at: https://sqlalche.me/e/20/xd2s)
```

### こっちは成功するのか...
ではこのコードならどうでしょう。`session.commit()`を`main()`関数での一連の処理の最後に実行します。

```python
import sys
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine

from app.routers.chat_mode import Chat



engine=create_async_engine("postgresql+asyncpg://{ユーザ名}:{パスワード}@localhost:5432/postgres")
 
Session = sessionmaker(engine,class_=AsyncSession)

async def main():
    async with Session() as session:
        user = Chat()
        user.name = "Sato Taro"
        user.age = 30
        session.add(user)
        
        print(user.user_id)
        await session.commit()
if __name__ == "__main__":
    import asyncio
    asyncio.run(main())
```
`print`の出力結果は以下になります
```
41ca6b1d-ab95-4bb2-bc01-4eecf8e95de2
```

### 何が起こっているのか
`session.refresh()`の有無、`session.commit()`の場所によってエラーになったりならなかったりします。
何が起こっているのか調べてみましょう。
コードを以下のように修正します。
`session.commit()`の前後と`session.refresh()`の後にそれぞれ`user`オブジェクトを出力するようにします。

```python
import sys
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine

from app.routers.chat_mode import Chat



engine=create_async_engine("postgresql+asyncpg://{ユーザ名}:{パスワード}@localhost:5432/postgres")
 
Session = sessionmaker(engine,class_=AsyncSession)

async def main():
    async with Session() as session:
        user = Chat()
        user.name = "Sato Taro"
        user.age = 30
        session.add(user)
        
        print(f"user1: {user}")
        await session.commit()
        print(f"user2: {user}")
        
        await session.refresh(user)
        print(f"user3: {user}")
        
        
if __name__ == "__main__":
    import asyncio
    asyncio.run(main())
```

結果は以下のようになります。
```
user1: user_id=UUID('531b2a04-ad98-44a7-9b2c-db3bd62ed3c0') name='Sato Taro' age=30
user2: 
user3: age=30 user_id=UUID('531b2a04-ad98-44a7-9b2c-db3bd62ed3c0') name='Sato Taro'
```

`session.commit()`の実行後、`user`オブジェクトが空になっていることが分かります。
`session.commit()`は現在のトランザクションによる変更をデータベースに反映し、永続化する効果があります。
この際、トランザクションで用いられていたオブジェクトが失効し、消去されます。
そのため、`session.commit()`の実行後に、そのトランザクションで用いられていたオブジェクト(`user`)の要素にアクセスしようとした場合などにエラーが発生したというわけです。

また、オブジェクトの再読み込みについても`asyncio`のような非同期処理を行う手法を用いている場合は動作しません。
そのため、明示的に`session.refresh()`を行って、最新の状態のオブジェクトをロードしてくる、あるいは`session.commit()`を処理の最後に行う必要があるということになります。
実際の利用シーンでは、オブジェクトの要素を`retun`するような場合が多いと思いますので、多くの場合で
`session.commit()`と`session.refresh()`をしっかりと行うという方法を採用することになるかと思われます。

# まとめ
- sqlalchemyの`session.commit()`はそのトランザクションで使われているオブジェクトを失効させ、内容を削除する
- 特に非同期処理の場合、オブジェクトは再読み込みされない
- `session.refresh("オブジェクトの変数")`で最新の状態のオブジェクトを読み込むことができる

なんで`session.refresh()`が必要なんだっけ？と聞かれたときや、オブジェクトの参照処理を移動したらエラーになったぞ？という場面に遭遇した際の解決の一助になればと思います。ありがとうございました！

# 参考

https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session