djangoでのビジネスロジックとデータアクセスの分離


484

私は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に適用できるこのようなアプローチを実装するための良い方法は何ですか?


14
信号について読む
Konstant

1
タグを削除しましたが、DCIを使用して、システムの機能(機能)とシステム(データ/ドメインモデル)の分離を完成させることができます
Rune FS

2
シグナルコールバックにすべてのビジネスロジックを実装することを提案しますか?残念ながら、私のアプリケーションのすべてがデータベース内のイベントにリンクできるわけではありません。
defuz 2012

ルーンFS、私はDCIを使用しようとしましたが、私のプロジェクトにはそれほど必要ではないように思われました:コンテキスト、オブジェクトへのミックスインとしてのロールの定義など。「does」と「は」最小限の例を挙げていただけますか?
defuz 2012

回答:


635

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

さらに、私はあなたの質問の3番目の部分を次のように解釈しました:これらのモデルを分離しておくことができないことに気づく方法。

これらは2つの非常に異なる概念であり、それらを分離しておくことは常に困難です。ただし、この目的で使用できる一般的なパターンとツールがいくつかあります。

ドメインモデルについて

最初に認識する必要があるのは、ドメインモデルは実際にはデータに関するものではないということです。「このユーザーをアクティブにする」、「このユーザーを非アクティブにする」、「現在アクティブになっているユーザーは?」、「このユーザーの名前は何ですか?」などのアクション質問についてです。古典的な用語では、それはクエリコマンドについてです

コマンドで考える

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

与えられた非アクティブユーザ管理者は、このユーザがアクティブその後、ユーザがアクティブになる確認メールがユーザに送信される、エントリは、システムログに追加される (等など)




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

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

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

コマンドの表現

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

サービス層

サービスモジュールがすでにされています@Heddeで説明します。ここでは、個別のモジュールを定義し、各コマンドは関数として表されます。

services.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フォームを使用することです。私はこのアプローチを好みます。なぜなら、それは複数の密接に関連する側面を組み合わせるからです:

  • コマンドの実行(それは何をしますか?)
  • コマンドパラメータの検証(これは可能ですか?)
  • コマンドの表示(どうすればよいですか?)

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

クエリで考える

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

これらのクエリへの回答に着手する前に、常に2つの質問を自問する必要があります。これは、テンプレート専用のプレゼンテーションクエリ、および/またはコマンドの実行に関連付けられたビジネスロジッククエリ、および/またはレポートクエリですか。

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

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

クエリを作成する

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

User.objects.filter(active=True)

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

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

最初の代替手段は、単に表示用のクエリであるカスタムタグとテンプレートフィルターに役立ちます。

template.html

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

template_tags.py

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

クエリメソッド

クエリが単なるプレゼンテーションではない場合は、services.py(使用している場合)にクエリを追加するか、querys.pyを導入できます。モジュールをます。

querys.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の基本QuerySetをオーバーライドするには、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

クエリモデル

本質的に複雑であるがかなり頻繁に実行されるクエリの場合、クエリモデルの可能性があります。クエリモデルは、1つのクエリに関連するデータが別のモデルに格納される非正規化の形式です。もちろん、トリックは、非正規化モデルをプライマリモデルと同期させることです。クエリモデルは、変更が完全に自分の管理下にある場合にのみ使用できます。

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または同様のフレームワークを使用して、信号処理をバックグラウンドタスクにオフロードできます。

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

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


11
DDDをdjango関連の質問に組み込んだ答えを見るのは素晴らしいことです。Djangoが永続性のためにActiveRecordを採用しているからといって、懸念の分離が窓の外に出る必要があるという意味ではありません。すばらしい答えです。
Scott Coates 2013

6
オブジェクトを削除する前に、略奪されたユーザーがオブジェクトの所有者であることを検証したい場合、ビューまたはフォーム/サービスモジュールで確認する必要がありますか?
Ivan

6
@イヴァン:両方。これビジネス上の制約の一部であるため、フォーム/サービスモジュールに含める必要があります。それはすべきであるあなたが唯一の存在アクションは、ユーザーが実際に実行することができなければならないこともあるため表示であること。
出版社、2014

