1. ホーム
  2. パイソン

[解決済み】djangoのビジネスロジックとデータアクセスの分離

2022-03-23 08:34:02

質問

私は Django でプロジェクトを書いていますが、コードの 80% はファイル models.py . このコードは混乱していて、ある時間が経つと、何が本当に起こっているのかわからなくなってしまいます。

ここで悩むのが

  1. 私は、モデルレベル(これは、本来は データベースからのデータの処理にのみ責任を負う) メールの送信、他のサービスへのAPIを歩く、など。
  2. また、ビジネスロジックをビューに配置することは受け入れられません。 この方法では、制御が難しくなります。例えば、私の場合 アプリケーションでは、少なくとも3つの方法で新しい のインスタンスを作成します。 User しかし、技術的には一律に作成されるべきです。
  3. メソッドと 自分のモデルのプロパティが非決定的になり、その結果 副作用があります。

ここで簡単な例を挙げます。最初は User モデルはこのようなものでした。

class User(db.Models):

    def get_present_name(self):
        return self.name or 'Anonymous'

    def activate(self):
        self.status = 'activated'
        self.save()

時間が経つと、こんな風になりました。

class User(db.Models):

    def get_present_name(self): 
        # property became non-deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

    def activate(self):
        # method now has a side effect (send message to user)
        self.status = 'activated'
        self.save()
        send_mail('Your account is activated!', '…', [self.email])

私が欲しいのは、コード内のエンティティを分離することです。

  1. 私のデータベースのエンティティ、永続化レベル。アプリケーションはどのようなデータを保持するのか?
  2. 私のアプリケーションのエンティティ、ビジネスロジックレベル。私のアプリケーションは何をするのか?

Djangoで適用できるこのようなアプローチを実装するためのグッドプラクティスは何でしょうか?

どのように解決するのですか?

の違いについてのご質問のようです。 データモデル ドメインモデル - 後者は、エンドユーザーが認識するビジネスロジックとエンティティを見つける場所であり、前者は実際にデータを保存する場所です。

さらに、質問の3番目の部分は、「これらのモデルを分離していないことにどう気づくか」ということだと解釈しています。

この2つは非常に異なる概念であり、これらを分けて考えることは常に困難です。しかし、この目的のために使える共通のパターンやツールがいくつかあります。

ドメインモデルについて

最初に認識すべきことは、ドメイン・モデルは実際にはデータに関するものではなく、次のようなものであるということです。 アクション 質問 例えば、「このユーザーをアクティブにする」「このユーザーを非アクティブにする」「現在アクティブになっているのはどのユーザーか」「このユーザーの名前は何か」などが挙げられます。古典的な言い方をすれば、次のようなことです。 クエリ コマンド .

コマンドで考える

まず、この例のコマンド、「"このユーザーをアクティブにする"」と「"このユーザーを非アクティブにする"」について見てみましょう。コマンドの良いところは、与えられた「いつ」「どんな」シナリオで簡単に表現できることです。

<ブロッククオート

与えられた 活動休止中のユーザー

いつ 管理者がこのユーザーをアクティブにしたとき

では ユーザーがアクティブになる

そして ユーザーに確認メールが送信される

そして システムログにエントリーが追加されます

(などなど)。

このようなシナリオは、インフラストラクチャのさまざまな部分が単一のコマンドによってどのように影響を受けるかを確認するのに便利です。この場合、データベース(ある種の「アクティブ」フラグ)、メールサーバー、システムログなどです。

このようなシナリオは、テスト駆動開発環境を構築する際にも非常に役に立ちます。

そして最後に、コマンドで考えることは、タスク指向のアプリケーションを作るのにとても役に立ちます。これはユーザーにも喜ばれるでしょう:-)

コマンドを表現する

Django はコマンドを表現する簡単な方法を 2 つ提供しています。どちらも有効なオプションで、2 つのアプローチを混ぜることは珍しくありません。

サービス層

サービスモジュール は、すでに Hedde氏による解説 . ここでは、別のモジュールを定義し、各コマンドは関数として表現されます。

サービス.py

def activate_user(user_id):
    user = User.objects.get(pk=user_id)

    # set active flag
    user.active = True
    user.save()

    # mail user
    send_mail(...)

    # etc etc

フォームを使う

もう一つの方法は、各コマンドに Django フォームを使うことです。私はこの方法を好みます。なぜなら、密接に関連する複数の側面を組み合わせることができるからです。

  • コマンドの実行(何をするのか?)
  • コマンドパラメータの検証(こんなことができるのか?)
  • コマンドの表示(どうすればいいのか?)

forms.py

class ActivateUserForm(forms.Form):

    user_id = IntegerField(widget = UsernameSelectWidget, verbose_name="Select a user to activate")
    # the username select widget is not a standard Django widget, I just made it up

    def clean_user_id(self):
        user_id = self.cleaned_data['user_id']
        if User.objects.get(pk=user_id).active:
            raise ValidationError("This user cannot be activated")
        # you can also check authorizations etc. 
        return user_id

    def execute(self):
        """
        This is not a standard method in the forms API; it is intended to replace the 
        'extract-data-from-form-in-view-and-do-stuff' pattern by a more testable pattern. 
        """
        user_id = self.cleaned_data['user_id']

        user = User.objects.get(pk=user_id)

        # set active flag
        user.active = True
        user.save()

        # mail user
        send_mail(...)

        # etc etc

