SQLModel入門

このハンズオンでは、PythonのORM(Object-Relational Mapping)ライブラリであるSQLModelの基本的な使い方について実際にハンズオン形式で手を動かしながら体験します。

  • ORMの概念と、SQLを直接記述する方法との違い
  • SQLModelの特徴と基本的な使い方
  • テーブルモデルの定義
  • エンジンとセッションによるデータベース接続
  • SQLModelを使ったCRUD操作(作成・取得・更新・削除)

1. 事前準備

このハンズオンでは、以下のツールが必要です。まだ準備できていない場合は、リンク先の手順に沿って準備をお願いします。

2. ORMとは

データベースを操作する方法には、SQLを直接記述する方法ORM(Object-Relational Mapping)を使う方法の2つがあります。

SQLを直接記述する方法では、PythonコードからSQL文字列をmysql-connectorなどのライブラリに渡し、データベースを操作します。一方、ORMを使う方法では、PythonコードからPythonオブジェクトを操作するだけで、ORMが自動的にSQLを生成してデータベースを操作します。

これまでの講座Pythonでデータベースを操作しようでは、mysql-connector-pythonを使ってSQLを直接記述し、データベースを操作してきました。例えば、ユーザの一覧を取得するには以下のようなコードを書きました。

cursor.execute("SELECT id, name, age FROM users")
rows = cursor.fetchall()
for row in rows:
    print(f"id: {row[0]}, name: {row[1]}, age: {row[2]}")

この方法では、SQL文を文字列として記述し、取得した結果もタプルのリストとして扱います。データへのアクセスはrow[0]row[1]のようにインデックスで行うため、どのカラムの値かがコードから読み取りにくい面があります。

ORM(Object-Relational Mapping)は、データベースのテーブルをPythonのクラスに、レコードをクラスのインスタンスに対応させる仕組みです。ORMを使うと、SQLを直接書く代わりにPythonのコードでデータベースを操作できるようになります。同じ処理を以下のように記述できます。

users = session.exec(select(User)).all()
for user in users:
    print(f"id: {user.id}, name: {user.name}, age: {user.age}")

SQLを直接書く代わりにPythonのコードでデータベースを操作でき、user.nameのように属性名でデータにアクセスできます。

2.1 ORMのメリット

ORMを使うことで、いくつかのメリットがあります。

まず、SQLを直接書かなくてよいという点です。Pythonの文法でデータベース操作を記述できるため、SQLの記法を意識する必要がありません。

次に、型安全である点です。型ヒントにより、エディタの補完やエラー検知が利用できます。

また、SQLインジェクション対策にもなります。ORMがクエリを自動生成するため、パラメータ化クエリを手動で記述する必要がありません。

💡 ポイント
ORMは便利な仕組みですが、SQLの知識が不要になるわけではありません。ORMが生成するSQLを理解したり、パフォーマンスの問題を調査したりする際には、SQLの基礎知識が役立ちます。これまでの講座でSQLの基本を学んだことは、ORMを使う上でも大きなアドバンテージになります。

2.2 SQL直接記述とORMの比較

SQL直接記述とORMの違いを以下の表にまとめます。

項目 SQL直接記述(mysql-connector) ORM(SQLModel)
データベース操作の記述 SQL文字列を直接記述する Pythonオブジェクトを操作する
SQLの知識 必須 基本的には不要(ORMが自動生成)
型チェック なし(文字列のため) あり(型ヒントによる補完・検知)
SQLインジェクション対策 手動でパラメータ化が必要 ORMが自動的に対策する
学習コスト SQLの知識があればすぐに使える ORM固有のAPIを学ぶ必要がある
📝 SQLインジェクションとは
SQLインジェクションとは、ユーザの入力値にSQL文を埋め込むことで、意図しないデータベース操作を実行させる攻撃手法です。例えば、ログインフォームの入力欄に' OR 1=1 --のような文字列を入力すると、本来のSQL文の条件が書き換えられ、認証を突破されたりデータを不正に取得されたりする危険があります。SQL直接記述の場合はプレースホルダ(%s?)を使って入力値を安全に埋め込む必要がありますが、ORMを使う場合はクエリの生成が自動化されるため、この対策が不要になります。