4
カスタムマネージャメソッドは、クエリを実装する良い方法ですUser.objects.inactive_users()。ただし、ここでのIMOのプロキシモデルの例では、セマンティクスが正しくu = InactiveUser.objects.all()[0]; u.active = True; u.save()ありませんisinstance(u, InactiveUser) == True。また、多くの場合、クエリモデルを維持する効果的な方法はdbビューを使用することです。
Aryeh Leib Taurog

1
@adnanmuttalebこれは正しいです。回答自体は「ドメインモデル」という用語のみを使用していることに注意してください。私の答えがDDDであるからではなく、その本がドメインモデルについて考えるのを助けるのに素晴らしい仕事をするので、DDDへのリンクを含めました。
公開者

148

私は通常、ビューとモデルの間にサービス層を実装します。これは、プロジェクトのAPIのように機能し、何が起こっているかをヘリコプターで見ることができます。私は、Javaプロジェクト(JSF)でこのレイヤー技術をよく使用している同僚からこの慣習を継承しました。

models.py

class Book:
   author = models.ForeignKey(User)
   title = models.CharField(max_length=125)

   class Meta:
       app_label = "library"

services.py

from library.models import Book

def get_books(limit=None, **filters):
    """ simple service function for retrieving books can be widely extended """
    return Book.objects.filter(**filters)[:limit]  # list[:None] will return the entire list

views.py

from library.services import get_books

class BookListView(ListView):
    """ simple view, e.g. implement a _build and _apply filters function """
    queryset = get_books()

心に留めておいてください、私は通常、モデル、ビュー、サービスをモジュールレベルで取り、プロジェクトのサイズに応じてさらに分離します


8
私は一般的なアプローチが好きですが、私の理解から、あなたの具体的な例は通常マネージャーとして実装されます。
arie

9
@arieとは限りませんが、おそらくより良い例として、ウェブショップサービスには、カートセッションの生成、製品評価計算のような非同期タスク、電子メールの作成と送信などが含まれます
Hedde van der Heide

4
私もこのアプローチが好きで、Javaにもやってきます。私はPythonが初めてです。views.pyをどのようにテストしますか?サービス層をどのようにモックしますか(たとえば、サービスがリモートAPI呼び出しを行う場合)?
Teimuraz

71

まず第一に、自分を繰り返さないでください

次に、過剰設計しないように注意してください。時々それは単なる時間の浪費であり、誰かが重要なことに集中できなくなることがあります。Python禅を確認する時々。

アクティブなプロジェクトを見てください

  • より多くの人々=より適切に整理する必要性
  • ジャンゴリポジトリ、彼らは簡単な構造を有しています。
  • ピップリポジトリ彼らがstraigtforwardディレクトリ構造を持っています。
  • ファブリックリポジトリはまた見に良いものです。

    • 下にすべてのモデルを配置できます yourapp/models/logicalgroup.py
  • たとえばUserGroup関連モデルは下に行くことができますyourapp/models/users.py
  • 例えばPollQuestionAnswer ...の下に行くことができますyourapp/models/polls.py
  • __all__中に必要なものをロードするyourapp/models/__init__.py

MVCの詳細

  • モデルはあなたのデータです
    • これにはあなたの実際のデータが含まれます
    • これには、セッション/ Cookie /キャッシュ/ fs /インデックスデータも含まれます
  • ユーザーはコントローラーを操作してモデルを操作します
    • これは、API、またはデータを保存/更新するビューである可能性があります
    • これはrequest.GET/ で調整できますrequest.POST ... etcで
    • ページングフィルタリングも考えてみてください。
  • データはビューを更新します
    • テンプレートはデータを受け取り、それに応じてフォーマットします
    • テンプレートなしのAPIもビューの一部です。例えばtastypieまたはpiston
    • これはミドルウェアも考慮する必要があります。

ミドルウェア / テンプレートタグを活用する

  • リクエストごとにいくつかの作業を行う必要がある場合、ミドルウェアは1つの方法です。
    • 例:タイムスタンプを追加する
    • 例:ページヒットに関するメトリックの更新
    • 例:キャッシュへの投入
  • オブジェクトをフォーマットするために常に繰り返されるコードスニペットがある場合、テンプレートタグが適しています。
    • 例:アクティブなタブ/ブレッドクラムのURL

