CLOVER🍀

That was when it all began.

PythonのORM、SQLModelをMySQLで試す

これは、なにをしたくて書いたもの?

PythonのORMをそろそろ試してみようかなと思いまして。

PythonのORMといえばSQLAlchemyが有名らしいのですが、FastAPIの作者がSQLAlchemyを使って作成している
SQLModelというものがあるようなので、MySQLで試してみることにしました。

SQLModel

SQLModelは、Pythonオブジェクトを使ってデータベースと対話するライブラリーです。

SQLModel

PydanticとSQLAlchemyを使っています。

SQLModel is based on Python type annotations, and powered by Pydantic and SQLAlchemy.

Welcome to Pydantic - Pydantic

SQLAlchemy - The Database Toolkit for Python

現時点ではどちらもちゃんと使ったことがないんですけど。

FastAPIのドキュメントでは、こちらのページに出てきます。

SQL (Relational) Databases - FastAPI

主な機能はこちらです。

  • 直感的な記述が可能
    • エディターサポート、補完
  • 使いやすい
    • 適切なデフォルト設定
  • 互換性
    • FastAPI、Pydantic、SQLAlchemyとの互換性
  • 拡張可能
    • SQLAlchemyとPydanticのすべての機能を利用できる
  • 短く書ける
    • コードの重複を最小限に抑える
    • 単一の型アノテーションで多くの作業を行える
    • SQLAlchemyとPydanticでモデルの重複はない

Features - SQLModel

ドキュメントはこちら。

Learn - SQLModel

ドキュメントは、まだ作成途中という感じがしますね。

GitHubリポジトリーはこちら。

GitHub - fastapi/sqlmodel: SQL databases in Python, designed for simplicity, compatibility, and robustness.

どのデータベースに対応しているかが書かれていないのですが、SQLAlchemyと同じと考えてよさそうです。

Dialects — SQLAlchemy 2.0 Documentation

チュートリアルではSQLiteが使われていますが、以下のデータベースが利用可能なのでしょう。

どのコネクターが使えるかは、各データベースのダイアレクトのページに書かれています。たとえばMySQLはこちら。

MySQL and MariaDB — SQLAlchemy 2.0 Documentation

とりあえず、SQLModelを使うということでこのあたりを進めていこうと思います。

Install SQLModel - SQLModel

Tutorial - User Guide - SQLModel

環境

今回の環境はこちら。

$ python3 --version
Python 3.12.3


$ uv --version
uv 0.5.25

MySQLは172.17.0.2でアクセスできるものとします。

 MySQL  localhost:33060+ ssl  practice  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.4.4     |
+-----------+
1 row in set (0.0007 sec)

SQLModel+PyMySQLをインストールする

まずはプロジェクトを作成します。

$ uv init --vcs none sqlmodel-example
$ cd sqlmodel-example

SQLModelをインストール。

$ uv add sqlmodel
$ uv add PyMySQL[rsa]

Install SQLModel - SQLModel

その他、テストなどに必要なライブラリーをインストール。

$ uv add --dev pytest mypy ruff

確認はテストコードで行うことにします。

pyproject.toml

[project]
name = "sqlmodel-example"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "pymysql[rsa]>=1.1.1",
    "sqlmodel>=0.0.22",
]

[dependency-groups]
dev = [
    "mypy>=1.14.1",
    "pytest>=8.3.4",
    "ruff>=0.9.3",
]

[tool.mypy]
strict = true
disallow_any_unimported = true
disallow_any_expr = true
disallow_any_explicit = true
warn_unreachable = true
pretty = true

インストールされたライブラリーの一覧。

$ uv pip list
Package           Version
----------------- -------
annotated-types   0.7.0
cffi              1.17.1
cryptography      44.0.0
greenlet          3.1.1
iniconfig         2.0.0
mypy              1.14.1
mypy-extensions   1.0.0
packaging         24.2
pluggy            1.5.0
pycparser         2.22
pydantic          2.10.6
pydantic-core     2.27.2
pymysql           1.1.1
pytest            8.3.4
ruff              0.9.3
sqlalchemy        2.0.37
sqlmodel          0.0.22
typing-extensions 4.12.2

