Djangoモデルのsave()メソッド:実装パターンと活用事例

Djangoのモデルクラスでsave()メソッドをオーバーライドする際、引数の受け渡し方法には主に2つの設計アプローチがあり、それぞれ用途と保守性に応じた選択が求められます。

アプローチ1:可変長引数による汎用的実装

以下のように*args**kwargsを用いることで、将来追加される可能性のある引数にも柔軟に対応できます:

def save(self, *args, **kwargs):
    # 前処理(例:タイムスタンプ更新)
    self.updated_at = timezone.now()
    if not self.pk:
        self.created_at = self.updated_at
    super().save(*args, **kwargs)

このスタイルは、Django内部の引数仕様変更への耐性が高く、サードパーティライブラリとの連携時にも安定して動作します。

アプローチ2:明示的パラメータによる厳密な定義

一方、DjangoのModel.save()が実際に受け付ける引数を明示的に列挙する方式もあります:

def save(
    self,
    force_insert=False,
    force_update=False,
    using=None,
    update_fields=None
):
    self.updated_at = timezone.now()
    if not self.pk:
        self.created_at = self.updated_at
    super().save(
        force_insert=force_insert,
        force_update=force_update,
        using=using,
        update_fields=update_fields
    )

この記述は型ヒントやIDEの補完支援に優れ、呼び出し側が引数の意味を直感的に理解できるため、チーム開発や長期保守プロジェクトでは推奨されます。

実践的な拡張パターン

1. タイムスタンプの自動管理

from django.db import models
from django.utils import timezone

class Article(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, blank=True)
    published_at = models.DateTimeField(null=True, blank=True)
    updated_at = models.DateTimeField(auto_now=True)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        if not self.pk and not self.published_at:
            self.published_at = timezone.now()
        super().save(*args, **kwargs)

2. データ整合性の強制チェック

class InventoryItem(models.Model):
    name = models.CharField(max_length=150)
    stock_quantity = models.PositiveIntegerField()
    min_stock_level = models.PositiveIntegerField(default=0)

    def save(self, *args, **kwargs):
        if self.stock_quantity < self.min_stock_level:
            raise ValidationError(
                f"在庫数({self.stock_quantity})は最低水準({self.min_stock_level})未満です"
            )
        super().save(*args, **kwargs)

3. 関連レコードの同期生成

class Order(models.Model):
    customer = models.ForeignKey('Customer', on_delete=models.CASCADE)
    status = models.CharField(max_length=20, default='draft')

    def save(self, *args, **kwargs):
        is_new = self._state.adding
        super().save(*args, **kwargs)
        if is_new:
            # 注文作成時に関連する配送情報も初期化
            ShippingInfo.objects.create(order=self)

4. 条件付き保存制御

class DraftPost(models.Model):
    content = models.TextField()
    is_published = models.BooleanField(default=False)

    def save(self, *args, **kwargs):
        if self.is_published and not self.pk:
            # 公開済み投稿は新規作成不可(下書きから遷移必須)
            raise PermissionError("公開済み記事は既存レコードからの更新のみ許可されます")
        super().save(*args, **kwargs)

5. 外部サービスとの連携

import requests

class Notification(models.Model):
    recipient = models.EmailField()
    message = models.TextField()
    sent_at = models.DateTimeField(null=True, blank=True)

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if not self.sent_at:
            try:
                response = requests.post(
                    "https://api.example.com/notify",
                    json={"to": self.recipient, "body": self.message},
                    timeout=5
                )
                if response.status_code == 200:
                    self.sent_at = timezone.now()
                    super().save(update_fields=['sent_at'])
            except requests.RequestException:
                pass  # 通信失敗時はログ出力など別途処理

タグ: Django Python web-development ORM model

6月4日 19:02 投稿