0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Django】複数のマネージャーを結合してみました

Posted at

はじめに

Django には ORM でデータベースを扱うための基本的な要素として、モデル、マネージャー、クエリセット があります。それぞれ公式ドキュメントで以下のように説明されています。

モデル:
データに関する唯一かつ決定的な情報源です。あなたが保持するデータが必要とするフィールドとその動作を定義します。

マネージャー:
Django のモデルに対するデータベースクエリの操作を提供するインターフェイスです。

クエリセット:
作成、取得、更新および削除を行えるようにデータベースを抽象化した API

マネージャーの結合

前提

私の所属しているプロジェクトでは論理削除モデルマルチテナント設計を導入しています。
論理削除モデルでは、レコードを物理削除せず deleted_at カラムに削除日時を記録します。データ取得では deleted_at IS NULL の条件で絞り込み、削除済みデータは除外します。
マルチテナント設計において、本プロジェクトでは1つのデータベースを全テナントで共有しています。ほぼ全てのモデルにtenant_idを保持させ、データ取得時には常にこのtenant_idによるフィルタリングを行うことで、テナントごとにデータを分離しています。

以下のように、クエリセットとマネージャーとモデルを定義しています。

クエリセット↓↓↓

QuerySet
from django.db.models import QuerySet

# 論理削除用のクエリセットクラス
class SoftDeleteQuerySet(QuerySet):
    def delete(self):
        ...

マルチテナント用の QuerySet は作成しておりません。

マネージャー↓↓↓

Manager
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 はグローバルに取得できるものとします。

モデル↓↓↓

Model
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 の設計として、マネージャーは単一の役割を持つことが前提なのかもしれません)。
ただ、実際の開発では「論理削除」と「テナントフィルタ」といった、複数の共通処理を併用する必要がありました。この課題を解決するため考案したのが クエリセットとマネージャーを動的に結合する独自メソッド の作成でした。

クエリセットとマネージャーを結合する独自メソッド

_create_manager
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()
base_manager
# 物理削除を行うマネージャーを作成する
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_deleteall_tenant 引数を真偽値で受け取ることで、論理削除とマルチテナント設計のモデル用のマネージャーを作成することができます。
論理削除を行うモデルと行わないモデルは半々ぐらいだと思ったので、soft_deleteの値だけを変えた、create_base_manager メソッドと create_soft_delete_manager メソッドも作成しました。
これらを用いて、前提 で記載したモデルを修正します。

クエリセットとマネージャーは同じものを利用します。

Model
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

SoftDeleteModelTenantModel を継承した CustomSoftDeleteModel を別のモデルがさらに継承することで、そのモデルはデフォルトで論理削除とマルチテナントに対応したものとなります。
あるモデルに特有のクエリセットとマネージャーを利用したい場合は、以下のように使えます。

your_model
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 は独自のYourQuerySetYourManagerを用いつつ、論理削除モデルとマルチテナント設計が適用されています。all_tenant_objects も用意することで全テナントのデータにアクセスする方法も残してあります。

さいごに

マネージャー同士の結合に関して検索しても、有効な実装例やベストプラクティスを見つけられませんでした。そこで、Django の設計思想を踏まえつつ、ChatGPT を活用しながら独自にクエリセットとマネージャーを動的に合成する方法を設計・実装しました。
本記事で紹介したアプローチは、現時点でのベターな解として運用に耐えうる手応えを得ていますが、まだ発展途上でもあります。今後も実践の中で検証と改善を重ね、より堅牢で再利用性の高いモジュールへと進化させていく予定です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?