【Python】Djangoモデル設計:「is_completed」フラグは悪手か?日時で状態を管理する利点

Djangoでモデルを設計する際、「タスクが完了したか(is_completed)」、「ユーザーが有効か(is_active)」といった状態を示すために、BooleanField(ブール値フラグ)を追加するのは、直感的で簡単な方法です。

しかし、この一見シンプルなアプローチは、アプリケーションが成長するにつれて大きな技術的負債となる可能性があります。なぜなら、ブール値フラグは**「いつ」**という非常に重要な時間的文脈を欠落させてしまうからです。

今回は、安易なフラグ管理をやめ、代わりに「日時(タイムスタンプ)」で状態を管理することの絶大なメリットを、ToDoリストの「タスク」モデルを例に解説します。

目次

問題点:ブール値と日時が引き起こす「データの不整合」

ToDoアプリケーションのTaskモデルを設計する際、タスクが完了したかどうかをBooleanFieldで管理し、さらに「いつ完了したか」も知りたくなったため、DateTimeFieldも追加したとします。

悪い例:状態(is_completed)と日時(completed_at)が併存している

from django.db import models

class Task(models.Model):
    title = models.CharField("タスク名", max_length=255)
    is_completed = models.BooleanField("完了フラグ", default=False)
    completed_at = models.DateTimeField("完了日時", null=True, blank=True)

この設計には、データの**「信頼できる唯一の情報源(Single Source of Truth)」**が存在しないという根本的な欠陥があります。これにより、以下のような矛盾したデータ状態が生まれるリスクがあります。

  • is_completed = True なのに completed_at = None
  • is_completed = False なのに completed_at = (日時の値)

このようなデータの不整合は、アプリケーションロジックを非常に複雑にします。タスクを完了させる処理は、常に2つのフィールドを同時に、かつ正確に更新することを強いられます。

# 悪いロジック:2つのフィールドを同期させる必要がある
def complete_task(task: Task):
    task.is_completed = True
    task.completed_at = timezone.now()
    task.save()

解決策:「日時」を唯一の情報源とする

この問題を解決するクリーンなアプローチは、is_completedというブール値フラグを完全に削除し、completed_at(完了日時)フィールドの存在有無をもって状態を定義することです。

  • completed_atNULLNone)である → 未完了
  • completed_atが日時の値を持つ → 完了

良い例:completed_atフィールドだけで状態を管理する

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

class Task(models.Model):
    title = models.CharField("タスク名", max_length=255)
    
    # このフィールドが、状態と日時の両方を管理する唯一の情報源
    completed_at = models.DateTimeField("完了日時", null=True, blank=True)

この設計により、タスクを完了させる処理は劇的にシンプルになり、データの不整合が発生する余地はなくなります。

# 良いロジック:一箇所の更新で状態が確定する
def complete_task(task: Task):
    task.completed_at = timezone.now()
    task.save()

派生的な「状態」を提供する方法

is_completedフラグがなくなると、テンプレートやif文で使いにくいのでは?」と思うかもしれません。この問題は、モデルに状態を問い合わせるためのプロパティやメソッドを定義することで、エレガントに解決できます。

1. @propertyで状態を派生させる

モデルインスタンスから「完了しているか」というブール値にアクセスしたい場合は、@propertyを使います。

class Task(models.Model):
    # ... (フィールド定義は上記と同じ) ...

    @property
    def is_completed(self) -> bool:
        """このタスクが完了しているかどうかを返す。"""
        return self.completed_at is not None

これにより、if task.is_completed: という、以前とまったく同じ直感的なコードを使いながら、内部的にはcompleted_atという信頼できる情報源を参照できます。

2. Managerでクエリを単純化する

「完了済みタスク一覧」や「未完了タスク一覧」を取得するためのクエリも、カスタムManagerにまとめることで非常にクリーンになります。

class TaskQuerySet(models.QuerySet):
    def completed(self):
        """完了済みのタスクのみを返す。"""
        return self.filter(completed_at__isnull=False)
        
    def pending(self):
        """未完了のタスクのみを返す。"""
        return self.filter(completed_at__isnull=True)

class Task(models.Model):
    title = models.CharField("タスク名", max_length=255)
    completed_at = models.DateTimeField("完了日時", null=True, blank=True)
    
    objects = TaskQuerySet.as_manager()

    @property
    def is_completed(self) -> bool:
        return self.completed_at is not None

# 呼び出し側が非常にクリーンになる
# Task.objects.filter(is_completed=True) ではなく
completed_tasks = Task.objects.completed()
# Task.objects.filter(completed_at__isnull=True) ではなく
pending_tasks = Task.objects.pending()

日時で管理する圧倒的なメリット

  1. データの整合性: completed_atという「信頼できる唯一の情報源」を持つことで、データが矛盾した状態になることを防ぎます。
  2. 豊富なコンテキストの獲得: ブール値のTrueが失っていた「いつ?」という情報を保持できます。これにより、「昨日完了したタスクの数」や「今週の達成率」といった、将来の分析的な機能要求にも即座に対応できます。
  3. ロジックの単純化: 状態を更新するロジックがシンプルになり、バグの混入を防ぎます。

まとめ

モデルに状態を追加したくなった時、安易にBooleanField(例: is_xxx)を追加する前に、一度立ち止まってみましょう。

「その状態は、何かが『行われた』時点、あるいは『開始された』時点を示していないか?」

もしそうであれば、BooleanFieldの代わりにDateTimeField(例: activated_at, completed_at, deleted_at)を使うことを強く推奨します。ブール値の「状態」は、その日時フィールドからいつでも派生させることができるのです。この設計が、あなたのアプリケーションをより堅牢で、拡張性の高いものにしてくれます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次