DjangoのQuerySetのdelete()について理解する

QuerySetのdelete()の挙動について教えてもらった & 自分でも改めて手を動かして理解してみました。

結論

delete()は単純に指定したModelが削除されるだけではなく返り値も存在します(=ここがまずちゃんと分かっていなかった所でした)。

具体的にはタプルで以下の値を受け取ることができます。

  • 削除されたオブジェクトの数
  • 削除されたオブジェクトのアプリ名・モデル名と削除されたオブジェクトの数

改めて知ってみると、何がどう削除されたかという情報は知りたい情報だと思うので「それはそうだよな」という気持ちになりました。

コードの例

とはいえ、文字だけだと分かりにくいので、実際に結果を見てみましょう。

https://github.com/nibuno/djangoTaskApp で書いている簡易的なTask管理アプリケーションの例です。

以下のModelが存在しています。

  • User
  • Task
>>> user = User.objects.first()
>>> Task.objects.create(title="サンプル",created_user=user,updated_user=user)
 <Task: Task object (1)>
>>> Task.objects.create(title="サンプル",created_user=user,updated_user=user)
 <Task: Task object (2)>
>>> Task.objects.create(title="サンプル",created_user=user,updated_user=user)
 <Task: Task object (3)>
>>> Task.objects.filter(title="サンプル").delete()
 (3, {'tasks.Task': 3})

はじめにユーザーを取得して、生成後、削除しています。

最後の結果にあるように(3, {'tasks.Task': 3})のように削除されたオブジェクトの数と削除されたオブジェクトのアプリ名・モデル名と削除されたオブジェクトの数が受け取られることがわかりました。

上記だと、1つのModelだけしか削除されていませんが、他のModelも一緒に削除された場合は以下のようになります。

こちらは公式リファレンスから引用しています。

>>> b = Blog.objects.get(pk=1)
>>> Entry.objects.filter(blog=b).delete()
(4, {'blog.Entry': 2, 'blog.Entry_authors': 2})

https://docs.djangoproject.com/en/5.0/ref/models/querysets/#delete より

delete()が使えない例

公式リファレンスには、以下のように書かれています。

You cannot call delete() on a QuerySet that has had a slice taken or can otherwise no longer be filtered.

この点についてはすぐ理解出来なかったのですが、

  • limit
  • offset
  • distinct
  • values
  • value_list

を利用したものが使えない、とコードを見て理解出来ました。

実際のコードはこのようになっています。

if self.query.is_sliced:
    raise TypeError("Cannot use 'limit' or 'offset' with delete().")
if self.query.distinct_fields:
    raise TypeError("Cannot call delete() after .distinct(*fields).")
if self._fields is not None:
    raise TypeError("Cannot call delete() after .values() or .values_list()")

https://github.com/django/django/blob/b47bdb4cd9149ee2a39bf1cc9996a36a940bd7d9/django/db/models/query.py#L1164 より抜粋

そのため、例えば以下のようなコードを実行しようとすると、TypeErrorが発生することが分かりました。

>>> Task.objects.all()[:10].delete()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/tatsuya/djangoTaskApp/venv/lib/python3.12/site-packages/django/db/models/query.py", line 1128, in delete
    raise TypeError("Cannot use 'limit' or 'offset' with delete().")
TypeError: Cannot use 'limit' or 'offset' with delete().

このようなケースでは、SQL的には余り望ましく無いと思いますが、for文で取得した後に削除するケースになるかと思います。

for task in Task.objects.filter(title="サンプル"):
    task.delete()

その他

以下のような事もdelete()の挙動に含まれるようでした。

  • delete()ではForeignKeyon_deleteに従って関連モデルの削除の可否が決まる
  • SQLで削除するため、関連モデルのdelete()が常に呼び出される訳ではない
  • delete()をカスタマイズしているので、必ず呼び出す必要がある時は繰り返し処理で呼び出す(本ブログでいう、for文で取得後の削除処理のような)

この辺りはまた別の記事にもまとめていけたらなと思います。