Skip to content

MTVについて

FastAPIは、WebアプリケーションのUIをサーバー側でレンダリングするのではなく、APIを提供することに特化しているため、MVC(Model-View-Controller)よりも**MTV(Model-Template-View)**の概念で役割を捉える方がより適切です。

問題のあるコード(パターンがない場合)

Section titled “問題のあるコード(パターンがない場合)”

問題のあるコード:

# 問題: すべてのロジックが1つのエンドポイントに混在
from fastapi import FastAPI
from sqlalchemy import create_engine, text
app = FastAPI()
engine = create_engine("postgresql://user:pass@localhost/db")
@app.get("/users/{user_id}")
def get_user(user_id: int):
# HTTP処理
# データベース操作
with engine.connect() as conn:
result = conn.execute(text(f"SELECT * FROM users WHERE id = {user_id}"))
user = result.fetchone()
# ビジネスロジック
if user and user['age'] < 18:
user['status'] = 'minor'
else:
user['status'] = 'adult'
# レスポンス生成
return {"id": user['id'], "name": user['name'], "status": user['status']}
# 問題点:
# 1. テストが困難(HTTP、DB、ビジネスロジックが混在)
# 2. SQLインジェクションのリスク
# 3. 再利用性が低い
# 4. 責務が不明確

解決: MTVパターン