3. SQLModelとは

SQLModelは、FastAPIの作者であるSebastián Ramírezが開発したPython用のORMライブラリです。内部的にSQLAlchemy(Pythonで最も広く使われているORM)とPydantic(データバリデーションライブラリ)を統合しており、テーブル定義とデータのバリデーションを1つのクラスで行うことができます。

SQLModelには主に3つの特徴があります。

1つ目は、Pydanticモデルとテーブル定義を統合できる点です。1つのクラスでデータのバリデーションとデータベースのテーブル定義を兼ねることができるため、モデルの重複を避けられます。

2つ目は、型ヒントベースで定義できる点です。Pythonの型ヒントでカラムの型を定義できるため、エディタの補完機能やエラー検知を活用しながら開発を進められます。

3つ目は、FastAPIとの高い親和性です。FastAPIと同じ作者が開発しているため、両者の連携がスムーズに行えます。

💡 ポイント
SQLModelは内部的にSQLAlchemyを利用しています。SQLAlchemy単体でも同様のことができますが、SQLModelはPydanticとの統合により、FastAPIと組み合わせて使う場合にコードをシンプルに記述できます。

4. プロジェクトの準備

4.1 パッケージのインストール

SQLModelと、MySQLに接続するためのドライバであるPyMySQLをインストールします。

Windowsの場合:

pip install sqlmodel pymysql python-dotenv

Macの場合:

pip3 install sqlmodel pymysql python-dotenv
📝 PyMySQLとは
PyMySQLは、PythonからMySQLに接続するためのドライバです。これまでの講座で使用したmysql-connector-pythonと同じ役割を持ちますが、SQLModel(SQLAlchemy)ではPyMySQLがよく使われます。
⚠️ 「which is not on PATH」という警告が出る場合
WARNING: The script dotenv is installed in '...' which is not on PATH.という警告は、python-dotenvパッケージに付属するコマンドラインツールのインストール先がPATHに含まれていないことを意味します。このハンズオンではコマンドラインツールは使用しないため、この警告は無視して問題ありません。
⚠️ 「You are using pip version ...」という警告が出る場合
WARNING: You are using pip version X.X.X; however, version Y.Y.Y is available.という警告は、pip自体のバージョンが古いことを意味します。パッケージのインストールは正常に完了しているため、このハンズオンには影響ありません。

4.2 フォルダとファイルの作成

まず、任意の場所にsqlmodel-practiceフォルダを作成します。

sqlmodel-practice/  ← このフォルダを作成

作成したフォルダをVisual Studio Codeで開きます。Visual Studio Codeのメニューから「ファイル」→「フォルダーを開く」を選択し、作成したフォルダを開いてください。

main.pyを作成します。Visual Studio Codeのエクスプローラーでsqlmodel-practiceフォルダを右クリックし、「新しいファイル」を選択してmain.pyという名前で作成してください。

sqlmodel-practice/
└── main.py  ← このファイルを作成

次に、環境変数を管理するための.envファイルを作成します。.envファイルは、データベースの接続情報(ホスト名、ユーザ名、パスワードなど)をコードから分離して管理するためのファイルです。接続情報をコードに直接記述すると、パスワードなどの機密情報がソースコードに含まれてしまうため、.envファイルに外出しするのが一般的です。sqlmodel-practiceフォルダ内に.envという名前でファイルを作成します。

sqlmodel-practice/
├── main.py
└── .env  ← このファイルを作成

作成したファイルに以下の内容を記述して保存します。

DB_HOST=localhost
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=sqlmodel_testdb
💡 ポイント
DB_PASSWORDには、MySQLのインストール時に設定したrootパスワードを指定してください。本講座のコード例ではyour_passwordと記載していますが、ご自身のパスワードに置き換えてください。

4.3 データベースの作成

SQLModelではテーブルの作成はPythonコードから行えますが、データベース自体の作成はSQLModel(SQLAlchemy)のサポート外です。そのため、MySQL CLIからデータベースを作成します。

mysql -u root -p -e "CREATE DATABASE sqlmodel_testdb;"

パスワードを入力すると、データベースsqlmodel_testdbが作成されます。データベースが正しく作成されたことを確認します。

mysql -u root -p -e "SHOW DATABASES;"

