データ モデルとドメイン モデルの違いについて質問しているようです。後者はエンド ユーザーが認識するビジネス ロジックとエンティティを見つけることができる場所であり、前者は実際にデータを保存する場所です。
さらに、私はあなたの質問の 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 フォームを使用することです。密接に関連する複数の側面を組み合わせているため、私はこのアプローチを好みます。
- コマンドの実行 (何をしますか?)
- コマンド パラメータの検証 (これを行うことができますか?)
- コマンドの表示 (どうすればよいですか?)
フォーム.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)
もちろん、これはデータが実際にデータ モデルで表現されている場合にのみ機能します。これは必ずしもそうではありません。そのような場合は、以下のオプションを検討できます。
カスタムタグとフィルター
最初の代替手段は、カスタム タグとテンプレート フィルターなど、単なるプレゼンテーション用のクエリに役立ちます。
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にクエリを追加するか (それを使用している場合)、queries.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 のベース 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 つのコマンドによってのみ変更される場合、これは非常に便利です。
フォーム.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)
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 ドキュメント: シグナル
アーキテクチャ: ドメイン駆動設計