FastAPIプロダクション構成の教科書 — 依存性注入・非同期DB・JWT・テスト・デプロイを一気通貫で

約42分で読めます by ぽんたぬき
FastAPIプロダクション構成の教科書 — 依存性注入・非同期DB・JWT・テスト・デプロイを一気通貫で

FastAPIプロダクション構成の教科書 — 依存性注入・非同期DB・JWT・テスト・デプロイを一気通貫で

はじめに — 「入門」と「実務」の間にある谷

FastAPIのチュートリアルを一通り終えた方なら、こんな経験があるのではないでしょうか。「Hello Worldは書けた。簡単なCRUDも動いた。でも、実際の本番APIをどう設計すればいいのか、まったく分からない」という壁です。

依存性注入、非同期データベース接続、JWTリフレッシュトークン、Pydantic v2によるバリデーション、テスト戦略、Dockerデプロイ——これらのトピックは個別に解説した記事こそ存在しますが、実務で使える一枚絵として整理された日本語記事はほぼ存在しません。本記事はその空白を埋めることを目的としています。

この記事で作るもの

JWT認証つきREST APIを例に、以下の構成を一気通貫で解説します。

  • レイヤードアーキテクチャによるディレクトリ設計
  • Depends()を使ったサービス層の分離
  • SQLAlchemy 2.x AsyncSessionによる非同期DB接続
  • アクセストークン+リフレッシュトークンのJWT認証
  • Pydantic v2 model_validatorによる複合バリデーション
  • pytest-asyncioによるAPIテスト
  • gunicorn + Dockerによるプロダクションデプロイ

対象読者と前提知識

  • FastAPIの公式チュートリアルを完了済みの方
  • Python 3.12以上、async/awaitの基礎を理解している方
  • 実務経験1〜3年程度の中級者

検証環境(2026年時点)

ライブラリ バージョン
FastAPI 0.115〜0.123系
Pydantic v2.11.x
SQLAlchemy 2.0.30
uvicorn 0.32
gunicorn 23.0
Python 3.12〜3.13

インストールは uv add "fastapi[standard]" が2026年現在の推奨手順です。pip install fastapi[all] と異なり、依存関係の解決が高速で、uv.lock によって再現性が保証されます。


リポジトリ構成 — レイヤードアーキテクチャの全体像

推奨ディレクトリ構成

まず完成形のディレクトリ構成を示します。各章でこのツリーのどのファイルを扱うかを意識しながら読み進めてください。

app/
├── api/
│   ├── dependencies/
│   │   ├── auth.py        # get_current_user, require_permission
│   │   └── database.py    # get_db セッション依存
│   └── routers/
│       ├── auth.py        # /auth/* エンドポイント
│       ├── users.py       # /users/* エンドポイント
│       └── items.py       # /items/* エンドポイント
├── models/                # SQLAlchemy ORM モデル
│   ├── user.py
│   └── item.py
├── schemas/               # Pydantic スキーマ(Request/Response)
│   ├── user.py
│   └── item.py
├── services/              # ビジネスロジック層
│   ├── auth_service.py
│   └── item_service.py
├── crud/                  # DB CRUD 操作(生のクエリのみ)
│   ├── user_crud.py
│   └── item_crud.py
├── core/
│   ├── security.py        # JWT 処理・パスワードハッシュ
│   └── config.py          # pydantic-settings による設定管理
├── database.py            # 非同期エンジン・セッションファクトリ
└── main.py
conftest.py
gunicorn.conf.py
Dockerfile
docker-compose.yml

なぜ schemasmodels を分けるのか

初学者がよくやってしまうのが、SQLAlchemyのORMモデルをそのままPydanticスキーマとして使い回すパターンです。これは小規模では動きますが、すぐに破綻します。

  • models/:データベースの永続化構造を表す。テーブル定義の都合で設計する
  • schemas/:APIの外部との契約を表す。クライアントが何を送り、何を受け取るかで設計する

たとえばユーザーモデルには hashed_password カラムが存在しますが、レスポンスには絶対に含めてはいけません。schemas/UserResponse を独立させることで、このような情報漏洩を構造レベルで防止できます。

servicescrud の責務分離

責務
crud/ DBへの読み書きのみ get_user_by_email(db, email)
services/ ビジネスルールの実行 パスワード検証→トークン生成→ログ記録

