Django の ProxyModelについて


Django には ProxyModel というのがある。これはどうに使うと便利で、どういう時に活躍するものなのか?

プロキシモデル
https://docs.djangoproject.com/en/3.0/topics/db/models/#proxy-models

単に説明だけみるとこういう事です。

モデルのPythonの動作のみを変更したい場合があります。
デフォルトのマネージャーを変更したり、新しいメソッドを追加したりする場合があります。

しかし、色々と痒いところがあるようですし、便利に使うにはどうしたら良いのかは悩みどころです。あくまでも目的があっての手段ですから、無理やり使いたいわけではありませんし。
Right way to return proxy model instance from a base model instance in Django?

とりあえず、こういう時はどうでしょうか?

Djangoはプロジェクト内にいくつもアプリケーションを持てる構造です。よくあるパターンとしては、Djangoで管理画面(Django Adminではなく中の人用の管理ツール)、DRFでAPIを作るケースでしょうか? こういう時に、この2つのアプリではDBは共通で使いたいことが多いはずです。例えば規模が大きいサービスではマイクロサービス化され、そういう分け方(管理の仕方)ではないかもしれませんが、何よりも先に成果がほしいスタートアップでは、こういう構成は結構多いと思います。

こういう構成を取る時に、Django,DRFではモデルを共通して使う方法を取る必要がありますが、これは検索すれば結構簡単に見つかりますし、システムの出来上がりも実装もこれだけで基本的には何も問題がありません。

modelにたくさんメソッド書きますか?

DjangoのORMは優秀で、ほぼ生のSQLを書く機会はありませんが、viewにORMの命令を書いて良いとは思っていません。こういうのですが。

class MyViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    queryset = schema.MyObject.objects.all().order_by('id')
    serializer_class = MyObjectSerializer

これって昔からよく言われる ControllerにSQLを書くんじゃねーってのと何が違うのでしょうか?
こんな感じにするともっとその駄目さがわかるでしょうか?

class MyViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    queryset = schema.MyObject.objects.filter(type=schema.MyObject.TYPE.OPEN).order_by('id')
    serializer_class = MyObjectSerializer

ということで、modelにメソッドを書くことが良いと思っています。

class MyViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    queryset = schema.MyObject.filter_of_open_object()
    serializer_class = MyObjectSerializer

class MyObject(models.Model):
    @classmethod
    def filter_of_open_object(cls):
        return cls..objects.filter(type=schema.MyObject.TYPE.OPEN).order_by('id')

こんな感じ。

別のアプリに関係ないメソッドを共通で利用するモデルに書くのってどうなの??

例で言えばAdminアプリに必要な論理削除されたデータすらも含めて一覧するLIST系のQUERYはAPI側には不要です。不要というより間違えて叩かれると(実装ミスですが)、見せてはならないデータが外に出てしまうことになります。

こういう時にProxyModelが使えます。

こういう感じです。

Modelメソッドと、ProxyModelの注意点

ProxyModelの書き方例

class Article(XModel):
    TYPE = Choices(
        ('DRAFT',   '下書き'),
        ('PUBLISH', '公開中'),
    )

    type = StatusField(
        choices_name='TYPE',
        blank=False,
        default=TYPE.DRAFT,
        help_text="記事の状態",
    )

    text = models.TextField(
        null=False,
        blank=False,
        default="",
        help_text="記事の内容",
    )

    writer = models.ForeignKey(
        to="Account",
        related_name="articles",
        null=False,
        on_delete=models.DO_NOTHING,
        help_text="記事の記入者",
    )

class ArticleProxy(ProxyModel, schema.Article):

    @classmethod
    def list_of_draft(cls):
        return cls.objects.filter(type=cls.TYPE.DRAFT)

こんな形で書いてください。ポイントは cls.objects.filterです。意味合いは同じでも schema.Article.objects.filterだと、戻り値は Articleの配列になります(厳密にはQuerySetであり配列じゃないですが)

Model(Proxy)にメソッドを書く注意点

class ArticleProxy(ProxyModel, schema.Article):

    @classmethod
    def list_of_future(cls):
        return cls.objects.filter(resavation_at=now())


class MyViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    queryset = ArticleProxy.list_of_future()
    serializer_class = ArticleSerializer

これ、バグってるの分かりますか? now()は一度しか評価されず、リクエストのある度に計算されないです。
この問題の本質は、本来メソッド(古い言い方だと関数)はブラックボックスであり中身を知らなくても呼べるべきことところです。この例だとViewの実装者は普通に「便利なメソッドがあるな、使おう!」ってなりますよね。

なぜ、こういうこだわりをするのか?

爆速で事業を立ち上げるスタートアップでは、コミュニケーションすらも無駄だという考え方が存在しています(僕の中には)。ホウレンソウからザッソウに変わりながらも 1on1が推奨されたり、マネジメントを重要視したりしている時代な気がしていますが、XTechではマネジメントが必要な組織は古い、それぞれがプロではないから、そういうのが必要なんだと考えています。(僕のキャリアで一番長いのはマネジメントですがw)

要するに実装をするときにでも、コミュニケーションを取らなくても分かる、普通に作っていれば変なバグやセキュリティホールを作らないというフレームワークは生産性を上げるために大事だと考えています。

ProxyModelのススメ

各アプリケーションを実装する人は、ProxyModel上にメソッドを追加したり、それを利用したりする。普通に利用していれば管理者しかアクセスしないデータにはアクセスできない(そういうメソッドを書かない限り)

最後に

上の例であげたModelにメソッドを書いて、それがブラックボックス的に利用されてバグを踏むケースも回避策があります。これは皆さんが勝手に想像するか、次のコンテンツにでもしようかなと思ってます。

走り書きなので誤字脱字、変なところがあると思いますので、コメント頂ければ時間作って見直します!