私はDjangoでプロジェクトを書いていますが、コードの80%がmodels.py
ファイルに入っています。このコードは紛らわしいので、しばらくすると、実際に何が起きているのか理解できなくなります。
これが私を悩ますものです:
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])
私が欲しいのは私のコードでエンティティを分離することです。
Djangoに適用できるそのようなアプローチを実装するための良い習慣は何ですか?
データモデルとドメインモデルの違いについて質問しているようです - 後者がビジネスロジックとエンティティを知覚できる場所にあります。あなたのエンドユーザーによって、前者はあなたが実際にあなたのデータを保存するところです。
さらに、私はあなたの質問の第3部を次のように解釈しました:これらのモデルを別々にしておくことの失敗に気づく方法。
これらは2つの非常に異なる概念であり、それらを別々にしておくことは常に困難です。ただし、この目的に使用できる一般的なパターンとツールはいくつかあります。
最初に認識する必要があるのは、ドメインモデルは実際にはデータに関するものではないということです。actionsおよびquestions「このユーザーをアクティブにする」、「このユーザーを非アクティブにする」、「現在どのユーザーをアクティブにしていますか」、「何」などです。このユーザーの名前は? "古典的に言えば、それはqueryとcommandsについてです。
例の中のコマンド「このユーザーをアクティブにする」と「このユーザーを非アクティブにする」を見てみましょう。コマンドのいいところは、それらが小さな与えられた時のシナリオで簡単に表現できることです。
与えられた非アクティブなユーザー
管理者がこのユーザーをアクティブにしたとき
次にユーザーがアクティブになります
および確認の電子メールがユーザーに送信されます
およびエントリがシステムログに追加されます
(など)
このようなシナリオは、インフラストラクチャのさまざまな部分が1つのコマンドで影響を受ける可能性があることを確認するのに役立ちます。この場合、データベース(ある種の「アクティブ」フラグ)、メールサーバー、システムログなどです。
このようなシナリオは、テスト駆動開発環境の設定にも役立ちます。
そして最後に、コマンドで考えることは本当にあなたがタスク指向のアプリケーションを作成するのを助けます。あなたのユーザーはこれを理解するでしょう:-)
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
他の方法は各コマンドに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つの質問をするべきです:これは私のテンプレートのためだけのpresentational質問、そして/またはbusiness logicquery私のコマンドやreportingクエリを実行することに関係しています。
プレゼンテーションクエリは単にユーザインタフェースを改善するために行われます。ビジネスロジッククエリに対する回答は、コマンドの実行に直接影響します。レポートクエリは単に分析目的のためのものであり、時間的制約が緩やかです。これらのカテゴリは相互に排他的ではありません。
もう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にクエリを追加するか(あるいはそれを使っているなら)、またはquery.pyモジュール:
query.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()
メソッドをオーバーライドすることで、マネージャの基本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または同様のフレームワークを使用して、信号処理をバックグラウンドタスクにオフロードできます。
signals.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()
このアプローチを使うとき、あなたのコードがきれいなままであるかどうか決定するのはばかげて簡単になります。以下のガイドラインに従ってください。
ビューについても同じことが言えます(ビューは同じ問題に悩まされることが多いため)。
私は通常、ビューとモデルの間にサービス層を実装します。これはあなたのプロジェクトの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 """
if limit:
return Book.objects.filter(**filters)[:limit]
return Book.objects.filter(**filters)
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()
あなたの心に、私は通常、モジュールレベルにモデル、ビューやサービスを取り、プロジェクトの規模に応じてさらに分離する
まず、 自分自身を繰り返さないでください 。
次に、過剰なエンジニアリングを行わないように注意してください。時には時間の無駄であり、誰かが重要なことに集中できなくなることがあります。 zen of python を時々確認してください。
アクティブなプロジェクトを見てください
ファブリックリポジトリ も参照してください。
yourapp/models/logicalgroup.py
の下に配置できますUser
、Group
および関連モデルはyourapp/models/users.py
の下に配置できますPoll
、Question
、Answer
...はyourapp/models/polls.py
の下に配置できます__all__
内のyourapp/models/__init__.py
に必要なものをロードしますrequest.GET
/request.POST
... etcで調整できますtastypie
またはpiston
ミドルウェア / templatetagsを活用する
モデルマネージャーを活用する
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
一言で言えば、あなたが持つことができる
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
またはあなたを助ける他のもの;必要なインターフェイスと境界を見つけるのが役立ちます。
DjangoはMVCを少し修正したものを使っています。 Djangoには「コントローラ」という概念はありません。最も近いプロキシは "ビュー"です。これは、MVCではビューがDjangoの "テンプレート"に似ているため、MVC変換と混同しがちです。
Djangoでは、「モデル」は単なるデータベースの抽象化ではありません。いくつかの点で、それはMVCのコントローラとしてのDjangoの「見解」と義務を共有しています。インスタンスに関連した振る舞い全体を保持します。そのインスタンスがその動作の一部として外部APIと対話する必要がある場合でも、それはモデルコードです。実際、モデルはデータベースと対話する必要はまったくないため、外部APIに対する対話型レイヤーとして完全に存在するモデルがあると考えられます。それは「モデル」のはるかに自由な概念です。
Djangoでは、MVCの構造はChris Prattが言ったように、他のフレームワークで使われている古典的なMVCモデルとは異なり、これをする主な理由はCakePHPのような他のMVCフレームワークで起こるような厳しすぎるアプリケーション構造を避けることだと思います。
Djangoでは、MVCは次のように実装されていました。
ビューレイヤは2つに分割されています。ビューはHTTPリクエストを管理するためだけに使用されるべきであり、それらは呼び出されてそれらに応答します。ビューはアプリケーションの他の部分(フォーム、モデルフォーム、カスタムクラス、単純な場合はモデルと直接)と通信します。インターフェイスを作成するためにテンプレートを使います。テンプレートはDjangoにとって文字列のようなもので、コンテキストをそれらにマッピングし、このコンテキストはアプリケーションによってビューに伝えられました(ビューが要求したとき)。
モデル層はカプセル化、抽象化、検証、インテリジェンスを提供し、データをオブジェクト指向にします(いつかDBMSもそうするでしょう)。これはあなたが巨大なmodels.pyファイルを作るべきであることを意味するわけではありません(実際には非常に良いアドバイスはあなたのモデルを異なるファイルに分割し、それらを 'models'と呼ばれるフォルダに入れ、この中に '__init__.py'ファイルを作ることです)すべてのモデルをインポートし、最後にmodels.Modelクラスの属性 'app_label'を使用するフォルダ。モデルはデータを操作することからあなたを抽象化するべきです、それはあなたのアプリケーションをより簡単にするでしょう。また、必要に応じて、モデル用の「ツール」などの外部クラスを作成する必要があります。モデルのMetaクラスの 'abstract'属性を 'True'に設定して、モデルに継承を使用することもできます。
残りはどこにありますか?さて、小さなWebアプリケーションは一般的にデータへの一種のインタフェースです、いくつかの小さなプログラムのケースではデータを問い合わせたり挿入するためにビューを使用することで十分でしょう。より一般的なケースでは、FormsまたはModelFormを使用します。これらは実際には "コントローラ"です。これは一般的な問題に対する実用的な解決方法であり、非常に速いものではありません。それはウェブサイトがするのに使うことです。
Formsがあなたにとって不愉快でないなら、あなたは魔法をするためにあなた自身のクラスを作成するべきです、これの非常に良い例は管理アプリケーションです:あなたはModelAminコードを読むことができます、これは実際にコントローラとして働きます。標準的な構造はありません。既存のDjangoアプリを調べることをお勧めします。それぞれのケースによって異なります。これはDjango開発者が意図したことです。xmlパーサクラス、APIコネクタクラス、タスク実行用Celeryの追加、リアクタベースのアプリケーション用のツイスト、ORMのみの使用、Webサービスの作成、管理アプリケーションの変更などができます。 ..良質のコードを作成し、MVCの理念を尊重するかどうか、モジュールベースにして独自の抽象化レイヤを作成するのはあなたの責任です。とても柔軟です。
私の忠告:できるだけ多くのコードを読んでください、たくさんのDjangoアプリケーションがありますが、あまり真剣に考えないでください。それぞれのケースは異なり、パターンと理論は役に立ちますが、必ずしもそうではありませんが、これはあいまいな科学です。Djangoは、(管理者インターフェース、Webフォーム検証、国際化、オブザーバーパターン実装などの)前に述べたものや他のもの)、しかし良いデザインは経験豊富なデザイナーによるものです。
シモンズ:認証アプリケーションから(標準Djangoから)「ユーザー」クラスを使用すると、例えばユーザープロファイルを作成することができます、または少なくともそのコードを読む、それはあなたのケースに役立ちます。
私は主に選択された答え( https://stackoverflow.com/a/12857584/871392 )に同意しますが、[クエリの作成]セクションにオプションを追加します。
Makeフィルタクエリのためのモデル用のQuerySetクラスを定義することができます。その後、組み込みのManagerクラスやQuerySetクラスと同様に、このクエリセットクラスをモデルのマネージャに代入できます。
1つのドメインモデルを取得するために複数のデータモデルをクエリする必要がある場合は、これを以前に提案したように別のモジュールに配置する方が合理的です。
私はあなたに同意しなければならないでしょう。 Djangoにはたくさんの可能性がありますが、始めるのに最適な場所は Djangoのデザイン哲学 です。
モデルプロパティからAPIを呼び出すことは理想的ではないでしょう。ビュー内でこのようなことをして、おそらく物事をドライに保つためにサービスレイヤを作成するのがより理にかなっているようですAPIへの呼び出しがノンブロッキングで、呼び出しが高価なものである場合は、サービスワーカー(キューから消費するワーカー)にリクエストを送信することは意味があります。
Djangoのデザイン哲学モデルは、「オブジェクト」のあらゆる側面をカプセル化しています。したがって、そのオブジェクトに関連するすべてのビジネスロジックはそこに存在するはずです。
すべての関連ドメインロジックを含めます
Martin FowlerのActive Recordデザインパターンに従って、モデルは「オブジェクト」のあらゆる側面をカプセル化する必要があります。
あなたが説明する副作用は明白です、ここでのロジックはより良いクエリセットとマネージャに分類されるかもしれません。これが一例です。
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)