CRUDはDBを知っていますが、ビジネスルールは知りません。サービス層は複数のCRUD操作とビジネスロジックを組み合わせます。小規模プロジェクトではcrud層を省略してサービス層に直接クエリを書く判断も合理的です。


依存性注入(Depends)によるサービス層設計

Depends() が解決する問題

FastAPIの Depends() は、認証・DBセッション・共通パラメータのDRY化を実現する仕組みです。最大の利点は**「引数に宣言するだけで、テスト時に差し替えられる」**点にあります。

責務ごとに依存を分離する3層設計

# app/api/dependencies/auth.py

from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.core.security import decode_access_token
from app.models.user import User
from app.crud.user_crud import get_user_by_id
from app.api.dependencies.database import get_db

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")

async def get_current_user(
    token: Annotated[str, Depends(oauth2_scheme)],
    db: Annotated[AsyncSession, Depends(get_db)],
) -> User:
    payload = decode_access_token(token)
    if payload is None or payload.get("type") != "access":
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    user = await get_user_by_id(db, int(payload["sub"]))
    if user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    return user

def require_permission(required: str):
    """権限チェックを返す高階関数"""
    def checker(user: Annotated[User, Depends(get_current_user)]) -> User:
        if required not in user.permissions:
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
        return user
    return checker

ルーター単位 vs エンドポイント単位の使い分け【中級者が迷うポイント】

# ルーター全体に認証を適用する場合
router = APIRouter(
    prefix="/items",
    dependencies=[Depends(get_current_user)],  # 全エンドポイントに適用
)

# 戻り値が不要な場合(ここが重要)
@router.delete("/{item_id}", dependencies=[Depends(require_permission("admin"))])
async def delete_item(item_id: int, db: Annotated[AsyncSession, Depends(get_db)]):
    ...

# 戻り値を使う場合(引数として受け取る)
@router.get("/me")
async def get_my_items(current_user: Annotated[User, Depends(get_current_user)]):
    ...

原則:依存の戻り値をエンドポイント内で使わない場合は dependencies=[] リストに入れます。引数として宣言すると「なぜこの引数があるのか」が分かりにくくなります。

Annotated 型エイリアスによるコードの整理

Annotated を使うと、繰り返しのDI宣言をスッキリまとめられます。

# app/api/dependencies/database.py
from typing import Annotated, AsyncGenerator
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import AsyncSessionLocal

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with AsyncSessionLocal() as session:
        yield session

# 型エイリアスとして定義
DbSession = Annotated[AsyncSession, Depends(get_db)]
CurrentUser = Annotated[User, Depends(get_current_user)]

SQLAlchemy 2.x AsyncSession による非同期DB接続

非同期エンジンとセッションファクトリの正しい設定

# app/database.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession

DATABASE_URL = "postgresql+asyncpg://user:pass@db:5432/mydb"

engine = create_async_engine(
    DATABASE_URL,
    echo=False,          # 本番では False
    pool_pre_ping=True,  # 切断済みコネクションの自動検知
    pool_size=10,
    max_overflow=20,
)

AsyncSessionLocal = async_sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False,  # ← 最重要設定
)

expire_on_commit=False が必須な理由【最頻出のハマりどころ】

デフォルト(expire_on_commit=True)では、session.commit() 後にすべてのORMオブジェクトが「期限切れ」状態になります。同期環境では次のアクセス時に自動で再ロードされますが、非同期環境ではこの遅延ロードがグリーンレットの外で実行されるためエラーになります

sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called;
can't call await_() here.

このエラーを見たら、まず expire_on_commit=False を確認してください。設定することで、コミット後もオブジェクトの値をそのまま使い続けられます。

yield 依存によるセッションのライフサイクル管理

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with AsyncSessionLocal() as session:
        yield session
        # with ブロック終了時に自動的にセッションをクローズ
        # 例外発生時は自動ロールバック

async with コンテキストマネージャがセッションの開放とロールバックを保証します。明示的な try/finally は不要です。

Mapped Classes パターン(2.x推奨スタイル)

SQLAlchemy 2.xでは、Mapped 型と mapped_column() を使った型安全なモデル定義が推奨です。

# app/models/user.py
from datetime import datetime
from sqlalchemy import String, Boolean
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True, index=True)
    email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
    hashed_password: Mapped[str] = mapped_column(String(255))
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)
    created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)

