Separation of business logic and data access in django Ask Question

Separation of business logic and data access in django Ask Question

I am writing a project in Django and I see that 80% of the code is in the file models.py. This code is confusing and, after a certain time, I cease to understand what is really happening.

Here is what bothers me:

  1. I find it ugly that my model level (which was supposed to be responsible only for the work with data from a database) is also sending email, walking on API to other services, etc.
  2. Also, I find it unacceptable to place business logic in the view, because this way it becomes difficult to control. For example, in my application there are at least three ways to create new instances of User, but technically it should create them uniformly.
  3. I do not always notice when the methods and properties of my models become non-deterministic and when they develop side effects.

Here is a simple example. At first, the User model was like this:

class User(db.Models):

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

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

Over time, it turned into this:

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])

What I want is to separate entities in my code:

  1. Database level entities, i.e. database level logic: What kind of data does my application store?
  2. application level entities, i.e. business level logic: What does my application do?

What are the good practices to implement such an approach that can be applied in Django?

ベストアンサー1

It seems like you are asking about the difference between the data model and the domain model – the latter is where you can find the business logic and entities as perceived by your end user, the former is where you actually store your data.

Furthermore, I've interpreted the 3rd part of your question as: how to notice failure to keep these models separate.

These are two very different concepts and it's always hard to keep them separate. However, there are some common patterns and tools that can be used for this purpose.

About the Domain Model

The first thing you need to recognize is that your domain model is not really about data; it is about actions and questions such as "activate this user", "deactivate this user", "which users are currently activated?", and "what is this user's name?". In classical terms: it's about queries and commands.

Thinking in Commands

Let's start by looking at the commands in your example: "activate this user" and "deactivate this user". The nice thing about commands is that they can easily be expressed by small given-when-then scenario's:

given an inactive user
when the admin activates this user
then the user becomes active
and a confirmation e-mail is sent to the user
and an entry is added to the system log
(etc. etc.)

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

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

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

コマンドの表現

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

フォームの使用

もう 1 つの方法は、各コマンドに Django フォームを使用することです。この方法は、複数の密接に関連する側面を組み合わせているため、私はこの方法を好みます。

  • コマンドの実行(何をするのか?)
  • コマンドパラメータの検証(これが可能か?)
  • コマンドの提示 (どうすればいいでしょうか?)

フォーム.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

クエリで考える

例にはクエリが含まれていなかったので、役に立つクエリをいくつか作ってみました。私は「質問」という用語を使うことを好みますが、クエリは古典的な用語です。興味深いクエリは、「このユーザーの名前は何ですか?」、「このユーザーはログインできますか?」、「非アクティブ化されたユーザーのリストを表示してください」、「非アクティブ化されたユーザーの地理的分布は?」などです。

これらの質問に答え始める前に、常に自分自身に次の質問をする必要があります。

  • 私のテンプレート専用のプレゼンテーションクエリ、および/または
  • コマンドの実行に関連するビジネスロジッククエリ、および/または
  • レポートクエリ。

プレゼンテーション クエリは、ユーザー インターフェイスを改善するためだけに作成されます。ビジネス ロジック クエリへの回答は、コマンドの実行に直接影響します。レポート クエリは分析目的のみで、時間的制約は緩やかです。これらのカテゴリは相互に排他的ではありません。

もう 1 つの質問は、「回答を完全に制御できますか?」です。たとえば、ユーザーの名前を照会する場合 (このコンテキストでは)、外部 API に依存しているため、結果を制御することはできません。

クエリの作成

Django で最も基本的なクエリは、Manager オブジェクトの使用です。

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 (使用している場合) にクエリを追加するか、querys.pyモジュールを導入することができます。

クエリ.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()方法。

モデル.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

クエリモデル

本質的に複雑だが頻繁に実行されるクエリの場合、クエリ モデルが考えられます。クエリ モデルは非正規化の形式であり、単一のクエリの関連データが別のモデルに格納されます。もちろん、秘訣は非正規化モデルをプライマリ モデルと同期させることです。クエリ モデルは、変更が完全に制御できる場合にのみ使用できます。

モデル.py

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

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

フォーム.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'])

フォーム.py

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

モデル.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 ドキュメント: シグナル

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

おすすめ記事