パスワードを入力すると、データベースの一覧が表示されます。

+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sqlmodel_testdb    |
| sys                |
+--------------------+

一覧にsqlmodel_testdbが含まれていれば、データベースの作成は成功です。

📝 なぜデータベースだけ手動で作成するのか
ORMは「テーブルの作成・操作」を担当するツールであり、「データベース(スキーマ)の作成」はデータベースサーバの管理操作にあたります。SQLModel(SQLAlchemy)は既存のデータベースに対してテーブルを作成・操作する機能を提供しますが、データベース自体の作成は範囲外です。

5. テーブルの作成

SQLModelでは、Pythonのクラスを定義することでデータベースのテーブル構造を表現します。このクラスをテーブルモデルと呼びます。基本的な構文は以下のとおりです。

from typing import Optional
from sqlmodel import SQLModel, Field

class モデル名(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    フィールド名: 型

SQLModelを継承し、table=Trueを指定することで、このクラスがデータベースのテーブルに対応するモデルであることを示します。テーブルモデルを定義した後、create_engine()エンジン(データベースへの接続を管理するオブジェクト)を作成し、SQLModel.metadata.create_all()でテーブルを自動作成します。

5.1 Pythonコードの修正

実際に試してみましょう。main.pyに以下の内容を記述して保存します。

import os
from typing import Optional

from dotenv import load_dotenv
from sqlmodel import Field, Session, SQLModel, create_engine, select

load_dotenv()


class User(SQLModel, table=True):
    __tablename__ = "users"
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(max_length=10)
    age: int


DB_HOST = os.getenv("DB_HOST", "localhost")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
DB_NAME = os.getenv("DB_NAME", "sqlmodel_testdb")

DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
engine = create_engine(DATABASE_URL)

# テーブルの作成
SQLModel.metadata.create_all(engine)
print("テーブル users を作成しました")

コードを解説します。

class User(SQLModel, table=True):
    __tablename__ = "users"
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(max_length=10)
    age: int

この後作成するusersテーブルと同じ構成のテーブルモデルを定義しています。各要素の意味は以下のとおりです。

要素 説明
SQLModel, table=True データベースのテーブルに対応するモデルであることを示す
__tablename__ = "users" 対応するテーブル名を指定する
id: Optional[int] 主キー。Noneを許容するのは、新規作成時にはまだIDが割り振られていないため
Field(primary_key=True) このフィールドが主キーであることを示す
Field(max_length=10) 文字列の最大長を10文字に制限する(MySQLのVARCHAR(10)に相当)
📝 table=Trueの意味
table=Trueを指定したクラスは、データベースのテーブルに対応する「テーブルモデル」になります。table=Trueを指定しないクラスは、テーブルには対応せず、データの構造やバリデーション(入力チェック)のみを定義するモデルとして機能します。この違いは次の講座で活用します。
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
DB_NAME = os.getenv("DB_NAME", "sqlmodel_testdb")

os.getenv().envファイルに定義した環境変数を読み込んでいます。第1引数が環境変数名、第2引数が環境変数が見つからなかった場合のデフォルト値です。load_dotenv()によって.envファイルの内容が環境変数として読み込まれているため、os.getenv("DB_HOST").envに記載したlocalhostが取得されます。

DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
engine = create_engine(DATABASE_URL)

エンジンは、データベースへの接続を管理するオブジェクトです。create_engine()に接続URLを渡して作成します。接続URLはmysql+pymysql://から始まり、ユーザ名、パスワード、ホスト名、データベース名を指定します。

📝 接続URLの形式
SQLModelの接続URLは、前の講座のmysql.connector.connect(host=..., user=..., password=..., database=...)に相当します。mysql+pymysqlの部分は「MySQLデータベースにPyMySQLドライバで接続する」という意味です。
SQLModel.metadata.create_all(engine)

create_all()は、table=Trueが指定されたすべてのモデルに対応するテーブルを、データベースに自動的に作成します。テーブルがすでに存在する場合はスキップされます。

💡 ポイント
前の講座ではcursor.execute("CREATE TABLE users ...")のようにSQLを手動で記述していました。SQLModelでは、Pythonのクラス定義がそのままテーブル定義となり、create_all()で自動的にテーブルが作成されます。

5.2 Pythonの実行確認

main.py を実行します。

python main.py

以下の出力が表示されれば成功です。

テーブル users を作成しました

5.3 MySQLで確認

MySQLに接続して、テーブルが作成されているか確認してみましょう。

mysql -u root -p -e "SHOW TABLES FROM sqlmodel_testdb;"

パスワードを入力すると、以下のようにテーブルの一覧が表示されます。usersが含まれていれば成功です。

+----------------------------+
| Tables_in_sqlmodel_testdb  |
+----------------------------+
| users                      |
+----------------------------+
💡 ポイント
create_engine()echo=Trueを指定すると、SQLModelが内部で実行するSQLがターミナルに出力されます。学習中はecho=Trueにしておくと、ORMがどのようなSQLを生成しているかを確認できます。

6. データの作成(CREATE)

テーブルの準備ができたので、次はレコードを追加します。SQLModelでは、セッションを使ってデータベースを操作します。セッションは、前の講座におけるconnectioncursorの両方の役割を担うオブジェクトです。

モデルのインスタンスを作成し、session.add()でセッションに追加した後、session.commit()で変更をデータベースに反映します。

with Session(engine) as session:
    インスタンス = モデル名(フィールド名=値, ...)
    session.add(インスタンス)
    session.commit()
📝 セッションとは
前の講座では、connection = mysql.connector.connect(...) で接続を作成し、cursor = connection.cursor() でカーソルを取得して操作を行いました。SQLModelのSessionはこの2つの役割を1つにまとめたもので、データベースへの接続管理とSQL操作を統合して行います。with文を使うことで、ブロックを抜ける際にセッションが自動的に閉じられます。

6.1 Pythonコードの修正

実際に試してみましょう。main.pyを以下の内容に書き換えます。

import os
from typing import Optional

from dotenv import load_dotenv
from sqlmodel import Field, Session, SQLModel, create_engine, select

load_dotenv()


class User(SQLModel, table=True):
    __tablename__ = "users"
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(max_length=10)
    age: int


DB_HOST = os.getenv("DB_HOST", "localhost")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
DB_NAME = os.getenv("DB_NAME", "sqlmodel_testdb")

DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
engine = create_engine(DATABASE_URL)

SQLModel.metadata.create_all(engine)

# データの作成
with Session(engine) as session:
    user1 = User(name="Suzuki", age=20)
    user2 = User(name="Tanaka", age=30)
    user3 = User(name="Suzuki", age=25)

    session.add(user1)
    session.add(user2)
    session.add(user3)
    session.commit()

    print(f"作成されたユーザ: {user1.name}(ID: {user1.id})")
    print(f"作成されたユーザ: {user2.name}(ID: {user2.id})")
    print(f"作成されたユーザ: {user3.name}(ID: {user3.id})")

追加したコードを解説します。

with Session(engine) as session:

with文でSessionを作成しています。withブロックを抜ける際にセッションが自動的に閉じられるため、手動でクローズ処理を書く必要がありません。

    user1 = User(name="Suzuki", age=20)

Userモデルのインスタンスを作成しています。この時点ではまだデータベースには保存されていません。

    session.add(user1)

session.add()でインスタンスをセッションに追加しています。複数のインスタンスを追加する場合は、session.add()を複数回呼び出します。

    session.commit()

session.commit()で、セッションに追加されたすべてのインスタンスがINSERT文として実行され、データベースに保存されます。前の講座のconnection.commit()と同じ役割です。commit()を呼ばないと変更は反映されません。

    print(f"作成されたユーザ: {user1.name}(ID: {user1.id})")

commit()の後は、user1.idのようにデータベースが自動付与したID(AUTO_INCREMENT)にアクセスできます。

6.2 Pythonの実行確認

main.py を実行します。

python main.py

以下のような出力が表示されれば成功です。

作成されたユーザ: Suzuki(ID: 1)
作成されたユーザ: Tanaka(ID: 2)
作成されたユーザ: Suzuki(ID: 3)

6.3 MySQLで確認

MySQLに接続して、データが正しく作成されているか確認します。

mysql -u root -p -e "SELECT * FROM sqlmodel_testdb.users;"

パスワードを入力すると、以下の結果が表示されます。

+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | Suzuki |   20 |
|  2 | Tanaka |   30 |
|  3 | Suzuki |   25 |
+----+--------+------+

Pythonから実行したINSERTが正しく反映されていることが確認できます。

7. データの全件取得(READ)

次に、テーブルの全レコードを取得します。select()で取得するモデルを指定し、session.exec()で実行します。.all()で全件をリストとして取得できます。

with Session(engine) as session:
    結果 = session.exec(select(モデル名)).all()

7.1 Pythonコードの修正

実際に試してみましょう。main.pyを以下の内容に書き換えます。

import os
from typing import Optional

from dotenv import load_dotenv
from sqlmodel import Field, Session, SQLModel, create_engine, select

load_dotenv()


class User(SQLModel, table=True):
    __tablename__ = "users"
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(max_length=10)
    age: int


DB_HOST = os.getenv("DB_HOST", "localhost")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
DB_NAME = os.getenv("DB_NAME", "sqlmodel_testdb")

DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
engine = create_engine(DATABASE_URL)

SQLModel.metadata.create_all(engine)

# データの全件取得
with Session(engine) as session:
    users = session.exec(select(User)).all()
    for user in users:
        print(f"id: {user.id}, name: {user.name}, age: {user.age}")

追加したコードを解説します。

    users = session.exec(select(User)).all()

select(User)はSQL文のSELECT * FROM usersに相当します。session.exec()でクエリを実行し、.all()で全件をリストとして取得しています。取得したデータはUserモデルのインスタンスとして返されるため、user.nameのように属性名でアクセスできます。

    for user in users:
        print(f"id: {user.id}, name: {user.name}, age: {user.age}")

前の講座ではrow[0]row[1]のようにインデックスでアクセスしていましたが、SQLModelではuser.iduser.nameのように属性名でアクセスできるため、コードの可読性が向上しています。

7.2 Pythonの実行確認

main.py を実行します。

python main.py

以下のように、usersテーブルの全レコードが表示されれば成功です。

id: 1, name: Suzuki, age: 20
id: 2, name: Tanaka, age: 30
id: 3, name: Suzuki, age: 25

8. データの条件付き取得(READ)

条件を指定してレコードを取得するには、select().where()を追加します。

with Session(engine) as session:
    結果 = session.exec(select(モデル名).where(モデル名.フィールド名 == 値)).all()

8.1 Pythonコードの修正

実際に試してみましょう。main.pyを以下の内容に書き換えます。

import os
from typing import Optional

from dotenv import load_dotenv
from sqlmodel import Field, Session, SQLModel, create_engine, select

load_dotenv()


class User(SQLModel, table=True):
    __tablename__ = "users"
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(max_length=10)
    age: int


DB_HOST = os.getenv("DB_HOST", "localhost")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
DB_NAME = os.getenv("DB_NAME", "sqlmodel_testdb")

DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
engine = create_engine(DATABASE_URL)

SQLModel.metadata.create_all(engine)

# データの条件付き取得
with Session(engine) as session:
    users = session.exec(select(User).where(User.name == "Suzuki")).all()
    for user in users:
        print(f"id: {user.id}, name: {user.name}, age: {user.age}")

追加したコードを解説します。

    users = session.exec(select(User).where(User.name == "Suzuki")).all()

.where(User.name == "Suzuki")は、SQL文のWHERE name = 'Suzuki'に相当します。前の講座ではcursor.execute("SELECT * FROM users WHERE name = %s", ("Suzuki",))のようにプレースホルダを使用していましたが、SQLModelではPythonの比較演算子をそのまま使って条件を記述できます。

8.2 Pythonの実行確認

main.py を実行します。

python main.py

nameSuzukiのレコードのみが表示されれば成功です。

id: 1, name: Suzuki, age: 20
id: 3, name: Suzuki, age: 25

9. データのID指定取得(READ)

主キー(ID)を指定して1件だけ取得するには、session.get()を使います。

with Session(engine) as session:
    結果 = session.get(モデル名, 主キーの値)

9.1 Pythonコードの修正

実際に試してみましょう。main.pyを以下の内容に書き換えます。

import os
from typing import Optional

from dotenv import load_dotenv
from sqlmodel import Field, Session, SQLModel, create_engine, select

load_dotenv()


class User(SQLModel, table=True):
    __tablename__ = "users"
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(max_length=10)
    age: int


DB_HOST = os.getenv("DB_HOST", "localhost")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
DB_NAME = os.getenv("DB_NAME", "sqlmodel_testdb")

DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
engine = create_engine(DATABASE_URL)

SQLModel.metadata.create_all(engine)

# データのID指定取得
with Session(engine) as session:
    user = session.get(User, 1)
    if user:
        print(f"id: {user.id}, name: {user.name}, age: {user.age}")
    else:
        print("ユーザが見つかりません")

追加したコードを解説します。

    user = session.get(User, 1)

session.get(User, 1)は、usersテーブルからID(主キー)が1のレコードを取得します。レコードが存在しない場合はNoneが返されます。

    if user:
        print(f"id: {user.id}, name: {user.name}, age: {user.age}")
    else:
        print("ユーザが見つかりません")

session.get()Noneを返す可能性があるため、if user:で存在チェックを行っています。

9.2 Pythonの実行確認

main.py を実行します。

python main.py

IDが1のレコードが表示されれば成功です。

id: 1, name: Suzuki, age: 20

10. データの更新(UPDATE)

レコードを更新するには、まずsession.get()で対象のレコードを取得し、属性を変更してからsession.commit()で変更を確定させます。

with Session(engine) as session:
    対象 = session.get(モデル名, 主キーの値)
    対象.フィールド名 = 新しい値
    session.add(対象)
    session.commit()

10.1 Pythonコードの修正

実際に試してみましょう。main.pyを以下の内容に書き換えます。

import os
from typing import Optional

from dotenv import load_dotenv
from sqlmodel import Field, Session, SQLModel, create_engine, select

load_dotenv()


class User(SQLModel, table=True):
    __tablename__ = "users"
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(max_length=10)
    age: int


DB_HOST = os.getenv("DB_HOST", "localhost")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
DB_NAME = os.getenv("DB_NAME", "sqlmodel_testdb")

DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
engine = create_engine(DATABASE_URL)

SQLModel.metadata.create_all(engine)

# データの更新
with Session(engine) as session:
    user = session.get(User, 1)
    if user:
        print(f"更新前: id: {user.id}, name: {user.name}, age: {user.age}")
        user.age = 21
        session.add(user)
        session.commit()
        session.refresh(user)
        print(f"更新後: id: {user.id}, name: {user.name}, age: {user.age}")

追加したコードを解説します。

        user.age = 21

取得したインスタンスの属性を直接変更しています。前の講座ではcursor.execute("UPDATE users SET age = %s WHERE id = %s", (21, 1))のようにSQL文を記述していましたが、SQLModelではPythonの属性代入で更新を表現できます。

        session.add(user)
        session.commit()

変更したインスタンスをsession.add()でセッションに追加し、session.commit()で変更を確定させています。

        session.refresh(user)

session.refresh()は、データベースから最新の値を再取得してインスタンスに反映します。これにより、更新後の値をすぐに参照できます。

10.2 Pythonの実行確認

main.py を実行します。

python main.py

以下のような出力が表示されれば成功です。

更新前: id: 1, name: Suzuki, age: 20
更新後: id: 1, name: Suzuki, age: 21

10.3 MySQLで確認

MySQLに接続して、レコードが更新されているか確認してみましょう。

mysql -u root -p -e "SELECT * FROM sqlmodel_testdb.users;"

パスワードを入力すると、以下の結果が表示されます。id1のSuzukiのage20から21に更新されていることがわかります。

+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | Suzuki |   21 |
|  2 | Tanaka |   30 |
|  3 | Suzuki |   25 |
+----+--------+------+

11. データの削除(DELETE)

レコードを削除するには、session.get()で対象のレコードを取得し、session.delete()で削除した後、session.commit()で変更を確定させます。

with Session(engine) as session:
    対象 = session.get(モデル名, 主キーの値)
    session.delete(対象)
    session.commit()

11.1 Pythonコードの修正

実際に試してみましょう。main.pyを以下の内容に書き換えます。

import os
from typing import Optional

from dotenv import load_dotenv
from sqlmodel import Field, Session, SQLModel, create_engine, select

load_dotenv()


class User(SQLModel, table=True):
    __tablename__ = "users"
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(max_length=10)
    age: int


DB_HOST = os.getenv("DB_HOST", "localhost")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
DB_NAME = os.getenv("DB_NAME", "sqlmodel_testdb")

DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
engine = create_engine(DATABASE_URL)

SQLModel.metadata.create_all(engine)

# データの削除
with Session(engine) as session:
    user = session.get(User, 3)
    if user:
        print(f"削除するユーザ: id: {user.id}, name: {user.name}, age: {user.age}")
        session.delete(user)
        session.commit()
        print("削除しました")

# 削除後のデータ確認
with Session(engine) as session:
    users = session.exec(select(User)).all()
    for user in users:
        print(f"id: {user.id}, name: {user.name}, age: {user.age}")

追加したコードを解説します。

        session.delete(user)
        session.commit()

session.delete()でセッションから削除対象として登録し、session.commit()でDELETE文が実行されます。

11.2 Pythonの実行確認

main.py を実行します。

python main.py

以下のような出力が表示されれば成功です。

削除するユーザ: id: 3, name: Suzuki, age: 25
削除しました
id: 1, name: Suzuki, age: 21
id: 2, name: Tanaka, age: 30

11.3 MySQLで確認

MySQLに接続して、レコードが削除されているか確認してみましょう。

mysql -u root -p -e "SELECT * FROM sqlmodel_testdb.users;"

パスワードを入力すると、以下の結果が表示されます。id3のレコードが削除されていることがわかります。

+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | Suzuki |   21 |
|  2 | Tanaka |   30 |
+----+--------+------+

12. SQLとの対応関係

ここまでのCRUD操作について、前の講座で学んだSQLを直接記述する方法と、SQLModelを使う方法を対比してまとめます。

操作 SQL(mysql-connector-python) SQLModel
作成 cursor.execute("INSERT INTO users (name, age) VALUES (%s, %s)", (...)) session.add(User(name=..., age=...))
全件取得 cursor.execute("SELECT * FROM users") session.exec(select(User)).all()
条件取得 cursor.execute("... WHERE name = %s", (...)) select(User).where(User.name == ...)
1件取得 cursor.execute("... WHERE id = %s", (...)) / fetchone() session.get(User, id)
更新 cursor.execute("UPDATE users SET age = %s WHERE id = %s", (...)) user.age = 値 / session.add(user)
削除 cursor.execute("DELETE FROM users WHERE id = %s", (...)) session.delete(user)
確定 connection.commit() session.commit()

SQLModelを使うことで、SQL文を文字列として記述する必要がなくなり、Pythonの文法でデータベース操作を表現できることがわかります。

13. クリーンアップ

ハンズオンが完了したら、作成したデータベースを削除します。MySQL CLIから以下のコマンドを実行してください。

mysql -u root -p -e "DROP DATABASE sqlmodel_testdb;"

パスワードを入力すると、データベースsqlmodel_testdbが削除されます。

13.1 MySQLで確認

MySQLに接続して、データベースが削除されているか確認してみましょう。

mysql -u root -p -e "SHOW DATABASES;"

パスワードを入力すると、以下のようにデータベースの一覧が表示されます。sqlmodel_testdbが一覧に存在しないことが確認できます。

+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+

14. まとめ

このハンズオンでは、SQLModelの基本的な使い方を体験しました。

  • ORM(Object-Relational Mapping)は、データベースのテーブルをPythonのクラスとして操作できる仕組みである
  • SQLModelは、SQLAlchemyとPydanticを統合したORMライブラリで、FastAPIとの連携に適している
  • SQLModelクラスにtable=Trueを指定することで、データベースのテーブルに対応するモデルを定義できる
  • create_engine()でデータベースへの接続を設定し、Sessionを使ってデータの操作を行う
  • session.add()でデータの作成、session.exec(select(...))でデータの取得、session.delete()でデータの削除ができる
  • session.commit()で変更をデータベースに確定させる(前の講座のconnection.commit()と同じ役割)
  • echo=Trueを指定すると、SQLModelが内部で実行するSQLを確認できる