モデルマネージャーを活用する

  • 作成するUserことができUserManager(models.Manager)ます。
  • インスタンスの残酷な詳細はに記載する必要がありmodels.Modelます。
  • のための悲惨な詳細はでqueryset行くことができmodels.Managerます。
  • User一度に1つずつ作成したい場合があるので、モデル自体に存在する必要があると考えるかもしれませんが、オブジェクトを作成するときには、おそらくすべての詳細はありません。

例:

class UserManager(models.Manager):
   def create_user(self, username, ...):
      # plain create
   def create_superuser(self, username, ...):
      # may set is_superuser field.
   def activate(self, username):
      # may use save() and send_mail()
   def activate_in_bulk(self, queryset):
      # may use queryset.update() instead of save()
      # may use send_mass_mail() instead of send_mail()

可能な限りフォームを使用する

モデルにマップするフォームがある場合、多くのボイラープレートコードを削除できます。ModelForm documentationかなり良いです。フォームのコードをモデルコードから分離することは、多くのカスタマイズがある場合(または、より高度な使用のために周期的なインポートエラーを回避する場合)に適しています。

可能な場合は管理コマンドを使用する

  • 例えば yourapp/management/commands/createsuperuser.py
  • 例えば yourapp/management/commands/activateinbulk.py

ビジネスロジックがある場合は、それを分離できます

  • django.contrib.auth dbにバックエンドがあるのと同じように、バックエンドを使用します...など。
  • settingビジネスロジック用にを追加します(例AUTHENTICATION_BACKENDS
  • あなたは使うことができます django.contrib.auth.backends.RemoteUserBackend
  • あなたは使うことができます yourapp.backends.remote_api.RemoteUserBackend
  • あなたは使うことができます yourapp.backends.memcached.RemoteUserBackend
  • 難しいビジネスロジックをバックエンドに委任する
  • 入力/出力に期待値を設定してください。
  • ビジネスロジックの変更は、設定の変更と同じくらい簡単です:)

バックエンドの例:

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

になる可能性があります:

class User(db.Models):
   def get_present_name(self):
      for backend in get_backends():
         try:
            return backend.get_present_name(self)
         except: # make pylint happy.
            pass
      return None

デザインパターンの詳細

インターフェース境界の詳細

  • 使用するコードは本当にモデルの一部ですか?->yourapp.models
  • コードはビジネスロジックの一部ですか?->yourapp.vendor
  • コードは汎用ツール/ライブラリの一部ですか?->yourapp.libs
  • コードはビジネスロジックライブラリの一部ですか?-> yourapp.libs.vendorまたはyourapp.vendor.libs
  • ここに良いものがあります:コードを個別にテストできますか?
    • いいね :)
    • いいえ、インターフェースに問題がある可能性があります
    • 明確な分離がある場合、単体テストはモックを使用して簡単に行うことができます
  • 分離は論理的ですか?
    • いいね :)
    • いいえ、これらの論理的な概念を個別にテストするのは難しいかもしれません。
  • 10倍のコードが追加されたときにリファクタリングが必要になると思いますか?
    • はい、駄目、ブエノなし、リファクタリングは大変な作業になる可能性があります
    • いいえ、それはただ素晴らしいです!

要するに、

  • yourapp/core/backends.py
  • yourapp/core/models/__init__.py
  • yourapp/core/models/users.py
  • yourapp/core/models/questions.py
  • yourapp/core/backends.py
  • yourapp/core/forms.py
  • yourapp/core/handlers.py
  • yourapp/core/management/commands/__init__.py
  • yourapp/core/management/commands/closepolls.py
  • yourapp/core/management/commands/removeduplicates.py
  • yourapp/core/middleware.py
  • yourapp/core/signals.py
  • yourapp/core/templatetags/__init__.py
  • yourapp/core/templatetags/polls_extras.py
  • yourapp/core/views/__init__.py
  • yourapp/core/views/users.py
  • yourapp/core/views/questions.py
  • yourapp/core/signals.py
  • yourapp/lib/utils.py
  • yourapp/lib/textanalysis.py
  • yourapp/lib/ratings.py
  • yourapp/vendor/backends.py
  • yourapp/vendor/morebusinesslogic.py
  • yourapp/vendor/handlers.py
  • yourapp/vendor/middleware.py
  • yourapp/vendor/signals.py
  • yourapp/tests/test_polls.py
  • yourapp/tests/test_questions.py
  • yourapp/tests/test_duplicates.py
  • yourapp/tests/test_ratings.py

