djangoで論理削除を使いたい


今回のお題

今回のお題はdjangoアプリでの論理削除の使い方です。

ユーザーの論理削除(退会処理)はAbstractUserクラスに最初から実装されていますが他のモデルについては自分で用意する必要があるので、メモとしてまとめておきます。

論理削除とは

レコードを削除した後も情報を参照できるようにすること。

例えば会員制のサービスでは退会後も過去の利用履歴の確認などができるようにしておく必要があるため、物理削除(=通常の削除)ではなく論理削除が使われる。

内部的にはdeleted_atというフィールドを用意し、この属性値の有無を用いて削除されているかいないかを判定する。

django_boost

論理削除を手軽に実装できるパッケージ。

具体的な使い方は後述。

導入方法は、pipでのインストールとINSTALLED_APPSへの追加のみ。

ターミナル
% pip3 install django_boost
settings.py
INSTALLED_APPS = [
  # 中略
  "django_boost",# 追加
]

LogicalDeletionMixin

django_boostを導入することで実装可能になるクラス。

このクラスを継承させたモデルでは論理削除を扱うことができる。

Mixinと名前がついているがmodels.Modelを継承しているため、これ単体でモデルの親とすることが可能(汎用ビューのように多重継承する必要はない)。

LogicalDeletionMixinのソースコード
class LogicalDeletionMixin(models.Model):
    """Provide logical delete."""

    deleted_at = models.DateTimeField(
        verbose_name=_("deleted date"), blank=True, null=True, default=None, editable=False)

    class Meta:
        abstract = True

    objects = LogicalDeletionManager()

    @classmethod
    def get_deleted_value(cls):
        return now()

    def delete(self, using=None, keep_parents=False, hard=False):
        if hard:
            return super().delete(using=using, keep_parents=keep_parents)
        self.deleted_at = self.get_deleted_value()
        return self.save()

    def revive(self, force_update=False, using=None):
        """Revive logical deleted item."""
        self.deleted_at = None
        return self.save()

    def is_dead(self):
        """Return True if the item is dead."""
        return self.deleted_at is not None

    def is_alive(self):
        """Return True if record is alive, otherwise False."""
        return self.deleted_at is None

論理削除の使い方

# 対象のインスタンスを論理削除
item.delete()
# 既存のdelete()メソッドが論理削除に置き換わります(DeleteViewを通した場合などを含めて)。

# 物理削除
item.delete(hard=True)

# 論理削除された日時の取得
item.deleted_at # 論理削除実行時に自動で設定される

# 論理削除されているかどうかの判定
item.is_alive() # 削除済みならfalse
item.is_dead() # 削除済みならtrue
# テンプレートの{% %}で使う際には最後の()はつけない

# 論理削除の状態に合わせたレコードの前取得
Item.objects.alive() # 削除されていないもののみ
Item.objects.dead() # 削除済みのみ
Item.objects.all() # 削除の有無に関わらず全て
# こちらもテンプレートでは()をつけない