FlaskでSQLSpecを使ったCRUDアプリ実装 - ORMを使わずに型安全なDB操作¶
はじめに¶
FlaskでのDB操作といえばSQLAlchemyが定番ですが、生SQLを書きつつ型安全性も保ちたい場面があります。そんな時に出会ったのがSQLSpecです。
警告
注意: SQLSpecは現在実験的なライブラリです。本番環境での使用前に十分な検証を行ってください。
SQLSpecとは¶
SQLSpec は生SQLを書きながら型安全なDB操作を可能にするライブラリ。ORM特有の記法を覚える必要がなく、SQL知識があればすぐに使えるのが特徴です。Flaskプラグインも提供されており、SQLite・PostgreSQL・MySQLなど複数のDBに対応しています。
プロジェクト構成¶
flask-sqlspec-demo/
├── app/
│ ├── __init__.py # Flaskアプリファクトリ
│ ├── database.py # DB初期化
│ └── routes.py # CRUDエンドポイント
├── data/ # SQLite DBファイル用ディレクトリ
├── pyproject.toml
├── docker-compose.yml
└── Dockerfile
サンプルレポジトリ¶
セットアップ¶
### 1. 依存関係
# pyproject.toml
[project]
name = "flask-sqlspec-demo"
version = "0.1.0"
description = "Flask + SQLSpec CRUD demo with SQLite"
requires-python = ">=3.12"
dependencies = [
"flask>=3.0.0",
"sqlspec[sqlite]>=0.1.0",
]
### 2. Docker環境
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY pyproject.toml ./
RUN pip install -e .
COPY app/ ./app/
EXPOSE 5000
CMD ["python", "-m", "flask", "--app", "app", "run", "--host=0.0.0.0"]
# docker-compose.yml
services:
app:
build: .
ports:
- "5001:5000"
volumes:
- ./data:/data
environment:
- PYTHONDONTWRITEBYTECODE=1
- PYTHONUNBUFFERED=1
実装¶
### 1. Flaskアプリファクトリ
# app/__init__.py
from flask import Flask
from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
from sqlspec.extensions.flask import SQLSpecPlugin
DB_PATH = "/data/app.db"
sqlalchemy_spec: SQLSpec = SQLSpec()
plugin: SQLSpecPlugin
db_config: SqliteConfig
def create_app() -> Flask:
"""Flask アプリを生成し、SQLSpec と CRUD Blueprintを登録する。"""
global plugin, db_config
app = Flask(__name__)
# SQLSpec設定
db_config = SqliteConfig(
connection_config={"database": DB_PATH},
extension_config={
"flask": {
"commit_mode": "autocommit",
"session_key": "db",
}
},
)
sqlalchemy_spec.add_config(db_config)
# Flaskプラグイン登録
plugin = SQLSpecPlugin(sqlalchemy_spec, app)
from app.database import init_db
from app.routes import items_bp
init_db(DB_PATH)
app.register_blueprint(items_bp)
return app
### 2. データベース初期化
# app/database.py
import sqlite3
CREATE_ITEMS_TABLE = """
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
"""
def init_db(db_path: str) -> None:
"""アプリ起動時に items テーブルを作成する。"""
with sqlite3.connect(db_path) as conn:
conn.execute(CREATE_ITEMS_TABLE)
conn.commit()
### 3. CRUDエンドポイント
# app/routes.py
from flask import Blueprint, jsonify, request
from app import plugin
items_bp = Blueprint("items", __name__, url_prefix="/items")
@items_bp.get("/")
def list_items():
"""全アイテムを取得する。"""
db = plugin.get_session()
rows = db.execute("SELECT id, name, description, created_at FROM items ORDER BY id")
items = list(rows.all() if rows else [])
return jsonify(items), 200
@items_bp.post("/")
def create_item():
"""新規アイテムを作成する。"""
data = request.get_json(silent=True) or {}
name = data.get("name", "").strip()
if not name:
return jsonify({"error": "nameは必須です"}), 400
description = data.get("description")
db = plugin.get_session()
db.execute(
"INSERT INTO items (name, description) VALUES (?, ?)",
(name, description),
)
row = db.execute(
"SELECT id, name, description, created_at FROM items WHERE rowid = last_insert_rowid()"
)
created = row.one()
return jsonify(created), 201
@items_bp.get("/<int:item_id>")
def get_item(item_id: int):
"""指定IDのアイテムを取得する。"""
db = plugin.get_session()
row = db.execute(
"SELECT id, name, description, created_at FROM items WHERE id = ?",
(item_id,),
)
result = row.one_or_none()
if result is None:
return jsonify({"error": "アイテムが見つかりません"}), 404
return jsonify(result), 200
@items_bp.put("/<int:item_id>")
def update_item(item_id: int):
"""指定IDのアイテムを更新する。"""
db = plugin.get_session()
existing = db.execute(
"SELECT id FROM items WHERE id = ?", (item_id,)
).one_or_none()
if existing is None:
return jsonify({"error": "アイテムが見つかりません"}), 404
data = request.get_json(silent=True) or {}
name = data.get("name", "").strip()
if not name:
return jsonify({"error": "nameは必須です"}), 400
description = data.get("description")
db.execute(
"UPDATE items SET name = ?, description = ? WHERE id = ?",
(name, description, item_id),
)
row = db.execute(
"SELECT id, name, description, created_at FROM items WHERE id = ?",
(item_id,),
)
return jsonify(row.one()), 200
@items_bp.delete("/<int:item_id>")
def delete_item(item_id: int):
"""指定IDのアイテムを削除する。"""
db = plugin.get_session()
existing = db.execute(
"SELECT id FROM items WHERE id = ?", (item_id,)
).one_or_none()
if existing is None:
return jsonify({"error": "アイテムが見つかりません"}), 404
db.execute("DELETE FROM items WHERE id = ?", (item_id,))
return jsonify({"message": f"アイテム {item_id} を削除しました"}), 200
SQLSpecの特徴的なポイント¶
### 1. Flask拡張の統合
SQLSpecのFlaskプラグインを使うことで、簡単にFlaskアプリに統合できます:
from sqlspec.extensions.flask import SQLSpecPlugin
plugin = SQLSpecPlugin(sqlalchemy_spec, app)
# エンドポイント内でセッション取得
db = plugin.get_session()
### 2. 結果の操作
SQLSpecは SQLResult オブジェクトを返し、便利なメソッドを提供します:
# 全件取得
rows = db.execute("SELECT * FROM items")
items = list(rows.all() if rows else [])
# 1件取得(必ず1件ある場合)
item = row.one()
# 1件取得(0件の場合はNone)
item = row.one_or_none()
# 影響を受けた行数
affected = result.rows_affected
### 3. パラメータバインディング
SQLインジェクション対策のため、パラメータは必ずプレースホルダーを使用:
# ✅ 安全
db.execute("SELECT * FROM items WHERE id = ?", (item_id,))
# ❌ 危険
db.execute(f"SELECT * FROM items WHERE id = {item_id}")
### 4. 自動コミットモード
Flask拡張の設定で autocommit モードを有効にできます:
extension_config={
"flask": {
"commit_mode": "autocommit",
"session_key": "db",
}
}
これにより、明示的なコミットが不要になります。
動作確認¶
### 1. アプリケーション起動
# Dockerで起動
docker-compose up --build
# またはローカルで起動
pip install -e .
python -m flask --app app run --debug
### 2. APIテスト
# アイテム作成
curl -X POST http://localhost:5001/items \
-H "Content-Type: application/json" \
-d '{"name": "テストアイテム", "description": "これはテストです"}'
# 全アイテム取得
curl http://localhost:5001/items/
# 特定アイテム取得
curl http://localhost:5001/items/1
# アイテム更新
curl -X PUT http://localhost:5001/items/1 \
-H "Content-Type: application/json" \
-d '{"name": "更新されたアイテム", "description": "更新されました"}'
# アイテム削除
curl -X DELETE http://localhost:5001/items/1
Flask + SQLSpecの利点¶
### 1. シンプルさ
学習コストが低い - SQL知識があればすぐに使える
コードが直感的 - ORM特有の記法を覚える必要がない
デバッグが容易 - 実行されるSQLが明確
### 2. 柔軟性
複雑なクエリも対応 - JOIN、サブクエリなどもそのまま書ける
DBに依存しない - SQLite、PostgreSQL、MySQLで同じ書き方
パフォーマンス最適化 - SQLを直接制御できる
### 3. 型安全性
結果の型変換 - 自動で適切なPython型に変換
エラーハンドリング - SQL実行時のエラーを適切に処理
IDEサポート - SQLシンタックスハイライトが効く
注意点と制限¶
### 1. 実験的なライブラリ
安定性 - API変更の可能性あり
ドキュメント - まだ整備されていない部分あり
コミュニティ - 小規模なため、情報が少ない
### 2. マイグレーション
自動生成なし - マイグレーションファイルは手動作成
バージョン管理 - 別途管理が必要
### 3. Flask統合
プラグイン依存 - Flask拡張に依存する部分あり
セッション管理 - 複雑なトランザクションには工夫が必要
まとめ¶
Flask + SQLSpecで試してみた感想としては、ORMの学習コストをかけずに型安全なDB操作ができるのは魅力的でした。特にSQLを直接書ける点は、複雑なクエリが必要な場面で重宝しそうです。
ただし実験的なライブラリなので、本番利用は慎重に。マイグレーションも手動になるため、小規模なプロジェクトや個人開発向けかもしれません。
「ORMは便利だけど、もっとシンプルにSQLを書きたい」という場合は試す価値ありそうです。
コードは GitHubリポジトリ に置いています。