またはあなたを助ける何か他のもの; 必要なインターフェース境界を見つけることが役立ちます。


27

Djangoは少し変更された種類のMVCを採用しています。Djangoには「コントローラー」という概念はありません。最も近いプロキシは「ビュー」であり、MVCではビューがDjangoの「テンプレート」に似ているため、MVC変換との混乱を招く傾向があります。

Djangoでは、「モデル」は単なるデータベースの抽象化ではありません。いくつかの点では、MVCのコントローラーとしてのDjangoの「ビュー」と同じ役割を果たします。インスタンスに関連付けられた動作全体を保持します。そのインスタンスが動作の一部として外部APIと対話する必要がある場合でも、それはモデルコードです。実際、モデルはデータベースと対話する必要がまったくないため、外部APIへの対話型レイヤーとして完全に存在するモデルが考えられます。これは、「モデル」のはるかに自由な概念です。


7

Djangoでは、MVC構造はChris Prattが言ったとおりであり、他のフレームワークで使用される従来のMVCモデルとは異なり、これを行う主な理由は、CakePHPのような他のMVCフレームワークで起こるような、あまりにも厳密なアプリケーション構造を回避することだと思います。

Djangoでは、MVCは次の方法で実装されました。

ビューレイヤーは2つに分割されます。ビューは、HTTPリクエストを管理するためにのみ使用する必要があります。ビューは呼び出されて応答します。ビューはアプリケーションの残りの部分(フォーム、モデルフォーム、カスタムクラス、単純なケースではモデルと直接)と通信します。インターフェイスを作成するには、テンプレートを使用します。テンプレートはDjangoにとって文字列のようなもので、コンテキストをテンプレートにマッピングし、このコンテキストはアプリケーションによってビューに伝えられます(ビューが要求したとき)。

モデルレイヤーは、カプセル化、抽象化、検証、インテリジェンスを提供し、データをオブジェクト指向にします(いつかはDBMSもそうするでしょう)。これは、大きなmodels.pyファイルを作成する必要があることを意味しません(実際、非常に良いアドバイスは、モデルを異なるファイルに分割し、それらを「models」という名前のフォルダーに入れ、「__ init__.py」ファイルをこれに作成することですすべてのモデルをインポートし、最後にmodels.Modelクラスの 'app_label'属性を使用するフォルダー。モデルはデータを操作することからあなたを抽象化するべきです、それはあなたのアプリケーションをより単純にします。また、必要に応じて、モデルの「ツール」などの外部クラスを作成する必要があります。モデルでheritageを使用して、モデルのメタクラスの「抽象」属性を「真」に設定することもできます。

残りはどこですか?まあ、小さなWebアプリケーションは一般にデータへの一種のインターフェースであり、ビューを使用してデータをクエリまたは挿入することで十分な小さなプログラムの場合もあります。より一般的なケースでは、実際には「コントローラー」であるFormsまたはModelFormsを使用します。これは、一般的な問題の実用的な解決策であり、非常に高速です。それはウェブサイトがやっていることです。

Formsが使い物にならない場合は、マジックを実行するための独自のクラスを作成する必要があります。これの非常に良い例は管理アプリケーションです。ModelAminコードを読み取ることができ、これは実際にはコントローラーとして機能します。標準的な構造はありません。既存のDjangoアプリを調べることをお勧めします。それはそれぞれのケースに依存します。これは、Django開発者が意図したものです。XMLパーサークラス、APIコネクタクラスを追加し、タスクを実行するためにCeleryを追加し、reactorベースのアプリケーション用にツイストし、ORMのみを使用し、Webサービスを作成し、管理アプリケーションを変更できます。 ..良質のコードを作成し、MVC哲学を尊重するかどうかにかかわらず、それをモジュールベースにして独自の抽象化レイヤーを作成するのは、あなたの責任です。それは非常に柔軟です。

私のアドバイス:できる限り多くのコードを読んでください。多くのdjangoアプリケーションがありますが、それほど真剣に受け止めないでください。それぞれのケースは異なり、パターンと理論は役立ちますが、常にではありませんが、これは不正確な科学です。djangoは、いくつかの痛みを軽減するために使用できる優れたツールを提供するだけです(管理インターフェイス、Webフォーム検証、i18n、オブザーバーパターンの実装など)。前述したものなど)が、優れたデザインは経験豊富なデザイナーによるものです。

