1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

Hello World!!

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

なぜQiitaを始めたの?

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

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

SQLAlchemyとは?

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

本題

SQlAlchemyでSession.commit()をする

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

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

SQlAlchemyでSession.commit()とSession.refresh()をする

次に以下のようなコードに修正します。session.refresh()の処理を追加し、
新しく追加したユーザーのIDをprintしています。

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

あれ、この場合は失敗するぞ?

次にコードを少し変えて、以下のようにしてみます。printsession.commit()session.refresh(user)の間に挿入します。

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()関数での一連の処理の最後に実行します。

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オブジェクトを出力するようにします。

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()が必要なんだっけ?と聞かれたときや、オブジェクトの参照処理を移動したらエラーになったぞ?という場面に遭遇した際の解決の一助になればと思います。ありがとうございました!

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?