skydum

個人的な作業記録とか備忘録代わりのメモ

SQLAlchemy2.0の使い方(少しずつ追記予定)

SQLAlchemy 2.0

ずっとSQLAlchemy1.4を使ってきたがそろそSQLAlchemy2.0を使おうかなと思いドキュメントを見たら書き方が結構変わっていて混乱したのでまとめておく。

ドキュメントへのリンクは基本的にSQLAlchemy2.0のドキュメントへのリンクです。

公式サイトの「SQLAlchemy 2.0 - Major Migration Guide」を見ると1.4から2.0への移行のためのガイドが書かれている。
SQLAlchemyの公式サイトはとても見づらい。

SQLAlchemyの基本的な使い方

SQLAlchemyの基本的な使い方を参考にしてください。
ネットで検索すると他にもたくさんの記事が出てくるかと思います。
ここでは基本的な使い方の説明は記載しません。
SQLAlchemy1.4→2.0で変更された部分についての記載と行います。

engineの作り方(変更なし)

# SQLAlchem 1.0
from sqlalchemy import create_engine
engine = create_engine(DATABASE_URL, echo=True)

# SQLAlchem 2.0
from sqlalchemy import create_engine
engine = create_engine(DATABASE_URL, echo=True)

モデルの作成方法とmigrateの方法

モデルの作成方法(結構変わっている)

  • モデルを作る際にdeclarative_baseを継承して作っていたが、新しい書き方ではDeclarativeBaseを継承してベースクラスを作成する。
  • カラムの定義を行う際にColumnを利用していたがmapped_columnを利用してカラムの定義を行い、型情報を持たせることができるようになった。
  • 型情報の定義にはMappedを利用し、定義方法はpythonの型アノテーションを利用する。
  • 型情報を持たせることができるようになったので、誤った型のデータを入れようとすると警告を出すことができるようになった。

migrateの方法(変更なし)

  • Baseの作り方は変わったがmigrateをする際の方法自体に変更はなし。
# SQLAlchem 1.0
# SQLAlchem 2.0
Base.metadata.create_all(engine)

モデルの定義方法とmigrateのサンプル

以下を実行するとsample.sqlite3としてusersテーブルが作成される。

SQLAlchemy1.0

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base

DATABASE_URL = "sqlite:///sample.sqlite3"
engine = create_engine(DATABASE_URL, echo=True)

Base = declarative_base()


class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    age = Column(Integer)


Base.metadata.create_all(engine)

SQLAlchemy2.0

型の定義ができるのがとても良い。

from typing import Optional

from sqlalchemy import Integer, String, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

DATABASE_URL = "sqlite:///sample.sqlite3"
engine = create_engine(DATABASE_URL, echo=True)


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
    name: Mapped[str] = mapped_column(String(50), nullable=False)
    fullname: Mapped[Optional[str]] = mapped_column(String)
    nickname: Mapped[Optional[str]] = mapped_column(String(30))


Base.metadata.create_all(bind=engine)

セッション

変更点

2.0になってsessionmakerの引数からautocommitが廃止された。
Sessionにはパラメーターとしては残っているがFalseにして置く必要がある。 https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.params.autocommit

セッションの種類

  • 通常のアプリケーションの場合

    • sessionmaker
      • sessionmakerで作ったsessionはsessionのファクトリ関数のため、実行するたびに別のsessionが作られる。
      • sessionのclose, commitを手動で行う必要がある。
  • マルチスレッドのアプリケーション(Flake等)の場合

    • scoped_session
      • scoped_sessionで作ったsessionは何度sessionを作っても同一スレッド内であれば同じsessionが取得できる。
      • 処理が終わる際にsessionが自動的にcloseされる。

セッションの作り方

SQLAlchemy1.4

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker

DATABASE_URL = "sqlite:///sample.sqlite3"
engine = create_engine(DATABASE_URL, echo=True)

Base = declarative_base()


class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    age = Column(Integer)


Base.metadata.create_all(engine)


def create_session():
    # 追加するデータを作る
    u = User(id=1, name="name", age=11)

    # sesson(ファクトリー関数)を作る
    session_local = sessionmaker(bind=engine, autoflush=False, autocommit=False)

    # scoped_sessionを使用する場合
    # session_local = scoped_session(sessionmaker(bind=engine, autoflush=False, autocommit=False))

    # sessionを作る
    session = session_local()

    # データを追加してコミットする
    session.add(u)
    session.commit()
    session.close()

    # (別の方法)with 文を使ってsession(ファクトリー関数から)作成
    # データを追加してコミットする
    with session_local() as session:
        u = User(id=2, name="name2", age=12)
        session.add(u)
        session.commit()

SQLAlchemy2.0

from sqlalchemy import Integer, String, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, scoped_session, sessionmaker

DATABASE_URL = "sqlite:///sample.sqlite3"
engine = create_engine(DATABASE_URL, echo=True)


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
    name: Mapped[str] = mapped_column(String(50), nullable=False)
    age: Mapped[int] = mapped_column(Integer)


Base.metadata.create_all(bind=engine)