PS:(標準のdjangoからの)認証アプリケーションの「User」クラスを使用して、たとえばユーザープロファイルを作成したり、少なくともそのコードを読み取ったりできます。これは、ケースに役立ちます。


1

古い質問ですが、とにかく私の解決策を提供したいと思います。これは、モデルオブジェクトもmodels.py内に配置するのが面倒である一方で、モデルオブジェクトにもいくつかの追加機能が必要であることの受け入れに基づいています。重いビジネスロジックは個人の好みに応じて個別に記述することもできますが、少なくとも、モデル自体に関連するすべてのことを行うモデルが好きです。このソリューションは、すべてのロジックをモデル内に配置したいユーザーもサポートします。

そのため、ロジックをモデル定義から分離し、IDEからすべてのヒントを得られるようにするハックを考案しまし

利点は明白であるべきですが、これは私が観察したいくつかのリストです:

  • DB定義はそのままです-ロジック「ガベージ」は添付されません
  • モデル関連のロジックはすべて1か所にきちんと配置されています
  • すべてのサービス(フォーム、REST、ビュー)には、ロジックへの単一のアクセスポイントがあります
  • すべてのベスト:私は私のことを実現したら、私は、任意のコードを書き換える必要はありませんでしたmodels.pyがあまりにも雑然となり、離れてロジックを分離しなければなりませんでした。分離はスムーズで反復的です。一度に機能を実行することも、クラス全体またはモデル全体を実行することもできます。

私はこれをPython 3.4以降とDjango 1.8以降で使用しています。

app / models.py

....
from app.logic.user import UserLogic

class User(models.Model, UserLogic):
    field1 = models.AnyField(....)
    ... field definitions ...

app / logic / user.py

if False:
    # This allows the IDE to know about the User model and its member fields
    from main.models import User

class UserLogic(object):
    def logic_function(self: 'User'):
        ... code with hinting working normally ...

私が理解できない唯一のことは、IDE(この場合はPyCharm)にUserLogicが実際にはユーザーモデルであることを認識させる方法です。しかし、これは明らかにハックなので、selfパラメーターの型を常に指定するというちょっとした迷惑を受け入れることができてとてもうれしいです。


実際、私はそれを使いやすいアプローチだと思っています。しかし、私は最終的なモデルを別のファイルに移動し、models.pyを継承しません。userlogic +モデル衝突したことはservice.pyようになる
MAKS

1

私はあなたに同意しなければなりません。djangoには多くの可能性がありますが、最初に始めるのはDjangoの設計哲学を確認することです。

  1. モデルプロパティからAPIを呼び出すのは理想的ではありません。ビューでこのようなことを行う方が理にかなっており、サービスレイヤーを作成して物事をドライに保つことができます。APIの呼び出しが非ブロッキングであり、呼び出しが負荷の高いものである場合、Service Worker(キューから消費するワーカー)にリクエストを送信することは意味があります。

  2. Djangoの設計哲学モデルによると、モデルは「オブジェクト」のあらゆる側面をカプセル化します。したがって、そのオブジェクトに関連するすべてのビジネスロジックがそこに存在する必要があります。

関連するすべてのドメインロジックを含める

