ORM

Django ORMのdelete入門|データを削除する方法

Django ORM

Django ORMのdelete入門
データを削除する方法

Django ORMでデータベースのレコードを削除する方法を解説します。単一削除から一括削除、論理削除まで紹介します。

こんな人向けの記事です

  • Django ORMでデータの削除方法を学びたい人
  • 関連データの削除動作(CASCADE等)を理解したい人
  • 論理削除の実装方法を知りたい人

Step 1delete()で1件削除

モデルインスタンスのdelete()メソッドを呼ぶと、そのレコードがデータベースから削除されます。

Python
# models.py
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.IntegerField()
    is_active = models.BooleanField(default=True)

    def __str__(self):
        return self.name

class Order(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    quantity = models.IntegerField()
    ordered_at = models.DateTimeField(auto_now_add=True)
Python
# 1件取得して削除
product = Product.objects.get(id=1)
print(f"削除対象: {product.name}")

result = product.delete()
print(result)
実行結果
削除対象: りんご
(3, {'app.Order': 2, 'app.Product': 1})

delete()の戻り値はタプルで、削除された総件数と、モデルごとの削除件数の辞書が返されます。上の例では、Product1件とそれに紐づくOrder2件の合計3件が削除されています。

Step 2一括削除

QuerySetのdelete()を使うと、条件に一致する複数のレコードを一括削除できます。

Python
# 非アクティブな商品を一括削除
deleted_count, details = Product.objects.filter(is_active=False).delete()
print(f"{deleted_count}件削除しました")
print(details)

# 価格が0の商品を一括削除
Product.objects.filter(price=0).delete()

# 特定の日時より古い注文を削除
from datetime import datetime, timedelta
threshold = datetime.now() - timedelta(days=365)
Order.objects.filter(ordered_at__lt=threshold).delete()
実行結果
5件削除しました
{'app.Order': 3, 'app.Product': 2}
全件削除に注意
Product.objects.all().delete()で全レコードが削除されます。実行前にfilter()の条件が正しいか、count()で件数を確認してから削除しましょう。

Step 3関連データの削除動作

ForeignKeyのon_delete引数で、親レコード削除時の関連データの動作を指定できます。

Python
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=100)

class Product(models.Model):
    name = models.CharField(max_length=200)

    # CASCADE: 親削除時に子も削除(デフォルト)
    category = models.ForeignKey(
        Category, on_delete=models.CASCADE
    )

class Review(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    comment = models.TextField()

    # PROTECT: 子が存在する場合、親の削除を禁止
    # on_delete=models.PROTECT

    # SET_NULL: 親削除時にNULLをセット(null=True必須)
    # on_delete=models.SET_NULL, null=True

    # SET_DEFAULT: 親削除時にデフォルト値をセット
    # on_delete=models.SET_DEFAULT, default=1

    # DO_NOTHING: 何もしない(外部キー制約違反の可能性あり)
    # on_delete=models.DO_NOTHING
Python
# CASCADEの動作確認
category = Category.objects.get(id=1)  # "食品"カテゴリ
print(f"商品数: {category.product_set.count()}")

# カテゴリを削除すると、紐づく商品とレビューも全て削除される
result = category.delete()
print(result)

# PROTECTの場合はProtectedErrorが発生
from django.db.models import ProtectedError
try:
    category.delete()
except ProtectedError:
    print("関連データが存在するため削除できません")
on_deleteの選択指針
CASCADE: 注文→注文明細のように、親がなければ子も意味がない場合
PROTECT: ユーザー→投稿のように、誤削除を防ぎたい場合
SET_NULL: 担当者→タスクのように、親がなくても子は残したい場合

Step 4論理削除の実装

データを物理的に削除せず、フラグで論理的に削除する方法です。データの復元や履歴管理が必要な場合に使います。

Python
# models.py
from django.db import models
from django.utils import timezone

class SoftDeleteManager(models.Manager):
    """論理削除されていないデータのみ返すマネージャー"""
    def get_queryset(self):
        return super().get_queryset().filter(deleted_at__isnull=True)

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.IntegerField()
    deleted_at = models.DateTimeField(null=True, blank=True)

    # デフォルトのマネージャー(論理削除を除外)
    objects = SoftDeleteManager()
    # 全データ取得用のマネージャー
    all_objects = models.Manager()

    def soft_delete(self):
        """論理削除"""
        self.deleted_at = timezone.now()
        self.save(update_fields=["deleted_at"])

    def restore(self):
        """論理削除の取り消し"""
        self.deleted_at = None
        self.save(update_fields=["deleted_at"])
Python
# 論理削除の実行
product = Product.objects.get(id=1)
product.soft_delete()

# 通常のクエリでは論理削除されたデータは見えない
print(Product.objects.count())       # 論理削除を除外
print(Product.all_objects.count())   # 全データ(論理削除含む)

# 論理削除されたデータを復元
product = Product.all_objects.get(id=1)
product.restore()

Step 5削除時の注意点

Python
# 1. 削除前に件数を確認する
queryset = Product.objects.filter(is_active=False)
count = queryset.count()
print(f"削除予定: {count}件")

if count > 0:
    confirm = input("本当に削除しますか? (y/n): ")
    if confirm == "y":
        queryset.delete()

# 2. トランザクションで安全に削除する
from django.db import transaction

try:
    with transaction.atomic():
        # 関連データを先に削除
        Order.objects.filter(product_id=1).delete()
        # 親データを削除
        Product.objects.filter(id=1).delete()
except Exception as e:
    print(f"エラーが発生しました: {e}")
    # トランザクション内なので自動ロールバック

# 3. pre_delete / post_deleteシグナルを活用
from django.db.models.signals import pre_delete
from django.dispatch import receiver

@receiver(pre_delete, sender=Product)
def product_pre_delete(sender, instance, **kwargs):
    print(f"削除されます: {instance.name}")
    # ログ記録やバックアップ処理など

まとめ

  • delete()でインスタンスまたはQuerySetのレコードを削除できる
  • on_deleteで関連データの削除動作を制御する(CASCADE, PROTECT, SET_NULL等)
  • 論理削除はカスタムマネージャーとdeleted_atフィールドで実装できる
  • 削除前にcount()で件数を確認し、トランザクションで安全に実行する
  • 重要なデータは物理削除ではなく論理削除を検討する