旧来の Column(Integer, primary_key=True) 記法も動作しますが、型チェッカーとの親和性やコードの可読性の観点から、2.x記法への移行を強くお勧めします。

コミットはどの層で行うか

CRUDレイヤーでは session.commit() を呼ばないことを原則とします。

# crud/user_crud.py
async def create_user(db: AsyncSession, user_data: dict) -> User:
    user = User(**user_data)
    db.add(user)
    await db.flush()  # IDを取得するためflushのみ(コミットしない)
    return user

# services/auth_service.py
async def register_user(db: AsyncSession, schema: UserCreate) -> User:
    hashed = hash_password(schema.password)
    user = await create_user(db, {"email": schema.email, "hashed_password": hashed})
    await send_welcome_email(user.email)  # 他の処理も同一トランザクション内
    await db.commit()  # サービス層でまとめてコミット
    return user

サービス層でコミットすることで、複数のCRUD操作を1トランザクションにまとめられます。

N+1問題と selectinload

非同期環境では遅延ロードが使えないため、関連データは必ずEager Loadingで取得します。

from sqlalchemy.orm import selectinload

result = await db.execute(
    select(User).options(selectinload(User.items))
)
users = result.scalars().all()

JWT認証 — アクセストークン+リフレッシュトークンの実装

なぜトークンを2種類に分けるのか

単一の長寿命トークン(例:7日有効)は実装が簡単ですが、漏洩時のリスクが大きく、失効手段がありません。2種類のトークンを組み合わせることでこの問題を解決します。

アクセストークン リフレッシュトークン
寿命 5〜15分 7〜30日
保管場所 レスポンスボディ / メモリ HttpOnly Cookie
DB保存 不要 必要(失効管理のため)
失効可否 不可(寿命まで有効) 可(DBから削除)

JWTペイロード設計

# アクセストークンのペイロード例
{
    "sub": "123",           # ユーザーID
    "type": "access",       # トークン種別(必須)
    "jti": "uuid-v4",       # JWT ID(リフレッシュトークンで再利用検知に使用)
    "scope": "read write",  # 権限スコープ
    "iss": "myapp",         # 発行者
    "aud": "myapp-api",     # 対象オーディエンス
    "exp": 1700000000,      # 有効期限
}

type クレームは特に重要です。これがないと、リフレッシュトークンをアクセストークンとして使う攻撃が成立してしまいます。

core/security.py の実装

# app/core/security.py
from datetime import datetime, timedelta, timezone
from typing import Any
import jwt
from passlib.context import CryptContext
from app.core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(plain: str, hashed: str) -> bool:
    return pwd_context.verify(plain, hashed)

def create_access_token(subject: str, extra: dict[str, Any] | None = None) -> str:
    expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    payload = {
        "sub": subject,
        "type": "access",
        "exp": expire,
        "iss": settings.JWT_ISSUER,
        "aud": settings.JWT_AUDIENCE,
        **(extra or {}),
    }
    return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)

def decode_access_token(token: str) -> dict | None:
    try:
        return jwt.decode(
            token,
            settings.SECRET_KEY,
            algorithms=[settings.ALGORITHM],
            audience=settings.JWT_AUDIENCE,
        )
    except jwt.InvalidTokenError:
        return None

リフレッシュトークンのローテーションと再利用検知

リフレッシュトークンを使用するたびに新しいトークンを発行し(ローテーション)、古いトークンをDB上で無効化します。使用済みのトークンが再度提示された場合はトークン盗難と判断して全セッションを失効させます。

# services/auth_service.py
async def refresh_tokens(db: AsyncSession, refresh_token: str) -> TokenPair:
    payload = decode_refresh_token(refresh_token)
    if payload is None:
        raise HTTPException(status_code=401, detail="Invalid token")

    jti = payload["jti"]
    stored = await get_refresh_token_by_jti(db, jti)

    if stored is None or stored.is_revoked:
        # 再利用検知 → 全セッション失効
        await revoke_all_user_tokens(db, int(payload["sub"]))
        raise HTTPException(status_code=401, detail="Token reuse detected")

    await revoke_refresh_token(db, jti)  # 旧トークンを失効
    new_access = create_access_token(payload["sub"])
    new_refresh = await create_refresh_token(db, payload["sub"])  # 新トークン発行
    await db.commit()
    return TokenPair(access_token=new_access, refresh_token=new_refresh)