# Model: データ構造とビジネスロジック
from pydantic import BaseModel
from sqlalchemy.orm import Session
class User(BaseModel):
id: int
name: str
age: int
@property
def status(self) -> str:
return 'minor' if self.age < 18 else 'adult'
# View: エンドポイント(HTTP処理)
@app.get("/users/{user_id}", response_model=User)
def get_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(UserModel).filter(UserModel.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return User.from_orm(user)
# メリット:
# 1. 各レイヤーを独立してテスト可能
# 2. ビジネスロジックの再利用が容易
# 3. 責務が明確
# 4. 型安全性が向上

MTV vs MVC vs クリーンアーキテクチャ

Section titled “MTV vs MVC vs クリーンアーキテクチャ”

MTVが適している場合:

# FastAPIの標準的なパターン
# - APIサーバーとして機能
# - シンプルな構造
# - 迅速な開発
# 構造
View (Endpoint) → Service → Model (Pydantic/SQLAlchemy)

メリット:

  • FastAPIの標準パターンと一致
  • シンプルで理解しやすい
  • 開発速度が速い

デメリット:

  • 複雑なビジネスロジックには不向き
  • 大規模アプリケーションには限界

クリーンアーキテクチャが適している場合:

# 適用範囲:
# - 複雑なビジネスロジック
# - 複数の外部システムとの統合
# - 長期的な保守性を最優先
# 構造
┌─────────────────────────────────┐
Endpoint (Interface) │
├─────────────────────────────────┤
│ Use Case (Application) │
├─────────────────────────────────┤
Domain (Business Logic) │
├─────────────────────────────────┤
Repository (Interface) │
├─────────────────────────────────┤
Infrastructure (Implementation) │
└─────────────────────────────────┘

実装例:

# Domain層: ビジネスロジック
class User:
def __init__(self, id: int, name: str, age: int):
self.id = id
self.name = name
self.age = age
def is_minor(self) -> bool:
return self.age < 18
# Use Case層: アプリケーションロジック
class GetUserUseCase:
def __init__(self, repo: UserRepository):
self.repo = repo
def execute(self, user_id: int) -> User:
return self.repo.find_by_id(user_id)
# Endpoint: HTTP処理
@app.get("/users/{user_id}")
def get_user(user_id: int, use_case: GetUserUseCase = Depends()):
user = use_case.execute(user_id)
return {"id": user.id, "name": user.name}

判断基準:

観点MTVクリーンアーキテクチャ
学習コスト低い高い
開発速度高い中程度
ビジネスロジックの表現力中程度高い
適用範囲小〜中規模大規模
FastAPIとの親和性非常に高い中程度

実践的な選択指針:

# MTVを選ぶべき場合:
# 1. シンプルなCRUDアプリケーション
# 2. 開発速度を優先する
# 3. 小規模から中規模のプロジェクト
# 4. FastAPIの標準パターンに従いたい
# クリーンアーキテクチャを選ぶべき場合:
# 1. 複雑なビジネスルールがある
# 2. 複数の外部システムと統合する必要がある
# 3. 長期的な保守性を最優先する
# 4. 大規模なエンタープライズアプリケーション
  1. Model(モデル) 💾

    • モデルはデータの構造とビジネスロジックを扱います。FastAPIでは、以下のものがモデルに相当します。
    • Pydanticモデル: リクエストボディやレスポンスのデータの構造を定義し、自動的なデータ検証を行います。
    • ORM(SQLAlchemyなど)モデル: データベースのテーブルとPythonオブジェクトをマッピングし、データベース操作を抽象化します。
  2. Template(テンプレート) 🖼️

    • テンプレートは、HTMLやUIを指します。FastAPIは主にAPIサーバーとして機能するため、通常、テンプレートは使用しません。クライアント側(例:React、Vue.js)がUIを構築し、FastAPIのAPIからデータを取得します。このため、MTVの観点ではこのレイヤーは存在しないか、クライアントサイドの責務となります。
  3. View(ビュー) 🖥️

    • ビューは、ユーザーからのリクエストを受け取り、モデルと連携して、最終的なレスポンスを返すロジックを担います。FastAPIにおいては、APIエンドポイント関数がこのビューに相当します。

主な役割:

  • リクエストからパスパラメータやクエリパラメータを取得します。
  • データベースや外部サービスからデータを取得するロジックを呼び出します。
  • データの整合性を確認し、エラーを返します。
  • Pydanticモデルを使って、レスポンスの形式を定義・保証します。

例: Viewとしてのエンドポイント関数

from fastapi import FastAPI, Depends, HTTPException
from typing import List
# 以下はモデルとサービスをインポートしていると仮定
from .schemas import UserResponse
from .services import get_user_from_db
app = FastAPI()
@app.get("/users/{user_id}", response_model=UserResponse)
def get_user_view(user_id: int):
"""
ユーザー情報を取得するAPIエンドポイント。
この関数がViewの役割を担う。
"""
user = get_user_from_db(user_id) # モデルやサービスからデータを取得
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user

この例では、get_user_view関数がユーザーIDを受け取り、サービスを介してデータを取得し、レスポンスを返す一連の処理を行っています。

例: FastAPI でテンプレートを読み込む例

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
app = FastAPI()
# templatesディレクトリを指定
templates = Jinja2Templates(directory="app/templates")
@app.get("/", response_class=HTMLResponse)
def read_root(request: Request):
# index.html をレンダリング
return templates.TemplateResponse("index.html", {"request": request, "title": "Home"})

MTVの考え方で整理すると、以下のようなディレクトリ構造になります。

.
├── app/
│ ├── __init__.py
│ ├── main.py # View (エンドポイント)
│ ├── schemas/
│ │ ├── __init__.py
│ │ └── user.py # Model (Pydanticモデル)
│ └── services/
│ ├── __init__.py
│ └── db_service.py # Model (データベース操作などのロジック)
│ └── templates/ # Template(apiでは使用しない)
│ ├── index.html
│ └── user.html
└── requirements.txt

このように、Viewを担うエンドポイント関数、Modelを担うPydanticモデルやサービスロジックを明確に分けることで、アプリケーションの構造をよりシンプルに保つことができます。

4. サービスレイヤーの導入 🛠️

Section titled “4. サービスレイヤーの導入 🛠️”

MTVモデルをさらに洗練させるために、ビジネスロジックをビュー(エンドポイント)から完全に分離するサービスレイヤーを導入することが推奨されます。これにより、コードの再利用性が高まり、テストが容易になります。

役割: データベース操作、外部API呼び出し、複雑な計算など、エンドポイント関数が直接行うべきではない処理を担います。

メリット:

  • 責務の分離: ビュー関数はリクエストとレスポンスの処理に集中し、ビジネスロジックはサービスレイヤーに一任されます。
  • 再利用性: 同じビジネスロジックを複数のエンドポイントやタスク(例:バックグラウンドジョブ)で共有できます。
  • テストの容易性: サービスレイヤーの関数は単独でテストできるため、単体テストの作成が簡単になります。

例: サービスレイヤーの導入

app/services/user_service.py

from sqlalchemy.orm import Session
from .. import models, schemas
def create_user_service(db: Session, user_data: schemas.UserCreate):
"""
ユーザー作成のビジネスロジック
"""
# ユーザーが既に存在するか確認
existing_user = db.query(models.User).filter_by(email=user_data.email).first()
if existing_user:
return None # ユーザーが既に存在する場合はNoneを返す
# 新しいユーザーを作成し、データベースに保存
db_user = models.User(email=user_data.email, name=user_data.name)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user

このcreate_user_service関数は、データベースセッションを受け取り、ユーザー作成に関連するすべてのロジック(検証、保存など)を処理します。

5. 依存性注入の活用(リファクタリング) 💉

Section titled “5. 依存性注入の活用(リファクタリング) 💉”

サービスレイヤーを導入する際、FastAPIの依存性注入を積極的に活用することで、ビューとサービスの連携が非常にスムーズになります。

役割: ビュー関数がサービスインスタンスやデータベースセッションを直接作成するのではなく、依存性注入を通じてそれらを受け取るようにします。

メリット:

  • 疎結合: ビューは具体的なサービスの実装に依存せず、必要な機能だけを要求できます。
  • テストのモック化: テスト時に本物のデータベース接続ではなく、モックのサービスやセッションを簡単に注入できます。

例: サービスレイヤーと依存性注入の統合

app/api/v1/endpoints/users.py(ビューの再構築)

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from ...services import user_service
from ...database import get_db
from ...schemas import UserResponse, UserCreate
router = APIRouter()
@router.post("/users/", response_model=UserResponse, status_code=201)
def create_new_user(user_data: UserCreate, db: Session = Depends(get_db)):
"""
新しいユーザーを作成
"""
# 依存性注入を通じてサービス関数を呼び出す
created_user = user_service.create_user_service(db, user_data)
if created_user is None:
raise HTTPException(status_code=400, detail="Email already registered")
return created_user

このコードでは、create_new_user関数はデータベースセッションをDependsで受け取り、そのセッションをuser_serviceに渡すだけです。これにより、エンドポイントは純粋なコントローラーとしての役割に集中できます。

アンチパターン1: Fat View(太ったビュー)

Section titled “アンチパターン1: Fat View(太ったビュー)”

問題のあるコード:

# 問題: エンドポイントにビジネスロジックが集中
@app.post("/users/")
def create_user(user_data: UserCreate, db: Session = Depends(get_db)):
# 問題: バリデーションロジックがエンドポイントにある
if db.query(User).filter(User.email == user_data.email).first():
raise HTTPException(status_code=400, detail="Email already exists")
# 問題: ビジネスロジックがエンドポイントにある
hashed_password = bcrypt.hashpw(user_data.password.encode(), bcrypt.gensalt())
# 問題: データベース操作がエンドポイントにある
db_user = User(email=user_data.email, password=hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
# 問題: メール送信ロジックがエンドポイントにある
send_welcome_email(db_user.email)
return db_user
# 解決: サービスレイヤーにビジネスロジックを移動
class UserService:
def __init__(self, db: Session):
self.db = db
def create_user(self, user_data: UserCreate) -> User:
# バリデーション
if self.db.query(User).filter(User.email == user_data.email).first():
raise ValueError("Email already exists")
# ビジネスロジック
hashed_password = bcrypt.hashpw(user_data.password.encode(), bcrypt.gensalt())
# データベース操作
db_user = User(email=user_data.email, password=hashed_password)
self.db.add(db_user)
self.db.commit()
self.db.refresh(db_user)
# 副作用(メール送信)
send_welcome_email(db_user.email)
return db_user
# エンドポイントはシンプルに
@app.post("/users/")
def create_user(user_data: UserCreate, db: Session = Depends(get_db)):
service = UserService(db)
try:
user = service.create_user(user_data)
return user
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

アンチパターン2: ModelにViewロジックが混在

Section titled “アンチパターン2: ModelにViewロジックが混在”

問題のあるコード:

# 問題: Pydanticモデルにビジネスロジックが含まれている
class User(BaseModel):
id: int
name: str
email: str
def send_email(self, subject: str, body: str):
# 問題: ViewロジックがModelにある
send_email(self.email, subject, body)
def format_for_display(self):
# 問題: フォーマットロジックがModelにある
return f"{self.name} ({self.email})"
# 解決: ビジネスロジックはService層に、フォーマットはView層に
class User(BaseModel):
id: int
name: str
email: str
class UserService:
def send_welcome_email(self, user: User):
send_email(user.email, "Welcome", "Welcome to our service!")
# View層でフォーマット
@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(UserModel).filter(UserModel.id == user_id).first()
return {
"id": user.id,
"display_name": f"{user.name} ({user.email})" # View層でフォーマット
}

FastAPIにおけるMTVパターンは、API開発において明確な責務分離を実現する重要な設計パターンです。

シニアエンジニアとして考慮すべき点:

  1. プロジェクト規模に応じた選択: 小規模ならMTV、大規模ならクリーンアーキテクチャ
  2. 責務の明確化: 各レイヤーの責務を明確にし、混在を避ける
  3. テスト容易性: 各レイヤーを独立してテストできるように設計
  4. 段階的な改善: 既存のコードベースに段階的にパターンを適用