はじめに
タイトルの通り Django を使っていて
「論理削除でも、関連テーブルのレコードを削除したい!どうにかやれないかなぁ?」
という内容です。
(論理削除は使わない方がいいのでは?関連レコードは削除しなくていいのでは?というのは置いておくとして)
Django の論理削除の実装については、調べればたくさん情報が出てくるので割愛します。
サンプルとして、以下のように Blog, Post, Comment
の三つの Model を用意し、全てを論理削除としました。
from django.db import models
from django.db.models.manager import BaseManager
from django.utils import timezone
class LogicalDeletionQuerySet(models.QuerySet):
def delete(self):
now = timezone.now()
return super().update(**{'deleted_at': now})
class LogicalDeletionManager(BaseManager.from_queryset(LogicalDeletionQuerySet)):
def get_queryset(self):
query_set = super().get_queryset()
return query_set.filter(deleted_at__isnull=True)
class LogicalDeletionModel(models.Model):
class Meta:
abstract = True
deleted_at = models.DateTimeField(null=True, default=None)
objects = LogicalDeletionManager()
all_objects = models.Manager()
def delete(self, **kwargs):
now = timezone.now()
self.deleted_at = now
self.save()
class Blog(LogicalDeletionModel):
name = models.CharField(max_length=255)
class Post(LogicalDeletionModel):
title = models.CharField(max_length=255)
content = models.TextField()
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
class Comment(LogicalDeletionModel):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
content = models.TextField()
この上で、on_delete=models.CASCADE
の場合、紐づくレコードが全て削除されるように変更を加えていきます。
before
>>> blog = Blog.objects.create(name='blog')
>>> post = blog.post_set.create(title='post')
>>> comment = post.comment_set.create(content='comment')
>>> post.delete()
>>> Post.objects.all()
<LogicalDeletionQuerySet []>
>>> Comment.objects.all()
<QuerySet [<Comment: Comment object (1)>]>
やること
-
model_instance.delete()
でCASCADE DELETE
-
model_queryset.delete()
でCASCADE DELETE
1. model_instance.delete()
で CASCADE DELETE
まずは LogicalDeletionModel
の delete()
に修正を加えます。
before
class LogicalDeletionModel(models.Model):
def delete(self, **kwargs):
now = timezone.now()
self.deleted_at = now
self.save()
これを実現するためには
- 削除されたインスタンスのモデルが ForeignKey として設定されているモデルの一覧
- ForeignKey のフィールド名
- ForeignKey の on_delete
の全てが取得出来る必要があります。
それが完了すれば
Model.objects.filter(field_name=deleted_instance).delete()
とすることで実装が完了します。
まず
削除されたインスタンスのモデルが ForeignKey として設定されているモデルの一覧は
instance._meta.related_objects
にアクセスすることで取得出来ました。
>>> blog._meta.related_objects
(<ManyToOneRel: blogs.post>,)
>>> post._meta.related_objects
(<ManyToOneRel: blogs.comment>,)
>>> comment._meta.related_objects
()
次にそのクラスを取得します。
instance._meta.related_objects
で取得出来たオブジェクトの
related_model
で取得出来ました。
>>> blog._meta.related_objects[0].related_model
<class 'blogs.models.Post'>
フィールド名は related_object
の field.name
で取得出来ました。
>>> blog._meta.related_objects[0].field.name
'blog'
ForeignKey の on_delete は related_object
の on_delete
で取得できました。
>>> blog._meta.related_objects[0].on_delete
<function CASCADE at 0x7f8be5cbb830>
これらを使って model_instance.delete()
での CASCADE DELETE
を実装することができました。
after
class LogicalDeletionModel(models.Model):
def delete(self, **kwargs):
now = timezone.now()
self.deleted_at = now
self.save()
related_objects = self._meta.related_objects
for related_object in related_objects:
if related_object.on_delete == models.CASCADE:
related_model = related_object.related_model
related_field_name = related_object.field.name
related_model.objects.filter(**{related_field_name: self}).delete()
>>> blog = Blog.objects.create(name='blog')
>>> post = blog.post_set.create(title='post')
>>> comment1 = post.comment_set.create(content='comment1')
>>> comment2 = post.comment_set.create(content='comment2')
>>> post.delete()
>>> Post.objects.all()
<LogicalDeletionQuerySet []>
>>> Comment.objects.all()
<QuerySet []>
2. model_queryset.delete()
で CASCADE DELETE
こちらは
- 削除された queryset のモデル
- 削除された queryset のモデルが ForeignKey として設定されているモデルの一覧
- ForeignKey のフィールド名
- ForeignKey の on_delete
を取得することで実装できます。
削除された queryset のモデルは queryset.model
で取得できます。
>>> blogs = Blog.objects.all()
>>> blogs.model
<class 'blogs.models.Blog'>
ここからは 1 の instance._meta
を model._meta
に置き換えるだけで全く同じです。
これを踏まえて LogicalDeletionQuerySet
の delete の実装を更新します。
before
class LogicalDeletionQuerySet(models.QuerySet):
def delete(self):
now = timezone.now()
return super().update(**{'deleted_at': now})
after
class LogicalDeletionQuerySet(models.QuerySet):
def delete(self):
queryset_model = self.model
related_objects = queryset_model._meta.related_objects
for related_object in related_objects:
if related_object.on_delete == models.CASCADE:
related_model = related_object.related_model
related_field_name = f'{related_object.field.name}__in'
related_model.objects.filter(**{related_field_name: self}).delete()
now = timezone.now()
return = super().update(**{'deleted_at': now})
では試してみます。
>>> blog = Blog.objects.create(name='blog')
>>> post = blog.post_set.create(title='post')
>>> comment = post.comment_set.create(content='comment')
>>> Blog.objects.all().delete()
1
>>> Blog.objects.all(), Post.objects.all(), Comment.objects.all()
(<LogicalDeletionQuerySet []>, <LogicalDeletionQuerySet []>, <LogicalDeletionQuerySet []>)
>>> Blog.all_objects.all(), Post.all_objects.all(), Comment.all_objects.all()
(<QuerySet [<Blog: Blog object (1)>]>, <QuerySet [<Post: Post object (1)>]>, <QuerySet [<Comment: Comment object (1)>]>)
うまくいきました。
おわり
少し強引に実装してしまったかな?と思ってはいますが
「論理削除でも、関連テーブルのレコードを削除したい!」
を実現することができました。
より良い方法があれば是非教えてください!
ありがとうございました!!
ここに書いたソースコードはこちらにあげています
https://github.com/donaisore/django-docker/tree/logical_deletion_cascade