2025年10月4日土曜日

SQLAlchemy でテーブルスキーマと automap を共存させる方法

SQLAlchemy でテーブルスキーマと automap を共存させる方法

概要

過去にも紹介しましたがすでにテーブルがある際にわざわざテーブルのスキーマを定義しなくても Python からテーブルの情報を取得したりできる機能が automap です

今回は automap をすでに使っている環境で新規にテーブルを追加するスキーマを automap と共存させる方法を紹介します

同一 Metadata を使うのがポイントです

環境

  • macOS 15.7.1
  • MySQL 9.4.0
  • Python 3.12.11
    • mysqlclient 2.2.7
    • SQLAlchemy 2.0.43
    • alembic 1.16.5

テーブル準備

USE testdb;
CREATE TABLE user_legacy (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

このテーブルは automap 用のテーブルになります

lib/db.py

データベース接続用のファイルです

from sqlalchemy import create_engine

# MySQL で先に testdb というデータベースを作成しておくこと
# CREATE DATABASE testdb;
# MySQL ドライバは mysqlclient なので mysqldb を指定する
# 今回はテストなのでパスワード無しで root ユーザで接続
DATABASE_URL = "mysql+mysqldb://root:@localhost:3306/testdb"

engine = create_engine(DATABASE_URL, echo=True)


def get_engine():
    return engine

lib/models.py

モデルを定義するファイルです
automap の定義と追加するテーブルのスキーマ定義を共存させます
automap では既存のテーブルのみを管理するので reflection_options を追加します

reflection_options の設定がないと以下のエラーになります

sqlalchemy.exc.InvalidRequestError: Table 'user' is already defined for this MetaData instance.  Specify 'extend_existing=True' to redefine options and columns on an existing Table object.
from sqlalchemy import Integer, MetaData, String
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

from lib.db import get_engine

# 共通の metadata を作る
metadata = MetaData()


# Declarative 用 Base
class Base(DeclarativeBase):
    metadata = metadata


# automap 用 Base
AutomapBase = automap_base(metadata=metadata)

# --- 既存DBをリフレクションしてクラス自動生成 ---
engine = get_engine()
# automap の対象となるテーブルを reflection_options で指定する
AutomapBase.prepare(
    engine,
    reflect=True,
    reflection_options={"only": ["user_legacy"]},  # automap で読み込むテーブルを限定
)

# 例: 既存のテーブル user_legacy を ORM クラスとして取得できる
# UserLegacy = AutomapBase.classes.user_legacy


# --- 新規テーブルは DeclarativeBase で定義 ---
class User(Base):
    __tablename__ = "user"

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

マイグレーション確認

今回新規で追加するのは user テーブルになります

  • pipenv run alembic revision --autogenerate -m "Create user table."

これでマイグレーションファイルが作成できたら upgrade します

  • pipenv run alembic upgrade head

これで user テーブルが作成されていれば OK です

mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| alembic_version  |
| user             |
| user_legacy      |
+------------------+
3 rows in set (0.004 sec)

app.py
で動作確認

automap と手動で定義したスキーマを使って各種 CRUD ができるか確認します

  • vim app.py
from sqlalchemy import delete, select, update
from sqlalchemy.orm import Session

from lib.db import get_engine
from lib.models import AutomapBase, Base, User

# SQLite / MySQL どちらでもOK
engine = get_engine()

# Declarative で定義したテーブルを作成 (まだ存在しなければ)
Base.metadata.create_all(engine)

# automap で読み込んだ既存テーブル
UserLegacy = AutomapBase.classes.user_legacy

# セッション作成
with Session(engine) as session:
    # --- INSERT (user) ---
    new_user = User(name="Alice", age=25)
    session.add(new_user)
    session.commit()
    print(f"Inserted User ID: {new_user.id}")

    # --- SELECT (user) ---
    stmt = select(User).where(User.name == "Alice")
    result = session.scalars(stmt).all()
    for user in result:
        print(f"Selected User: id={user.id}, name={user.name}, age={user.age}")

    # --- UPDATE (user) ---
    stmt = update(User).where(User.name == "Alice").values(age=26)
    session.execute(stmt)
    session.commit()
    print("Updated Alice's age to 26")

    # --- DELETE (user) ---
    stmt = delete(User).where(User.name == "Alice")
    session.execute(stmt)
    session.commit()
    print("Deleted Alice")

    # -------------------------
    # automap: user_legacy 操作
    # -------------------------
    # --- INSERT (user_legacy) ---
    new_legacy = UserLegacy(username="bob", email="bob@example.com")
    session.add(new_legacy)
    session.commit()
    print(f"Inserted UserLegacy ID: {new_legacy.id}")

    # --- SELECT (user_legacy) ---
    stmt = select(UserLegacy).where(UserLegacy.username == "bob")
    result = session.scalars(stmt).all()
    for ul in result:
        print(f"Selected UserLegacy: id={ul.id}, username={ul.username}, email={ul.email}")

    # --- UPDATE (user_legacy) ---
    stmt = update(UserLegacy).where(UserLegacy.username == "bob").values(email="bob@newmail.com")
    session.execute(stmt)
    session.commit()
    print("Updated bob's email")

    # --- DELETE (user_legacy) ---
    stmt = delete(UserLegacy).where(UserLegacy.username == "bob")
    session.execute(stmt)
    session.commit()
    print("Deleted bob from user_legacy")
  • pipenv run python app.py

user_legacy も user もどちらも操作できることを確認しましょう

最後に

SQLAlchemy の automap と独自で定義したテーブルスキームを共存させる方法を紹介しました
automap はデフォルトだとすべてのテーブル情報を読み込んでしまうので reflection_options を使うのがポイントです
あとは Metadata を共有すれば OK です

すでにテーブルがあるサービスを automap で管理してる場合にどうしても新規でテーブルを追加しなければならないケースなどに使えるテクニックかなと思います

参考サイト

0 件のコメント:

コメントを投稿