Martin FowlerのActive Record設計パターンに従って、モデルは「オブジェクト」のあらゆる側面をカプセル化する必要があります。

  1. あなたが説明する副作用は明らかです、ここでのロジックはクエリセットとマネージャーに分解する方が良いかもしれません。次に例を示します。

    models.py

    import datetime
    
    from djongo import models
    from django.db.models.query import QuerySet
    from django.contrib import admin
    from django.db import transaction
    
    
    class MyUser(models.Model):
    
        present_name = models.TextField(null=False, blank=True)
        status = models.TextField(null=False, blank=True)
        last_active = models.DateTimeField(auto_now=True, editable=False)
    
        # As mentioned you could put this in a template tag to pull it
        # from cache there. Depending on how it is used, it could be
        # retrieved from within the admin view or from a custom view
        # if that is the only place you will use it.
        #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'
    
        # Moved to admin as an action
        # def activate(self):
        #     # method now has a side effect (send message to user)
        #     self.status = 'activated'
        #     self.save()
        #     # send email via email service
        #     #send_mail('Your account is activated!', '…', [self.email])
    
        class Meta:
            ordering = ['-id']  # Needed for DRF pagination
    
        def __unicode__(self):
            return '{}'.format(self.pk)
    
    
    class MyUserRegistrationQuerySet(QuerySet):
    
        def for_inactive_users(self):
            new_date = datetime.datetime.now() - datetime.timedelta(days=3*365)  # 3 Years ago
            return self.filter(last_active__lte=new_date.year)
    
        def by_user_id(self, user_ids):
            return self.filter(id__in=user_ids)
    
    
    class MyUserRegistrationManager(models.Manager):
    
        def get_query_set(self):
            return MyUserRegistrationQuerySet(self.model, using=self._db)
    
        def with_no_activity(self):
            return self.get_query_set().for_inactive_users()

    admin.py

    # Then in model admin
    
    class MyUserRegistrationAdmin(admin.ModelAdmin):
        actions = (
            'send_welcome_emails',
        )
    
        def send_activate_emails(self, request, queryset):
            rows_affected = 0
            for obj in queryset:
                with transaction.commit_on_success():
                    # send_email('welcome_email', request, obj) # send email via email service
                    obj.status = 'activated'
                    obj.save()
                    rows_affected += 1
    
            self.message_user(request, 'sent %d' % rows_affected)
    
    admin.site.register(MyUser, MyUserRegistrationAdmin)

0

選択した回答(https://stackoverflow.com/a/12857584/871392)にほぼ同意しますが、[クエリの作成]セクションにオプションを追加します。

フィルタークエリなどを作成するためのモデルのQuerySetクラスを定義できます。その後、組み込みのManagerおよびQuerySetクラスが行うように、このquerysetクラスをモデルのマネージャーにプロキシできます。

ただし、1つのドメインモデルを取得するために複数のデータモデルにクエリを実行する必要がある場合は、これを前に提案したように別のモジュールに配置する方が合理的です。


0

長所と短所の異なるオプションに関する最も包括的な記事:

  1. アイデア#1:脂肪モデル
  2. アイデア#2:ビジネスロジックをビュー/フォームに配置する
  3. アイデア#3:サービス
  4. アイデア#4:クエリセット/マネージャー
  5. 結論

ソース:https : //sunscrapers.com/blog/where-to-put-business-logic-django/


あなたはいくつかの説明を追加する必要があります。
m02ph3u5

-6

Djangoは、Webページの配信に簡単に使用できるように設計されています。これに満足できない場合は、別のソリューションを使用する必要があります。

モデルのルートまたは一般的な操作(同じインターフェイスを持つため)と、モデルのコントローラーのその他の操作を記述しています。他のモデルの操作が必要な場合は、そのコントローラーをインポートします。

このアプローチは、私にとってもアプリケーションの複雑さにとっても十分です。

Heddeの応答は、djangoとpython自体の柔軟性を示す例です。

とにかく非常に興味深い質問!


9
質問に対する私の理解に役立っていると言っていいでしょうか。
Chris Wesseling

1
Djangoにはdjango.db.models以外にも多くの機能がありますが、ほとんどのエコシステムはdjangoモデルを使用するモデルに大きく依存しています。
andho

1
ソフトウェアの開発に使用されるデザインパターン。また、djangoは、Webページだけでなく中規模または大規模のソフトウェアの配信にも簡単に使用できるように設計されています。
Mohammad Torkashvand 2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.