🦔

【SQLAlchemy】SELECTしかしていなくてもcommitを実行させた方がいい場合

2022/05/10に公開約2,300字

はじめに

pythonでDB操作するときは、SQLAlchemyを利用しています。
その中で、selectしても最新のレコードが取得されないって事で嵌ったので備考録として残します。

起こったこと

下記検証コードのように、DBからレコードを取得し処理対象があればメイン処理をするというバッチ処理を想定する。

他のバッチでflgを更新してコミットされても、★1で取得する値が変わらずifの中にいつまたっても入ってくれなかった。

import datetime
import threading
import time

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import Column
from sqlalchemy.types import Integer, Date, DateTime, String, Numeric

# DB接続セッション
db_host = "localhost"
db_user = "hoge"
db_pass = "passw0rd"
db_name = "hogedb"
db_engine = create_engine(f'mysql://{db_user}:{db_pass}@{db_host}/{db_name}?charset=utf8')

db_session = scoped_session(
    sessionmaker(
        autocommit=False,
        autoflush=True,
        bind=db_engine,
    )
)
DbBase = declarative_base()


# テーブル定義
class Sample(DbBase):
    __tablename__ = "sample"
    id = Column(Integer, primary_key=True)
    flg = Column(Integer)


def get_flg():
    return db_session.query(Sample).filter_by(id=1).first().flg


while True:
    execute_flg = get_flg() # ★1

    if execute_flg:
        # 何らかのDB処理(commit or rollback でトランザクション意識して作成)
        print("実行しました")

    print(str(datetime.datetime.now()) + ":現在のフラグ=" + str(execute_flg))
    time.sleep(1)

結論

  1. SQLAlchemyautocommitFalse
  2. 無限ループで常駐
  3. MySQLのデフォルト分離レベルは、REPEATABLE READ
  4. MySQLのトランザクションはcommit or rollbackの直後に貼りなおされる他、最初のselect時にもトランザクションが開始される模様

これらの組み合わせで発生してた模様。

https://ja.wikipedia.org/wiki/トランザクション分離レベル

ざっくり説明

  • autocommitがFalseのため、明示的にcommit or rollbackをしない限りプログラム終了までトランザクションは終了しない。
  • 最初のフラグ取得のselectで自動でトランザクションが開始されている。
  • 分離レベルが、REPEATABLE READ 、トランザクション開始後に他バッチで更新された値は読み取れない
  • だから、ifの中は実行されない。

対処

selectだけしかしていないが、ループの最後に明示的にcommitを追加してあげればいい。

while True:
    execute_flg = get_flg() 

    if execute_flg:
        # 何らかのDB処理(commit or rollback でトランザクション意識して作成)
        print("実行しました")

    print(str(datetime.datetime.now()) + ":現在のフラグ=" + str(execute_flg))
    db_session.commit() # commitを明示的に実行
    time.sleep(1)

さいごに

明示的にトランザクションを切る場合はいいが、selectだけだしトランザクションあまり考えなくてもいいや・・・って思ってたら、こういうところではまる…

Discussion

ログインするとコメントできます