不要なファイルは削除しておきます。

$ rm hello.py

SQLModelを使ってみる

では、チュートリアルに沿って進めていきましょう。

Tutorial - User Guide - SQLModel

モデルとテーブルを作成する

最初はテーブルを作るわけですが、ふつうにSQLで作っておくパターンと、SQLModelでテーブルを作成するパターンが
あります。

せっかくなので、今回はSQLModelでテーブルを作成してみましょう。

Create a Table with SQLModel - Use the Engine - SQLModel

リレーションありのものも一緒に定義しておきます。

Relationship Attributes - Intro - SQLModel

models.py

from sqlmodel import Field, Relationship, SQLModel, create_engine


class Book(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    isbn: str = Field(unique=True)
    title: str
    price: int


class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    first_name: str
    last_name: str
    age: int
    posts: list["Post"] = Relationship(back_populates="user")


class Post(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    url: str
    user_id: int | None = Field(default=None, foreign_key="user.id")
    user: User | None = Relationship(back_populates="posts")


def create_db_and_tables() -> None:
    url = "mysql+pymysql://kazuhira:password@172.17.0.2:3306/practice"
    engine = create_engine(url, echo=True)
    SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
    create_db_and_tables()

まずは単純なテーブル定義。お題は書籍です。SQLModelでは、このようなクラスをモデルと呼ぶようです。

class Book(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    isbn: str = Field(unique=True)
    title: str
    price: int

idはプライマリーキーで、isbnはユニークキーにしています。

リレーションをつけている方は、また後で書きます。

table=Trueが付いていますが、これはテーブルモデルを表していて、もしtable=Trueがなければデータモデルになるようです。

It's also possible to have models without table=True, those would be only data models, without a table in the database, they would not be table models.

Those data models will be very useful later, but for now, we'll just keep adding the table=True configuration.

データモデルというのは、通常のPydanticのモデルに少し機能を加えたもののようです。また、SQLModelはPydanticの
モデルであるとも言えます。

テーブル作成は、こちらで行います。

def create_db_and_tables() -> None:
    url = "mysql+pymysql://kazuhira:password@172.17.0.2:3306/practice"
    engine = create_engine(url, echo=True)
    SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
    create_db_and_tables()

最初に作成しているのがEngineで、これはSQLAlchemyのもののようです。

    url = "mysql+pymysql://kazuhira:password@172.17.0.2:3306/practice"
    engine = create_engine(url, echo=True)

Create a Table with SQLModel - Use the Engine / Create the Engine

接続URLの書き方は、このあたりを参照。

Engine Configuration — SQLAlchemy 2.0 Documentation

Engine Configuration / Backend-Specific URLs / MySQL

そしてこちらで現在定義されているモデルをデータベースに反映します。

    SQLModel.metadata.create_all(engine)

Create a Table with SQLModel - Use the Engine / Create the Database and Table

これがもう少し進むと、データベースマイグレーションになるようです。詳しいことはAdvanced User Guideへと書かれて
いますが、現時点ではまだ記述がありません。

Create a Table with SQLModel - Use the Engine / Migrations

ではテーブルを作成します。

$ uv run models.py

Engineを作成する時にecho=Trueをつけているので、実行時の情報が標準出力に書き出されます。

2025-02-01 18:25:55,768 INFO sqlalchemy.engine.Engine SELECT DATABASE()
2025-02-01 18:25:55,768 INFO sqlalchemy.engine.Engine [raw sql] {}
2025-02-01 18:25:55,768 INFO sqlalchemy.engine.Engine SELECT @@sql_mode
2025-02-01 18:25:55,768 INFO sqlalchemy.engine.Engine [raw sql] {}
2025-02-01 18:25:55,768 INFO sqlalchemy.engine.Engine SELECT @@lower_case_table_names
2025-02-01 18:25:55,768 INFO sqlalchemy.engine.Engine [raw sql] {}
2025-02-01 18:25:55,769 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-02-01 18:25:55,769 INFO sqlalchemy.engine.Engine DESCRIBE `practice`.`book`
2025-02-01 18:25:55,769 INFO sqlalchemy.engine.Engine [raw sql] {}
2025-02-01 18:25:55,769 INFO sqlalchemy.engine.Engine DESCRIBE `practice`.`user`
2025-02-01 18:25:55,769 INFO sqlalchemy.engine.Engine [raw sql] {}
2025-02-01 18:25:55,770 INFO sqlalchemy.engine.Engine DESCRIBE `practice`.`post`
2025-02-01 18:25:55,770 INFO sqlalchemy.engine.Engine [raw sql] {}
2025-02-01 18:25:55,770 INFO sqlalchemy.engine.Engine
CREATE TABLE book (
        id INTEGER NOT NULL AUTO_INCREMENT,
        isbn VARCHAR(255) NOT NULL,
        title VARCHAR(255) NOT NULL,
        price INTEGER NOT NULL,
        PRIMARY KEY (id),
        UNIQUE (isbn)
)


2025-02-01 18:25:55,770 INFO sqlalchemy.engine.Engine [no key 0.00007s] {}
2025-02-01 18:25:55,823 INFO sqlalchemy.engine.Engine
CREATE TABLE user (
        id INTEGER NOT NULL AUTO_INCREMENT,
        first_name VARCHAR(255) NOT NULL,
        last_name VARCHAR(255) NOT NULL,
        age INTEGER NOT NULL,
        PRIMARY KEY (id)
)


2025-02-01 18:25:55,824 INFO sqlalchemy.engine.Engine [no key 0.00071s] {}
2025-02-01 18:25:55,864 INFO sqlalchemy.engine.Engine
CREATE TABLE post (
        id INTEGER NOT NULL AUTO_INCREMENT,
        title VARCHAR(255) NOT NULL,
        url VARCHAR(255) NOT NULL,
        user_id INTEGER,
        PRIMARY KEY (id),
        FOREIGN KEY(user_id) REFERENCES user (id)
)


2025-02-01 18:25:55,864 INFO sqlalchemy.engine.Engine [no key 0.00038s] {}
2025-02-01 18:25:55,916 INFO sqlalchemy.engine.Engine COMMIT

テーブルが作成されました。

 MySQL  localhost:33060+ ssl  practice  SQL > show tables;
+--------------------+
| Tables_in_practice |
+--------------------+
| book               |
| post               |
| user               |
+--------------------+
3 rows in set (0.0025 sec)

単一のテーブルを操作してみる

テーブルが作成できたので、次はいろいろと操作してみましょう。

確認はpytestでテストを実行しながら行います。

テストコードの雛形はこちら。

test_sqlmodel.py

from operator import or_
from sqlmodel import Session, col, create_engine, delete, func, select
import pytest

from models import Book

url = "mysql+pymysql://kazuhira:password@172.17.0.2:3306/practice"

engine = create_engine(url)


@pytest.fixture()
def delete_all() -> None:
    with Session(engine) as session:
        delete_all_statement = delete(Book)
        session.exec(delete_all_statement)

        session.commit()


# ここに、テストを書く!

ここからもEngineを使用します。登場していないクラスやメソッドを使っていますが、こちらはpytestのFixtureとして定義して、
テストの開始時にデータを削除するのに使っています。

データの登録。

def test_insert(delete_all: None) -> None:
    book1 = Book(
        isbn="978-4297141844",
        title="MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
        price=3080,
    )
    book2 = Book(
        isbn="978-4798161488", title="MySQL徹底入門 第4版 MySQL 8.0対応", price=4180
    )
    book3 = Book(
        isbn="978-4814400812", title="Pythonクイックリファレンス 第4版", price=5280
    )

    session = Session(engine)

    session.add(book1)
    session.add(book2)
    session.add(book3)

    assert book1.id is None
    assert book2.id is None
    assert book3.id is None

    session.commit()

    assert book1.id is not None
    assert book2.id is not None
    assert book3.id is not None

    session.close()

Create Rows - Use the Session - INSERT - SQLModel

Engineを使ってSessionを作成し、ここにモデルのインスタンスを登録していきます。

    session = Session(engine)

    session.add(book1)
    session.add(book2)
    session.add(book3)

プライマリーキーはAUTO_INCREMENTなカラムなのですが、これはコミットをすると値が反映されるようです。

    assert book1.id is None
    assert book2.id is None
    assert book3.id is None

    session.commit()

    assert book1.id is not None
    assert book2.id is not None
    assert book3.id is not None

最後にSessionをクローズします。

    session.close()

Create Rows - Use the Session - INSERT / Close the Session

なお、Sessionはコンテキストマネージャーを実装しているので、通常はwithで扱う方がよいでしょう。

def test_insert2(delete_all: None) -> None:
    book1 = Book(
        isbn="978-4297141844",
        title="MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
        price=3080,
    )
    book2 = Book(
        isbn="978-4798161488", title="MySQL徹底入門 第4版 MySQL 8.0対応", price=4180
    )
    book3 = Book(
        isbn="978-4814400812", title="Pythonクイックリファレンス 第4版", price=5280
    )

    with Session(engine) as session:
        session.add(book1)
        session.add(book2)
        session.add(book3)

        assert book1.id is None
        assert book2.id is None
        assert book3.id is None

        session.commit()

        assert book1.id is not None
        assert book2.id is not None
        assert book3.id is not None

Create Rows - Use the Session - INSERT / A Session in a with Block

以降はこの書き方で書いていきます。

データの更新。

Update Data - UPDATE - SQLModel

これは、オブジェクトの値を更新することで行うようです。

def test_update_select(delete_all: None) -> None:
    books = [
        Book(
            isbn="978-4297141844",
            title="MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
            price=3080,
        ),
        Book(
            isbn="978-4798161488", title="MySQL徹底入門 第4版 MySQL 8.0対応", price=4180
        ),
        Book(
            isbn="978-4814400812", title="Pythonクイックリファレンス 第4版", price=5280
        ),
    ]

    with Session(engine) as session:
        session.add_all(books)
        session.commit()

        book1 = books[0]
        book1.price = book1.price * 10

        session.commit()

        select_statement = select(Book).where(Book.isbn == "978-4297141844")
        results = session.exec(select_statement)

        books_result = results.one()

        assert (
            books_result.title
            == "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ"
        )
        assert books_result.price == 30800

データを登録して、その時に使っていたオブジェクトを更新してコミットすると

        session.add_all(books)
        session.commit()

        book1 = books[0]
        book1.price = book1.price * 10

        session.commit()

        select_statement = select(Book).where(Book.isbn == "978-4297141844")
        results = session.exec(select_statement)

なんとデータベースに反映されています。

        select_statement = select(Book).where(Book.isbn == "978-4297141844")
        results = session.exec(select_statement)

        books_result = results.one()

        assert (
            books_result.title
            == "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ"
        )
        assert books_result.price == 30800

Sessionでオブジェクトは管理されているようで、Session#refreshを使うことでデータベースから値を再取得できます。

以下はオブジェクトの値を変更したものの、再度データベースから読み込み直している例です。

def test_refresh(delete_all: None) -> None:
    books = [
        Book(
            isbn="978-4297141844",
            title="MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
            price=3080,
        ),
        Book(
            isbn="978-4798161488", title="MySQL徹底入門 第4版 MySQL 8.0対応", price=4180
        ),
        Book(
            isbn="978-4814400812", title="Pythonクイックリファレンス 第4版", price=5280
        ),
    ]

    with Session(engine) as session:
        session.add_all(books)
        session.commit()

        book = books[0]
        book.title = ""
        book.price = 0

        assert book.title == ""
        assert book.price == 0

        session.refresh(book)

        assert (
            book.title
            == "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ"
        )
        assert book.price == 3080

Update Data - UPDATE / Refresh the Object

データの取得。select文ですね。

def test_select(delete_all: None) -> None:
    books = [
        Book(
            isbn="978-4297141844",
            title="MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
            price=3080,
        ),
        Book(
            isbn="978-4798161488", title="MySQL徹底入門 第4版 MySQL 8.0対応", price=4180
        ),
        Book(
            isbn="978-4814400812", title="Pythonクイックリファレンス 第4版", price=5280
        ),
    ]

    with Session(engine) as session:
        session.add_all(books)
        session.commit()

        selected_book = session.exec(
            select(Book).where(Book.isbn == "978-4297141844")
        ).one()

        assert (
            selected_book.title
            == "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ"
        )

        selected_books = session.exec(
            select(Book).where(Book.price > 4000).order_by(col(Book.price).desc())
        ).all()

        assert len(selected_books) == 2

        assert selected_books[0].title == "Pythonクイックリファレンス 第4版"
        assert selected_books[0].price == 5280
        assert selected_books[1].title == "MySQL徹底入門 第4版 MySQL 8.0対応"
        assert selected_books[1].price == 4180

        selected_books2 = session.exec(
            select(Book)
            .where(Book.price > 4000)
            .where(Book.isbn == "978-4798161488")
            .order_by(col(Book.price).desc())
        ).all()

        assert len(selected_books2) == 1

        assert selected_books2[0].title == "MySQL徹底入門 第4版 MySQL 8.0対応"

        selected_books = session.exec(
            select(Book)
            .where(or_(Book.price > 4000, Book.isbn == "978-4798161488"))
            .order_by(col(Book.price).desc())
        ).all()

        assert len(selected_books) == 2

        assert selected_books[0].title == "Pythonクイックリファレンス 第4版"
        assert selected_books[0].price == 5280
        assert selected_books[1].title == "MySQL徹底入門 第4版 MySQL 8.0対応"
        assert selected_books[1].price == 4180

ここではselect文をDSLで作成することになります。selectにモデルを渡してselect句を組み立て、whereでフィルタリング
します。これをSession#execに渡すことでSQLが実行されます。

        selected_book = session.exec(
            select(Book).where(Book.isbn == "978-4297141844")
        ).one()

データが取得できました。ここでは1件取得をしています。

        assert (
            selected_book.title
            == "MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ"
        )

複数件取得、and条件、order byの例。

        selected_books = session.exec(
            select(Book).where(Book.price > 4000).order_by(col(Book.price).desc())
        ).all()

        assert len(selected_books) == 2

        assert selected_books[0].title == "Pythonクイックリファレンス 第4版"
        assert selected_books[0].price == 5280
        assert selected_books[1].title == "MySQL徹底入門 第4版 MySQL 8.0対応"
        assert selected_books[1].price == 4180

        selected_books2 = session.exec(
            select(Book)
            .where(Book.price > 4000)
            .where(Book.isbn == "978-4798161488")
            .order_by(col(Book.price).desc())
        ).all()

        assert len(selected_books2) == 1

        assert selected_books2[0].title == "MySQL徹底入門 第4版 MySQL 8.0対応"

orの例。

        selected_books = session.exec(
            select(Book)
            .where(or_(Book.price > 4000, Book.isbn == "978-4798161488"))
            .order_by(col(Book.price).desc())
        ).all()

        assert len(selected_books) == 2

        assert selected_books[0].title == "Pythonクイックリファレンス 第4版"
        assert selected_books[0].price == 5280
        assert selected_books[1].title == "MySQL徹底入門 第4版 MySQL 8.0対応"
        assert selected_books[1].price == 4180

count。これは、ドキュメントに載っていないように思います…。

def test_select_count(delete_all: None) -> None:
    books = [
        Book(
            isbn="978-4297141844",
            title="MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
            price=3080,
        ),
        Book(
            isbn="978-4798161488", title="MySQL徹底入門 第4版 MySQL 8.0対応", price=4180
        ),
        Book(
            isbn="978-4814400812", title="Pythonクイックリファレンス 第4版", price=5280
        ),
    ]

    with Session(engine) as session:
        session.add_all(books)
        session.commit()

        assert session.scalar(select(func.count(Book.id))) == 3

        assert session.scalar(select(func.count(Book.id)).where(Book.price > 5000)) == 1

ここまでコミットばかりしていましたが、ロールバック

def test_rollback(delete_all: None) -> None:
    books = [
        Book(
            isbn="978-4297141844",
            title="MySQL運用・管理[実践]入門 〜安全かつ高速にデータを扱う内部構造・動作原理を学ぶ",
            price=3080,
        ),
        Book(
            isbn="978-4798161488", title="MySQL徹底入門 第4版 MySQL 8.0対応", price=4180
        ),
        Book(
            isbn="978-4814400812", title="Pythonクイックリファレンス 第4版", price=5280
        ),
    ]

    with Session(engine) as session:
        session.add_all(books)

        session.rollback()

        assert session.scalar(select(func.count(Book.id))) == 0

そして最初に載せたFixtureの例がdeleteですね。

@pytest.fixture()
def delete_all() -> None:
    with Session(engine) as session:
        delete_all_statement = delete(Book)
        session.exec(delete_all_statement)

        session.commit()

Delete Data - DELETE - SQLModel

複数のテーブルを扱う

最初に、こんなモデルを定義していました。ユーザーと投稿をお題にしていますね。

class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    first_name: str
    last_name: str
    age: int
    posts: list["Post"] = Relationship(back_populates="user")


class Post(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    url: str
    user_id: int | None = Field(default=None, foreign_key="user.id")
    user: User | None = Relationship(back_populates="posts")

このあたりはリレーションの定義ですね。

    posts: list["Post"] = Relationship(back_populates="user")


    user_id: int | None = Field(default=None, foreign_key="user.id")
    user: User | None = Relationship(back_populates="posts")

Define Relationships Attributes - SQLModel

まずはテストの雛形を用意。

test_sqlmodel_relation.py

import pytest
from sqlmodel import Session, create_engine, delete, select

from models import Post, User


url = "mysql+pymysql://kazuhira:password@172.17.0.2:3306/practice"

engine = create_engine(url)


@pytest.fixture()
def delete_all() -> None:
    with Session(engine) as session:
        session.exec(delete(Post))
        session.exec(delete(User))

        session.commit()


# ここに、テストを書く!

データの登録。

def test_insert() -> None:
    with Session(engine) as session:
        katsuo = User(first_name="カツオ", last_name="磯野", age=11)
        wakame = User(first_name="ワカメ", last_name="磯野", age=9)

        session.add(katsuo)
        session.add(wakame)
        session.commit()

        katsuo_posts = [
            Post(
                title="Pythonのパッケージ&プロジェクト管理ツールであるuvをUbuntu Linux 24.04 LTSにインストールする",
                url="https://kazuhira-r.hatenablog.com/entry/2024/12/27/225046",
                user=katsuo,
            ),
            Post(
                title="PythonでUUID バージョン7、ULIDを扱う",
                url="https://kazuhira-r.hatenablog.com/entry/2024/12/01/163724",
                user=katsuo,
            ),
        ]

        wakame_posts = [
            Post(
                title="PyMySQLを使って、PythonからMySQLに接続してみる",
                url="https://kazuhira-r.hatenablog.com/entry/2024/11/06/235906",
                user=wakame,
            )
        ]

        session.add_all(katsuo_posts)
        session.add_all(wakame_posts)

        session.commit()

        assert len(katsuo.posts) == 2
        assert len(wakame.posts) == 1

オブジェクトを関連付けてあげてSessionに登録すれば

        katsuo_posts = [
            Post(
                title="Pythonのパッケージ&プロジェクト管理ツールであるuvをUbuntu Linux 24.04 LTSにインストールする",
                url="https://kazuhira-r.hatenablog.com/entry/2024/12/27/225046",
                user=katsuo,
            ),
            Post(
                title="PythonでUUID バージョン7、ULIDを扱う",
                url="https://kazuhira-r.hatenablog.com/entry/2024/12/01/163724",
                user=katsuo,
            ),
        ]

        wakame_posts = [
            Post(
                title="PyMySQLを使って、PythonからMySQLに接続してみる",
                url="https://kazuhira-r.hatenablog.com/entry/2024/11/06/235906",
                user=wakame,
            )
        ]

        session.add_all(katsuo_posts)
        session.add_all(wakame_posts)

        session.commit()

リレーションがあるオブジェクトにも反映されます。

        assert len(katsuo.posts) == 2
        assert len(wakame.posts) == 1

Create and Update Relationships - SQLModel

データの取得。

def test_select(delete_all: None) -> None:
    with Session(engine) as session:
        katsuo = User(first_name="カツオ", last_name="磯野", age=11)
        wakame = User(first_name="ワカメ", last_name="磯野", age=9)

        katsuo_posts = [
            Post(
                title="Pythonのパッケージ&プロジェクト管理ツールであるuvをUbuntu Linux 24.04 LTSにインストールする",
                url="https://kazuhira-r.hatenablog.com/entry/2024/12/27/225046",
                user=katsuo,
            ),
            Post(
                title="PythonでUUID バージョン7、ULIDを扱う",
                url="https://kazuhira-r.hatenablog.com/entry/2024/12/01/163724",
                user=katsuo,
            ),
        ]

        wakame_posts = [
            Post(
                title="PyMySQLを使って、PythonからMySQLに接続してみる",
                url="https://kazuhira-r.hatenablog.com/entry/2024/11/06/235906",
                user=wakame,
            )
        ]

        session.add(katsuo)
        session.add(wakame)
        session.add_all(katsuo_posts)
        session.add_all(wakame_posts)

        session.commit()

        katsuo_post_results = session.exec(
            select(Post).where(Post.title.contains("Linux"))
        ).all()

        assert len(katsuo_post_results) == 1
        assert (
            katsuo_post_results[0].title
            == "Pythonのパッケージ&プロジェクト管理ツールであるuvをUbuntu Linux 24.04 LTSにインストールする"
        )
        assert katsuo_post_results[0].user.first_name == "カツオ"

Read Relationships - SQLModel

データを読み出すと、リレーションのあるオブジェクトのデータもついてきます。

        katsuo_post_results = session.exec(
            select(Post).where(Post.title.contains("Linux"))
        ).all()

        assert len(katsuo_post_results) == 1
        assert (
            katsuo_post_results[0].title
            == "Pythonのパッケージ&プロジェクト管理ツールであるuvをUbuntu Linux 24.04 LTSにインストールする"
        )
        assert katsuo_post_results[0].user.first_name == "カツオ"

ちなみに、しれっとlike検索を使っています。

        katsuo_post_results = session.exec(
            select(Post).where(Post.title.contains("Linux"))
        ).all()

こんなところでしょうか。

おわりに

PythonのORM、SQLModelを試してみました。

扱ってみると、Jakarta Persistenceに近い印象を持ちました。その裏にいるSQLAlchemyは扱っていないのですが、こちらは
どうなんでしょうね。

また、ドキュメントはまだ途中のようで、やりたいことは調べながら進める感じになると思います。

とはいえFastAPIの作者が作っていることもあり、FastAPIからデータベースにアクセスする時はまずはこちらを使って
みようかなと思います。