VISASQ Dev Blog

ビザスク開発ブログ

SQLAlchemy 2.0 の autocommit モード廃止とその影響

はじめに

こんにちは。エキスパート開発チームの中原です。

エキスパート開発チームでは、新機能開発と並行して、技術負債の解消や開発体験の向上にも積極的に取り組んでいます。 今回はその取り組みの1つとして、SQLAlchemy 2.0 への移行について紹介します。

SQLAlchemy 2.0 は、パフォーマンス向上、型ヒントのサポート強化など、多くの改善が含まれます。 一方で autocommit モードが廃止され、トランザクション管理の方法が変更されるなど、既存コードの見直しが必要となります。 本記事では、autocommit モードの挙動と廃止による影響、特に Refreshing/Expiring の動作変更についてまとめます。

前提

  • SQLAlchemy 1.4 系を使用していること
  • サンプルコードは以下の想定で記載しています
  Base = declarative_base()

  class User(Base):
      __tablename__ = "users"
      id = Column(Integer, primary_key=True, autoincrement=True)
      email = Column(String(254), unique=True)

      addresses = relationship("Address")

  class Address(Base):
      __tablename__ = "addresses"
      id = Column(Integer, primary_key=True, autoincrement=True)
      user_id = Column(Integer, ForeignKey("users.id"))
      prefecture = Column(String(255))

  engine = create_engine("mysql+pymysql://user:pass@host/db_name")

autocommit モードについて

SQLAlchemy の autocommit モードは、ライブラリレベルでトランザクション管理を自動化するための仕組みです。 INSERT や UPDATE のようなデータの変更操作を行うと、自動的にコミットが行われます。 また、add/flush パターンで記述した場合は flush が行われたタイミングで SQLの発行とコミットが行われます。

Session = sessionmaker(bind=engine, autocommit=True, autoflush=True)
session = Session()

# execute はSQLが即時発行される
session.execute(select(User).where(User.email == "user1@example.com"))  # SELECT
session.execute(insert(User).values(email="user1@example.com"))  # INSERT/COMMIT

# add/flush では flush 時にSQLが発行される
session.add(User(email="user2@example.com"))
session.flush()   # INSERT/COMMIT

# autoflush が有効な場合は SELECT 前に flush が自動で実行される
session.add(User(email="user3@example.com"))
session.execute(select(User).where(User.email == "user3@example.com"))  # INSERT/COMMIT -> SELECT

autocommit をオフにした場合は明示的なコミットを行うまで、COMMIT が発行されなくなります。

Session = sessionmaker(bind=engine, autocommit=False, autoflush=True)
session = Session()

# execute はSQLが即時発行される
session.execute(select(User).where(User.email == "user1@example.com"))  # SELECT
session.execute(insert(User).values(email="user1@example.com"))  # INSERT

# add/flush では flush 時にSQLが発行される
session.add(User(email="user2@example.com"))
session.flush()   # INSERT

# autoflush が有効な場合は SELECT 前に flush が自動で実行される
session.add(User(email="user3@example.com"))
session.execute(select(User).where(User.email == "user3@example.com"))  # INSERT -> SELECT

# commit を行うことで初めて COMMIT が発行される
session.commit() # COMMIT

この動作が SQLAlchemy 2.0 のトランザクション管理と同等であるため、2.0 移行への対応として 1.4 の時点で autocommit をオフにすることが推奨されています。

Refreshing / Expiring への影響

SQLAlchemy には Refreshing / Expiring という仕組みがあり、こちらにも間接的に影響があります。 Refreshing / Expiring は、セッション内のオブジェクトの状態を最新に保つための仕組みで、以下のような動作をします。

Session = sessionmaker(bind=engine, autocommit=True, autoflush=True)
session = Session()

# SELECT は2回発行されるが、identity map により同じインスタンスが返される
user1 = session.scalar(select(User).where(User.email == "user1@example.com"))  # SELECT
user2 = session.scalar(select(User).where(User.email == "user1@example.com"))  # SELECT
print(user1 is user2)  # True(同じオブジェクトを返す)

# expire すると attribute を取得する際に再度 SELECT が発行される
session.expire(user1)
print(user1.email)  # SELECT

# refresh を行うことでも再度 SELECT が発行される
session.refresh(user1)  # SELECT

autocommit モードが有効な場合は flush した際に自動的に commit が行われ、commit と同時にセッション内のモデルが Expire されるため、次回アクセス時に最新の状態が取得されます。 これは DEFAULT 値が設定されたカラム(DB側で値が決定するカラム)や、relationship() で定義された属性に影響します。

Session = sessionmaker(bind=engine, autocommit=True, autoflush=True)
session = Session()

# joinedload で addresses を含めて取得
user = (
    session.execute(
        select(User).options(joinedload(User.addresses)).where(User.id == 1)
    )
    .unique()
    .scalar_one()
)
# この時点では addresses は空
print(len(user.addresses))  # 0

# Address を追加して flush で INSERT/COMMIT を実行
# autocommit モードでは flush 時に自動で commit され、user が expire される
session.add(Address(user_id=user.id, prefecture="東京都"))
session.flush()  # INSERT/COMMIT

# user.addresses にアクセスすると、SELECT が発行され addresses が再取得される
print(len(user.addresses))  # 1(SELECTが発行される)

autocommit モードがオフの場合は flush 時に commit/expire が行われないため、最新の状態が取得されません。

Session = sessionmaker(bind=engine, autocommit=False, autoflush=True)
session = Session()

# joinedload で addresses を含めて取得
user = (
    session.execute(
        select(User).options(joinedload(User.addresses)).where(User.id == 1)
    )
    .unique()
    .scalar_one()
)
# この時点では addresses は空
print(len(user.addresses))  # 0

# Address を追加して flush で INSERT を実行
# autocommit モードではないため flush 時に commit は行われず user は expire されない
session.add(Address(user_id=user.id, prefecture="東京都"))
session.flush()  # INSERT

# expire されていないため SELECT は発行されず、addresses は空のまま
print(len(user.addresses))  # 0

# 明示的に commit を行うことで、user が expire され addresses が再取得される
session.commit()
print(len(user.addresses))  # 1(SELECTが発行される)

終わりに

本記事では、SQLAlchemy 2.0 の autocommit モード廃止の影響について紹介しました。 アップデート作業のときは思わぬところで影響が出る可能性があるため、しっかりとテストは行なっておきたいですね!

ビザスクではエンジニアの仲間を募集しています! 少しでもビザスク開発組織にご興味を持たれた方は、ぜひ一度カジュアルにお話ししましょう! recruit.visasq.co.jp