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つ追加されています。
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
あれ、この場合は失敗するぞ?
次にコードを少し変えて、以下のようにしてみます。print
を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)
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()
が必要なんだっけ?と聞かれたときや、オブジェクトの参照処理を移動したらエラーになったぞ?という場面に遭遇した際の解決の一助になればと思います。ありがとうございました!
参考