はじめに
Django には ORM でデータベースを扱うための基本的な要素として、モデル、マネージャー、クエリセット があります。それぞれ公式ドキュメントで以下のように説明されています。
モデル:
データに関する唯一かつ決定的な情報源です。あなたが保持するデータが必要とするフィールドとその動作を定義します。
マネージャー:
Django のモデルに対するデータベースクエリの操作を提供するインターフェイスです。
クエリセット:
作成、取得、更新および削除を行えるようにデータベースを抽象化した API
マネージャーの結合
前提
私の所属しているプロジェクトでは論理削除モデルとマルチテナント設計を導入しています。
論理削除モデルでは、レコードを物理削除せず deleted_at
カラムに削除日時を記録します。データ取得では deleted_at IS NULL
の条件で絞り込み、削除済みデータは除外します。
マルチテナント設計において、本プロジェクトでは1つのデータベースを全テナントで共有しています。ほぼ全てのモデルにtenant_id
を保持させ、データ取得時には常にこのtenant_id
によるフィルタリングを行うことで、テナントごとにデータを分離しています。
以下のように、クエリセットとマネージャーとモデルを定義しています。
クエリセット↓↓↓
from django.db.models import QuerySet
# 論理削除用のクエリセットクラス
class SoftDeleteQuerySet(QuerySet):
def delete(self):
...
マルチテナント用の QuerySet は作成しておりません。
マネージャー↓↓↓
from django.db.models import Manager
# 論理削除用のマネージャー
class SoftDeleteManager(Manager.from_queryset(SoftDeleteQuerySet)):
def get_queryset(self):
return super().get_queryset().filter(deleted_at__isnull=True)
# マルチテナント用のマネージャー
class TenantManager(Manager):
def get_queryset(self):
tenant_id = get_current_tenant_id()
return super().get_queryset().filter(tenant_id=tenant_id)
tenant_id はグローバルに取得できるものとします。
モデル↓↓↓
from django.db import models
# 論理削除用のモデル
class SoftDeleteModel(models.Model):
deleted_at = models.DateTimeField(...)
class Meta:
abstract = True
# テナント用のモデル
class TenantModel(models.Model):
tenant_id = models.ForeignKey(...)
class Meta:
abstract = True
SoftDeleteManager と TenantManager はまだ利用していません。
論理削除用のモデルは django-soft-delete ライブラリを用いていて、その内マネージャーだけ少し加工しています。
https://pypi.org/project/django-soft-delete/
要件
「論理削除済みデータ以外を取得する」「自テナントのデータのみを取得する」は高頻度で行うのでそれぞれマネージャーとクエリセットを作成し、様々なモデルで自動的にこれらのフィルタリングを行うように実装します。
その際、以下のような要件を満たす必要がありました。
- 全てのモデルで論理削除を用いるわけではない
- たまに全テナントのデータを取得したいときがある
- モデル特有のマネージャーとクエリセットを適用できる柔軟性は必須
ゆえに、DRY原則、単一責任の原則、拡張性を考慮すると以下のマネージャーでは都合が悪いのです。
super().get_queryset().filter(deleted_at__isnull=True).filter(tenant_id=tenant_id)
Django では objects
に定義できるマネージャーは1つだけであり、また、マネージャー同士を結合する手段は標準では提供されていません(Django の設計として、マネージャーは単一の役割を持つことが前提なのかもしれません)。
ただ、実際の開発では「論理削除」と「テナントフィルタ」といった、複数の共通処理を併用する必要がありました。この課題を解決するため考案したのが クエリセットとマネージャーを動的に結合する独自メソッド の作成でした。
クエリセットとマネージャーを結合する独自メソッド
from django.db.models import Manager, QuerySet
def _create_manager(
queryset: Optional[Type[QuerySet]] = None,
manager: Optional[Type[Manager]] = None,
soft_delete: bool = False,
all_tenant: bool = False
) -> Type[Manager]:
querysets = [queryset] if queryset is not None else []
managers = [manager] if manager is not None else []
# 論理削除モデルの場合
if soft_delete:
querysets.append(SoftDeleteQuerySet)
managers.append(SoftDeleteManager)
# 自テナントのみのデータが必要な場合
if not all_tenant:
managers.append(TenantManager)
# カスタムクエリセット。*querysetsを先に継承させる
class CustomQueryset(*querysets, QuerySet):
pass
# カスタムマネージャー。*managersを後に継承させる
class CustomManager(Manager.from_queryset(CustomQueryset), *managers):
pass
return CustomManager()
# 物理削除を行うマネージャーを作成する
def create_base_manager(queryset=None, manager=None, all_tenant=False):
return _create_manager(
queryset=queryset,
manager=manager,
soft_delete=False,
all_tenant=all_tenant
)
# 論理削除を行うマネージャーを作成する
def create_soft_delete_manager(queryset=None, manager=None, all_tenant=False):
return _create_manager(
queryset=queryset,
manager=manager,
soft_delete=True,
all_tenant=all_tenant
)
クエリセットクラスとマネージャークラスを引数にして、カスタマイズしたマネージャークラスを返す _create_manager
メソッドを作成しました。soft_delete
と all_tenant
引数を真偽値で受け取ることで、論理削除とマルチテナント設計のモデル用のマネージャーを作成することができます。
論理削除を行うモデルと行わないモデルは半々ぐらいだと思ったので、soft_delete
の値だけを変えた、create_base_manager
メソッドと create_soft_delete_manager
メソッドも作成しました。
これらを用いて、前提 で記載したモデルを修正します。
クエリセットとマネージャーは同じものを利用します。
from django.db import models
# 論理削除用のモデル
class SoftDeleteModel(models.Model):
deleted_at = models.DateTimeField(...)
class Meta:
abstract = True
# テナント用のモデル
class TenantModel(models.Model):
tenant_id = models.ForeignKey(...)
class Meta:
abstract = True
# 物理削除用の抽象モデル
class CustomBaseModel(TenantModel):
objects = create_base_manager()
all_tenant_objects = create_base_manager(all_tenant=True)
class Meta:
abstract = True
# 論理削除用の抽象モデル
class CustomSoftDeleteModel(SoftDeleteModel, TenantModel):
objects = create_soft_delete_manager()
all_tenant_objects = create_soft_delete_manager(all_tenant=True)
class Meta:
abstract = True
SoftDeleteModel
と TenantModel
を継承した CustomSoftDeleteModel
を別のモデルがさらに継承することで、そのモデルはデフォルトで論理削除とマルチテナントに対応したものとなります。
あるモデルに特有のクエリセットとマネージャーを利用したい場合は、以下のように使えます。
from django.db import models
# モデル特有のクエリセット
class YourQuerySet(models.QuerySet):
def your_method(self):
...
# モデル特有のクエリセット
class YourManager(models.Manager):
def get_queryset(self):
return super().get_queryset().xxx()
# モデル定義
class YourModel(CustomSoftDeleteModel):
your_column = models.XXXField()
objects = create_soft_delete_manager(
queryset=YourQuerySet,
manager=YourManager
)
all_tenant_objects = create_soft_delete_manager(
queryset=YourQuerySet,
manager=YourManager,
all_tenant=True
)
YourModel
は独自のYourQuerySet
とYourManager
を用いつつ、論理削除モデルとマルチテナント設計が適用されています。all_tenant_objects
も用意することで全テナントのデータにアクセスする方法も残してあります。
さいごに
マネージャー同士の結合に関して検索しても、有効な実装例やベストプラクティスを見つけられませんでした。そこで、Django の設計思想を踏まえつつ、ChatGPT を活用しながら独自にクエリセットとマネージャーを動的に合成する方法を設計・実装しました。
本記事で紹介したアプローチは、現時点でのベターな解として運用に耐えうる手応えを得ていますが、まだ発展途上でもあります。今後も実践の中で検証と改善を重ね、より堅牢で再利用性の高いモジュールへと進化させていく予定です。