def create_session():
    # 追加するデータを作る
    u = User(id=1, name="name", age=11)

    # session(ファクトリー関数)を作る
    # autoflushのパラーメータが廃止された
    session_local = sessionmaker(bind=engine, autoflush=False)

    # scoped_sessionを使用する場合
    # session_local = scoped_session(sessionmaker(bind=engine, autoflush=False))

    # sessionを作る
    session = session_local()

    session.add(u)
    session.commit()
    session.close()

    # (別の方法)with文を使ってsessionを作成(1.4と違ってengieを渡して一気にsessionを作れる)
    # 後方五感のためにautocommitが残っているが指定する場合はFalseにする必要がある
    with Session(engine, autoflush=False, autocommit=False) as session:
        u = User(id=2, name="name2", age=12)
        session.add(u)
        session.commit()
        session.close()

データの追加

毎回コードの全文を記載すると長くなるので、ここからは必要な部分のコードのみを記載します。

変更点

SQLAlchemy1.4とSQLAlchemy2.0で大きな違いはなし。

データの追加方法

  • 共通 SQLAlchemy1.4, SQLAlchemy2.0

    • session.addでmodelオブジェクトを1件を追加する
    • session.add_allで複数のmodelオブジェクトを一括で追加する
    • session.bulk_save_objectsでmodelオブジェクトを一括で追加する
  • SQLAlchemy1.4

    • session.executeでDBのテーブルと同じキー名を持つdictを一括で追加する
    • session.bulk_insert_mappingsでmodelオブジェクトを指定してList[Dict]を一括で追加する
  • SQLAlchemy2.0

    • session.executeでinsertでモデルを指定してList[Dict]を一括で追加する

SQLAlchemy1.4, SQLAlchemy2.0

# 1件のデータを追加するパターン
def insert_data_basic():
    """発行されるSQL
    SQL      : INSERT INTO users (id, name, age) VALUES (?, ?, ?)
    parameter: (1, 'name', 11)
    """

    session = get_session()

    # 登録するオブジェクトをモデルから作成
    u = User(id=1, name="name", age=11)

    # 作成したオブジェクトをセッションに追加
    session.add(u)

    # セッションへ変更内容を反映する(コミットはしていないため、rollbackすることが可能)
    # flushをしたところでSQLが発行される
    session.flush()

    # セッションの内容をrollbackして取り消す
    session.rollback()

    # セッションnを閉じる
    session.close()


def insert_all_data():
    """発行されるSQL
    SQL      : INSERT INTO users (id, name, age) VALUES (?, ?, ?)
    parameter: ((1, 'name', 11), (2, 'name2', 12))
    """

    session = get_session()

    # 登録するオブジェクトをモデルから作成
    u = [
        User(id=1, name="name", age=11),
        User(id=2, name="name2", age=12),
    ]

    # リスト内のUserオブジェクトを一括で追加する
    session.add_all(u)

    # セッションへ変更内容を反映する(コミットはしていないため、rollbackすることが可能)
    # flushをしたところでSQLが発行される
    session.flush()

    # セッションの内容をrollbackして取り消す
    session.rollback()

    # セッションnを閉じる
    session.close()


def insert_bulk_save():
    """発行されるSQL
    SQL      : INSERT INTO users (id, name, age) VALUES (?, ?, ?)
    parameter: ((1, 'name', 11), (2, 'name2', 12))
    """
    session = get_session()

    # 登録するオブジェクトをList[Dict]の形式で作成
    u = [
        User(id=1, name="name", age=11),
        User(id=2, name="name2", age=12),
    ]

    # リスト内のUserオブジェクトを一括で追加する
    # bulk_save_objectsをしたところでSQLが発行される
    session.bulk_save_objects(u)

    # bulk_save_ojbectsでSQLが発行されているので、flushの効果はない
    session.flush()

    # セッションの内容をrollbackして取り消す
    session.rollback()

    # セッションnを閉じる
    session.close()

SQLAlchemy1.4

def insert_core():
    """発行されるSQL
    SQL      : INSERT INTO users (id, name, age) VALUES (?, ?, ?)
    parameter: ((1, 'name', 11), (2, 'name2', 12))
    """
    session = get_session()

    # 登録するデータをList[Dict]の形式で作成
    u = [
        dict(id=1, name="name", age=11),
        dict(id=2, name="name2", age=12),
    ]

    # リスト内のdict形式のオブジェクトを一括で追加する
    session.execute(User.__table__.insert(), u)

    # session.executeでSQLが発行されているので、flushの効果はない
    session.flush()

    # セッションの内容をrollbackして取り消す
    session.rollback()

    # セッションnを閉じる
    session.close()

SQLAlchemy2.0

def insert_core():
    """発行されるSQL
    SQL      : INSERT INTO users (id, name, age) VALUES (?, ?, ?)
    parameter: ((1, 'name', 11), (2, 'name2', 12))
    """
    session = get_session()

    # 登録するデータをList[Dict]の形式で作成
    u = [
        dict(id=1, name="name", age=11),
        dict(id=2, name="name2", age=12),
    ]

    # リスト内のdict形式のオブジェクトを一括で追加する
    session.execute(insert(User), u)

    # session.executeでSQLが発行されているので、flushの効果はない
    session.flush()

    # セッションの内容をrollbackして取り消す
    session.rollback()

    # セッションnを閉じる
    session.close()