認証まわりのアンチパターン

  • localStorage にJWTを保存する:XSSで簡単に盗まれます。アクセストークンはメモリ管理、リフレッシュトークンはHttpOnly Cookieが原則です
  • ❌ シークレットキーをコードにハードコードする:環境変数か、本番ではシークレットマネージャを使います
  • exp クレームを検証しないpython-josePyJWTexp 検証をデフォルトで行いますが、オプションで無効化しないよう注意してください

Pydantic v2 によるリクエスト/レスポンス検証

v1からの主要な変更点

v1 v2
@validator @field_validator
@root_validator @model_validator
.dict() .model_dump()
class Config: orm_mode = True model_config = ConfigDict(from_attributes=True)
Optional[X] X | None

model_validator による複合バリデーション

複数フィールドにまたがる検証が必要な場合は @model_validator(mode="after") を使います。

# app/schemas/user.py
from pydantic import BaseModel, EmailStr, field_validator, model_validator

class UserCreate(BaseModel):
    email: EmailStr
    password: str
    password_confirm: str

    @field_validator("password")
    @classmethod
    def password_strength(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError("パスワードは8文字以上必要です")
        if not any(c.isupper() for c in v):
            raise ValueError("大文字を1文字以上含めてください")
        return v

    @model_validator(mode="after")  # モデル全体を受け取る
    def passwords_match(self) -> "UserCreate":
        if self.password != self.password_confirm:
            raise ValueError("パスワードが一致しません")
        return self

mode="after" では検証済みのモデルインスタンスを受け取るため、フィールド間の比較が安全に行えます。

Request/Responseスキーマの3分割

class UserCreate(BaseModel):        # POST /users のリクエスト
    email: EmailStr
    password: str
    password_confirm: str

class UserUpdate(BaseModel):        # PATCH /users/{id} のリクエスト
    email: EmailStr | None = None
    is_active: bool | None = None

class UserResponse(BaseModel):      # 全エンドポイントのレスポンス
    id: int
    email: str
    is_active: bool
    created_at: datetime

    model_config = ConfigDict(from_attributes=True)  # ORMオブジェクトから生成可能に

pydantic-settings による設定管理

# app/core/config.py
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    DATABASE_URL: str
    SECRET_KEY: str
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
    REFRESH_TOKEN_EXPIRE_DAYS: int = 30
    JWT_ISSUER: str = "myapp"
    JWT_AUDIENCE: str = "myapp-api"

    model_config = SettingsConfigDict(env_file=".env", case_sensitive=True)

@lru_cache
def get_settings() -> Settings:
    return Settings()

settings = get_settings()

@lru_cache により、Settings インスタンスはプロセス内で一度だけ生成されます。テスト時は app.core.config.get_settings をオーバーライドすることでテスト用設定に差し替えられます。


pytest-asyncio によるAPIエンドポイントテスト

基本セットアップ

# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
# conftest.py
import pytest
from httpx import AsyncClient, ASGITransport
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.main import app
from app.database import Base
from app.api.dependencies.database import get_db

TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"

@pytest.fixture(scope="session")
async def engine():
    eng = create_async_engine(TEST_DATABASE_URL)
    async with eng.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield eng
    async with eng.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
    await eng.dispose()

@pytest.fixture
async def db_session(engine):
    async_session = async_sessionmaker(engine, expire_on_commit=False)
    async with async_session() as session:
        yield session
        await session.rollback()  # テストごとにロールバックして独立性を保つ

@pytest.fixture
async def client(db_session):
    async def override_get_db():
        yield db_session

    app.dependency_overrides[get_db] = override_get_db
    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
        yield c
    app.dependency_overrides.clear()

dependency_overrides で差し替える

app.dependency_overrides は FastAPI が提供するDI差し替えの公式メカニズムです。本番の get_db をテスト用セッションに1行で入れ替えられます。

app.dependency_overrides[get_db] = override_get_db

認証つきエンドポイントのテスト

@pytest.fixture
async def auth_client(client, db_session):
    """認証済みクライアントのフィクスチャ"""
    # テストユーザーを作成してトークンを取得
    response = await client.post("/auth/login", data={
        "username": "test@example.com",
        "password": "Testpassword1",
    })
    token = response.json()["access_token"]
    client.headers["Authorization"] = f"Bearer {token}"
    return client

async def test_get_items_requires_auth(client):
    response = await client.get("/items/")
    assert response.status_code == 401

async def test_get_items_authenticated(auth_client):
    response = await auth_client.get("/items/")
    assert response.status_code == 200

uvicorn と gunicorn によるプロセス管理

uvicorn単体 vs gunicorn + UvicornWorker

環境 推奨構成 理由
開発 uvicorn app.main:app --reload ホットリロードが使える
本番(コンテナ + K8s) uvicorn単体(複数レプリカで水平スケール) オーケストレータがプロセス管理を担う
本番(単一VM) gunicorn -k uvicorn.workers.UvicornWorker マルチプロセスでCPUを活用

gunicorn.conf.py の実践設定

# gunicorn.conf.py
import multiprocessing

# 非同期ワーカーの場合、CPUバウンドではないため (2×CPU+1) は過剰
# 1〜4程度から始めて負荷試験で調整する
workers = min(4, multiprocessing.cpu_count() * 2 + 1)
worker_class = "uvicorn.workers.UvicornWorker"
bind = "0.0.0.0:8000"
timeout = 60
graceful_timeout = 30
keepalive = 5
accesslog = "-"
errorlog = "-"
loglevel = "info"

グレースフルシャットダウンの実装

# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.database import engine

@asynccontextmanager
async def lifespan(app: FastAPI):
    # 起動時の処理(DB接続確認など)
    yield
    # シャットダウン時の処理
    await engine.dispose()  # コネクションプールを解放

app = FastAPI(lifespan=lifespan)

lifespan イベントを使うことで、SIGTERMを受けた際にリクエスト処理完了→コネクションプール解放の順序が保証されます。


Dockerコンテナ化とヘルスチェック設計

マルチステージビルドによる軽量イメージ

# Dockerfile
FROM python:3.12-slim AS builder

WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project

FROM python:3.12-slim AS runtime

WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"

# 非rootユーザーで実行(セキュリティ)
RUN useradd --create-home appuser
USER appuser

COPY --chown=appuser:appuser app/ ./app/
COPY gunicorn.conf.py ./

EXPOSE 8000
CMD ["gunicorn", "-c", "gunicorn.conf.py", "app.main:app"]

ヘルスチェックエンドポイントの3層設計

# app/api/routers/health.py
from fastapi import APIRouter, Depends
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.dependencies.database import get_db

router = APIRouter(prefix="/health", tags=["health"])

@router.get("/live")
async def liveness():
    """プロセスが生きているかのみ確認(Kubernetes liveness probe)"""
    return {"status": "ok"}

@router.get("/ready")
async def readiness(db: AsyncSession = Depends(get_db)):
    """依存サービス(DB等)への疎通確認(Kubernetes readiness probe)"""
    try:
        await db.execute(text("SELECT 1"))
        return {"status": "ok", "db": "connected"}
    except Exception as e:
        raise HTTPException(status_code=503, detail=f"DB unreachable: {e}")

@router.get("/startup")
async def startup():
    """起動完了確認(Kubernetes startup probe)"""
    return {"status": "ok"}
プローブ エンドポイント 用途
liveness /health/live 異常なら再起動
readiness /health/ready 異常ならトラフィック停止
startup /health/startup 起動完了まで他プローブを抑止

docker-compose.yml による開発環境

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "8000:8000"
    env_file: .env
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

condition: service_healthy を使うことで、PostgreSQLが完全に起動してからアプリが接続を試みることを保証できます。


まとめ — 中級者が押さえるべき7つの原則

本記事で解説してきた内容を、実務で参照できるチェックリストとしてまとめます。

  • [ ] schemas/models/ を分離している:ORMモデルをそのままAPIレスポンスに使わない
  • [ ] expire_on_commit=False を設定しているMissingGreenlet エラーの回避
  • [ ] コミットはサービス層で行っている:CRUDは flush のみ、トランザクション境界を明確に
  • [ ] JWTに type クレームを含めている:リフレッシュトークンのアクセストークン偽用を防ぐ
  • [ ] model_validator(mode="after") で複合バリデーションを実装している:フィールド間検証は @field_validator ではなく @model_validator
  • [ ] dependency_overrides でテスト用DBに差し替えている:本番コードを変更せずにテスト環境を構築
  • [ ] ヘルスチェックを3層に分けている:liveness / readiness / startup の役割を理解して使い分ける

次のステップ

本記事で紹介した構成をベースに、次のトピックに発展させることをお勧めします。

  • ARQ/Celery:バックグラウンドタスクと非同期ジョブキュー
  • レート制限slowapi を使ったエンドポイントごとのレート制御
  • OpenTelemetry:分散トレーシングと可観測性の導入
  • CI/CD:GitHub Actionsによるlint・テスト・Dockerビルドの自動化

付録:よくあるエラーと対処法

エラー 原因 対処法
MissingGreenlet: greenlet_spawn has not been called expire_on_commit=True 状態でコミット後にORMオブジェクトにアクセス async_sessionmakerexpire_on_commit=False を設定
greenlet_spawn has not been called (遅延ロード) 非同期環境でリレーションを遅延ロードしようとした selectinload / joinedload でEager Loadingを明示
422 Unprocessable Entity リクエストボディがPydanticスキーマにマッチしない レスポンスの detail フィールドを確認し、型・必須フィールドをチェック
DetachedInstanceError セッションが閉じた後にORMオブジェクトにアクセス expire_on_commit=False 設定、またはセッション内でシリアライズを完了させる
アクセストークンでリフレッシュAPIが呼べる JWTの type クレームを検証していない decode_refresh_token 内で payload["type"] == "refresh" を確認

関連記事

Gemini CLI ハンズオン:セットアップから実践活用まで完全ガイド【Claude Code・Codexとの比較付き】

Gemini CLI ハンズオン:セットアップから実践活用まで完全ガイド【Claude Code・Codexとの比較付き】

Gemini CLI ハンズオン:セットアップから実践活用まで完全ガイド【Claude Code・Codexとの比較付き】 Gemini CLIとは?なぜ今注目されるのか Zenn・Qiitaを眺めると、Claude Codeに関する実践記事は急増しています。一方、GoogleのGemini CLIについては、セットアップ方法を紹介した記事こそあれど、「実際にコマンドを打ちながら学べる」ハンズオン...

Andrej Karpathy発「CLAUDE.md」4原則 — AIコーディングエージェントの品質を劇的に変えるルールファイル

Andrej Karpathy発「CLAUDE.md」4原則 — AIコーディングエージェントの品質を劇的に変えるルールファイル

Andrej Karpathy発「CLAUDE.md」4原則 — AIコーディングエージェントの品質を劇的に変えるルールファイル 「AIに書かせたコードが動くけど読めない」「頼んでいない箇所まで書き換えられた」「テストも実行もせずに"完了しました"と返ってくる」——コーディングエージェントを日常的に使うエンジニアであれば、これらの経験に身に覚えがあるはずです。生成AIへの期待が高まる一方で、こうし...

github-mcp-server 完全ガイド|GitHub公式MCPサーバーでAIエージェントにリポジトリ操作を任せる

github-mcp-server 完全ガイド|GitHub公式MCPサーバーでAIエージェントにリポジトリ操作を任せる

github-mcp-server 完全ガイド|GitHub公式MCPサーバーでAIエージェントにリポジトリ操作を任せる > 本記事は2026年8月時点の情報をもとに執筆しています。github-mcp-serverは活発に開発が続いているため、最新情報は公式リポジトリをご確認ください。 --- はじめに:GitHub操作を「自然言語」で行える時代になった AIコーディングツールが普及した現在でも...

4BパラメータのオープンモデルがGPT-5.6 Solに並ぶ日 ― Castform × Neon Lakebaseで実現するRAGコスト1/100

4BパラメータのオープンモデルがGPT-5.6 Solに並ぶ日 ― Castform × Neon Lakebaseで実現するRAGコスト1/100

4BパラメータのオープンモデルがGPT-5.6 Solに並ぶ日 ― Castform × Neon Lakebaseで実現するRAGコスト1/100 > メタディスクリプション: 40億パラメータのオープンモデルが、RL学習とエージェント型検索でGPT-5.6 Sol同等の精度を1/100のコストで達成。Castformのアーキテクチャ、Neon Lakebase Postgresの役割、コスト試...

コメント

0/2000