クエリで考える

あなたの例にはクエリが含まれていなかったので、私は勝手に便利なクエリをいくつか作ってみました。私は質問という言葉を使いたいのですが、古典的な用語としてはクエリというものがあります。面白いクエリとしては、"このユーザーの名前は何ですか"、"このユーザーはログインできますか"、"無効にしたユーザーのリストを表示しますか"、"無効にしたユーザーの地域分布は何ですか".があります。

これらの問い合わせの回答に乗り出す前に、必ずこの質問を自分に投げかけてみてください、これは。

  • a プレゼンティブ クエリ、および/または
  • a ビジネスロジック コマンドの実行に関連するクエリ、および/または
  • a 報告 クエリを実行します。

プレゼンテーショナルクエリは、単にユーザーインターフェースを改善するために作られたものです。ビジネスロジッククエリの回答は、コマンドの実行に直接影響します。レポーティングクエリーは、単に分析を目的としたもので、時間的な制約が緩くなります。これらのカテゴリは相互に排他的ではありません。

例えば、ユーザー名を問い合わせる場合(この文脈では)、外部APIに依存しているため、結果を完全に制御することはできません。

クエリーの作成

Django で最も基本的なクエリは、マネージャオブジェクトを使うことです。

User.objects.filter(active=True)

もちろん、これはデータが実際にデータモデルで表現されている場合にのみ機能します。常にそうであるとは限りません。そのような場合は、以下のようなオプションを検討するとよいでしょう。

カスタムタグとフィルター

最初の選択肢は、単に体裁を整えるだけのクエリに有効です。カスタムタグとテンプレートフィルターです。

テンプレート.html

<h1>Welcome, {{ user|friendly_name }}</h1>

テンプレートタグ.py

@register.filter
def friendly_name(user):
    return remote_api.get_cached_name(user.id)

クエリーの方法

クエリが単に体裁を整えるだけのものでないなら、クエリを services.py (を使用する場合)、あるいは queries.py モジュールを使用します。

queries.py

def inactive_users():
    return User.objects.filter(active=False)


def users_called_publysher():
    for user in User.objects.all():
        if remote_api.get_cached_name(user.id) == "publysher":
            yield user 

プロキシモデル

プロキシモデルは、ビジネスロジックやレポーティングの文脈で非常に有用である。基本的には、モデルの拡張サブセットを定義します。をオーバーライドすることで、マネージャのベースクエリセットを上書きすることができます。 Manager.get_queryset() メソッドを使用します。

models.py

class InactiveUserManager(models.Manager):
    def get_queryset(self):
        query_set = super(InactiveUserManager, self).get_queryset()
        return query_set.filter(active=False)

class InactiveUser(User):
    """
    >>> for user in InactiveUser.objects.all():
    …        assert user.active is False 
    """

    objects = InactiveUserManager()
    class Meta:
        proxy = True

クエリモデル

本質的に複雑で、頻繁に実行されるようなクエリには、クエリモデルを使用することができます。クエリモデルは非正規化の一種で、一つのクエリに関連するデータは別のモデルに格納されます。もちろん、非正規化されたモデルを主モデルと同期させることがコツです。クエリモデルは、変更が完全に自分のコントロール下にある場合にのみ使用することができます。

models.py

class InactiveUserDistribution(models.Model):
    country = CharField(max_length=200)
    inactive_user_count = IntegerField(default=0)

最初のオプションは、コマンドの中でこれらのモデルを更新することです。この方法は、これらのモデルが1つか2つのコマンドによってのみ変更される場合、非常に便利です。

forms.py

class ActivateUserForm(forms.Form):
    # see above
   
    def execute(self):
        # see above
        query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
        query_model.inactive_user_count -= 1
        query_model.save()

より良い方法は、カスタム・シグナルを使用することです。これらのシグナルは、もちろん、あなたのコマンドによって発せられます。シグナルには、複数のクエリモデルを元のモデルと同期させておくことができるという利点があります。さらに、シグナルの処理はCeleryなどのフレームワークを用いてバックグラウンドタスクにオフロードすることができます。

シグナルズ.py

user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])

forms.py

class ActivateUserForm(forms.Form):
    # see above
   
    def execute(self):
        # see above
        user_activated.send_robust(sender=self, user=user)

models.py

class InactiveUserDistribution(models.Model):
    # see above

@receiver(user_activated)
def on_user_activated(sender, **kwargs):
        user = kwargs['user']
        query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
        query_model.inactive_user_count -= 1
        query_model.save()
    

清潔さを保つ

この方法を用いると、コードがクリーンであるかどうかを判断するのは、驚くほど簡単になります。以下のガイドラインに従うだけです。

  • 私のモデルには、データベースの状態を管理する以上のことを行うメソッドが含まれていますか?コマンドを抽出する必要があります。
  • 私のモデルには、データベースのフィールドにマッピングされないプロパティが含まれていますか?クエリを抽出する必要があります。
  • 私のモデルは、データベースではないインフラストラクチャ(メールなど)を参照していますか?コマンドを抽出する必要があります。

ビューも同様です(ビューはしばしば同じ問題に悩まされるため)。

  • 私のビューは、データベースモデルを積極的に管理していますか?コマンドを抽出する必要があります。

参考文献

Django ドキュメント: プロキシモデル

Django ドキュメント: シグナル

アーキテクチャ ドメイン駆動型設計