これは、なにをしたくて書いたもの?
PythonのORMをそろそろ試してみようかなと思いまして。
PythonのORMといえばSQLAlchemyが有名らしいのですが、FastAPIの作者がSQLAlchemyを使って作成している
SQLModelというものがあるようなので、MySQLで試してみることにしました。
SQLModel
SQLModelは、Pythonオブジェクトを使ってデータベースと対話するライブラリーです。
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でモデルの重複はない
ドキュメントはこちら。
ドキュメントは、まだ作成途中という感じがしますね。
どのデータベースに対応しているかが書かれていないのですが、SQLAlchemyと同じと考えてよさそうです。
Dialects — SQLAlchemy 2.0 Documentation
チュートリアルではSQLiteが使われていますが、以下のデータベースが利用可能なのでしょう。
- PostgreSQL
- MySQL/MariaDB
- Oracle Database
- Microsoft SQL Server
- SQLite
どのコネクターが使えるかは、各データベースのダイアレクトのページに書かれています。たとえばMySQLはこちら。
MySQL and MariaDB — SQLAlchemy 2.0 Documentation
とりあえず、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]
その他、テストなどに必要なライブラリーをインストール。
$ 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はユニークキーにしています。
- Create a Table with SQLModel - Use the Engine / Create the Table Model Class
- Create a Table with SQLModel - Use the Engine / Define the Fields, Columns
リレーションをつけている方は、また後で書きます。
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)
- Create Rows - Use the Session - INSERT / Create a Session
- Create Rows - Use the Session - INSERT / Add Model Instances to the Session
プライマリーキーは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
- Create Rows - Use the Session - INSERT / Commit the Session Changes
- Automatic IDs, None Defaults, and Refreshing Data - SQLModel
最後に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 == "カツオ"
データを読み出すと、リレーションのあるオブジェクトのデータもついてきます。
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からデータベースにアクセスする時はまずはこちらを使って
みようかなと思います。