要約
この記事では、Django の低レベル ORM クエリ メソッドの使用について、アンチパターンの観点から直接説明します。代わりに、ビジネス ロジックを含むモデル層の特定のフィールドに関連するクエリ API を構築する必要があります。これを Django で行うのはそれほど簡単ではありませんが、ORM のコンテンツ原則を深く理解することで、いくつかの簡単な方法を説明します。この目標を達成するために。
概要
Django アプリケーションを作成するとき、ビジネス ロジックをカプセル化し、実装の詳細を隠すためにモデルにメソッドを追加することに慣れています。このアプローチは非常に自然に思え、実際に Django の組み込みアプリケーションで使用されています。
>>> from django.contrib.auth.models import User >>> user = User.objects.get(pk=5) >>> user.set_password('super-sekrit') >>> user.save()
ここでの set_password は、django.contrib.auth.models.User モデルで定義されたメソッドであり、パスワードのハッシュ化の特定の実装を非表示にします。対応するコードは次のようになります。
from django.contrib.auth.hashers import make_password class User(models.Model): # fields go here.. def set_password(self, raw_password): self.password = make_password(raw_password)
Django を使用して、特定のフィールドのトップレベルの汎用インターフェイス、つまり低レベルの ORM ツールを構築しています。これに基づいて、抽象化レベルを高め、対話型コードを減らします。この利点は、コードがより読みやすく、再利用可能で、堅牢になることです。
これは別の例で実行しており、以下のデータベース情報を取得する例で使用します。
この方法を説明するために、簡単なアプリ (todo リスト) を使用して説明します。
注: これは一例です。少量のコードで実際の例を示すのは難しいためです。 ToDo リスト自体の継承についてはあまり心配せず、このメソッドを実行する方法に焦点を当ててください。
以下は models.py ファイルです:
from django.db import models PRIORITY_CHOICES = [(1, 'High'), (2, 'Low')] class Todo(models.Model): content = models.CharField(max_length=100) is_done = models.BooleanField(default=False) owner = models.ForeignKey('auth.User') priority = models.IntegerField(choices=PRIORITY_CHOICES, default=1
このデータを渡し、現在のユーザーの不完全で優先度の高い Todo を表示するビューを作成すると想像してください。コードは次のとおりです:
def dashboard(request): todos = Todo.objects.filter( owner=request.user ).filter( is_done=False ).filter( priority=1 ) return render(request, 'todos/list.html', { 'todos': todos, })
注: これは request.user.todo_set.filter(is_done=False, priority=1) として記述できます。しかし、これは単なる実験です。
なぜこのように書くのは良くないのでしょうか?
まず第一に、コードは冗長です。完了するには 7 行のコードが必要ですが、正式なプロジェクトではさらに複雑になります。
次に、実装の詳細をリークします。たとえば、コード内の is_done は BooleanField です。その型が変更されると、コードは機能しなくなります。
そうすると意図が不明確でわかりにくいです。
最後に、重複使用が発生します。例: cron を通じてすべてのユーザーに毎週 todo リストを送信するコマンド行を作成する必要があります。この場合、7 行のコードをコピーして貼り付ける必要があります。これは DRY に沿ったものではありません (繰り返さないでください)
大胆に推測してみましょう: 低レベルの ORM コードを直接使用するのはアンチパターンです。
どうすれば改善できますか?
マネージャーとクエリセットの使用
まず、概念を理解しましょう。
Django には、テーブルレベルの操作に関連する 2 つの密接に関連した構成要素があります: マネージャーとクエリセット
manager (django.db.models.manager.Manager のインスタンス) は、「データベースをクエリすることによって Django に提供されるプラグイン」として説明されています「。」 Manager は、テーブル レベルの機能のための ORM へのゲートウェイです。各モデルには、オブジェクトと呼ばれるデフォルトのマネージャーがあります。
Quesyset (django.db.models.query.QuerySet) は「データベース内のオブジェクトのコレクション」です。これは本質的には SELECT クエリであり、フィルタリング、並べ替えなど (フィルター処理、並べ替え) を使用して、クエリされたデータを制限または変更することもできます。 django.db.models.sql.query.Query インスタンスを作成または操作し、データベース バックエンド経由で実際の SQL でクエリを実行するために使用されます。
え?まだわかりませんか?
ORM をゆっくりと深く理解すると、Manager と QuerySet の違いが理解できるようになります。
よく知られているマネージャー インターフェイスは見た目とは異なるため、人々は混乱するでしょう。
マネージャーインターフェースは嘘です。
QuerySet メソッドはチェーン可能です。 QuerySet メソッド (フィルターなど) が呼び出されるたびに、コピーされたクエリセットが返され、次の呼び出しを待ちます。これは、Django ORM の滑らかな美しさの一部です。
しかし、Model.objects がマネージャーである場合、問題が発生します。開始点としてオブジェクトを呼び出し、結果の QuerySet にリンクする必要があります。
では、Django はそれをどのように解決するのでしょうか?
インターフェイスの嘘がここで暴露されます。すべての QuerySet メソッドは Manager に基づいています。このメソッドでは、
を介してすぐにtodoリストに戻り、クエリインターフェースの問題を解決しましょう。 Django が推奨する方法は、Manager サブクラスをカスタマイズしてモデルに追加することです。
self.get_query_set()的代理,重新创建一个QuerySet。 class Manager(object): # SNIP some housekeeping stuff.. def get_query_set(self): return QuerySet(self.model, using=self._db) def all(self): return self.get_query_set() def count(self): return self.get_query_set().count() def filter(self, *args, **kwargs): return self.get_query_set().filter(*args, **kwargs) # and so on for 100+ lines...
複数のマネージャーをモデルに追加したり、オブジェクトを再定義したり、単一のマネージャーを維持してカスタム メソッドを追加したりすることもできます。
次の方法を試してみましょう:
方法 1: 複数のマネージャー
class IncompleteTodoManager(models.Manager): def get_query_set(self): return super(TodoManager, self).get_query_set().filter(is_done=False) class HighPriorityTodoManager(models.Manager): def get_query_set(self): return super(TodoManager, self).get_query_set().filter(priority=1) class Todo(models.Model): content = models.CharField(max_length=100) # other fields go here.. objects = models.Manager() # the default manager # attach our custom managers: incomplete = models.IncompleteTodoManager() high_priority = models.HighPriorityTodoManager()
このインターフェースは次のように表示されます:
>>> Todo.incomplete.all() >>> Todo.high_priority.all()
This方法 いくつかの質問があります。
まず、この実装方法はかなり面倒です。クエリカスタム関数ごとにクラスを定義する必要があります。
第二に、これにより名前空間が台無しになります。 Django 開発者は、Model.objects をテーブルへのエントリ ポイントとみなします。そうすると、命名規則が破られてしまいます。
第三,不可链接的。这样做不能将managers组合在一起,获得不完整,高优先级的todos,还是回到低等级的ORM代码:Todo.incomplete.filter(priority=1) 或Todo.high_priority.filter(is_done=False)
综上,使用多managers的方法,不是最优选择。
方法2: Manager 方法
现在,我们试下其他Django允许的方法:在单个自定义Manager中的多个方法
class TodoManager(models.Manager): def incomplete(self): return self.filter(is_done=False) def high_priority(self): return self.filter(priority=1) class Todo(models.Model): content = models.CharField(max_length=100) # other fields go here.. objects = TodoManager()
我们的API 现在看起来是这样:
>>> Todo.objects.incomplete() >>> Todo.objects.high_priority()
这个方法显然更好。它没有太多累赘(只有一个Manager类)并且这种查询方法很好地在对象后预留命名空间。(译注:可以很形象、方便地添加更多的方法)
不过这还不够全面。 Todo.objects.incomplete() 返回一个普通查询,但我们无法使用 Todo.objects.incomplete().high_priority() 。我们卡在 Todo.objects.incomplete().filter(is_done=False),没有使用。
方法3:自定义QuerySet
现在我们已进入Django尚未开放的领域,Django文档中找不到这些内容。。。
class TodoQuerySet(models.query.QuerySet): def incomplete(self): return self.filter(is_done=False) def high_priority(self): return self.filter(priority=1) class TodoManager(models.Manager): def get_query_set(self): return TodoQuerySet(self.model, using=self._db) class Todo(models.Model): content = models.CharField(max_length=100) # other fields go here.. objects = TodoManager()
我们从以下调用的视图代码中可以看出端倪:
>>> Todo.objects.get_query_set().incomplete() >>> Todo.objects.get_query_set().high_priority() >>> # (or) >>> Todo.objects.all().incomplete()
差不多完成了!这并有比第2个方法多多少累赘,得到方法2同样的好处,和额外的效果(来点鼓声吧...),它终于可链式查询了!
>>> Todo.objects.all().incomplete().high_priority()
然而它还不够完美。这个自定义的Manager仅仅是一个样板而已,而且 all() 还有瑕疵,在使用时不好把握,而更重要的是不兼容,它让我们的代码看起来有点怪异。
>>> Todo.objects.all().high_priority()
方法3a:复制Django,代理做所有事
现在我们让以上”假冒Manager API“讨论变得有用:我们知道如何解决这个问题。我们简单地在Manager中重新定义所有QuerySet方法,然后代理它们返回我们自定义QuerySet:
class TodoQuerySet(models.query.QuerySet): def incomplete(self): return self.filter(is_done=False) def high_priority(self): return self.filter(priority=1) class TodoManager(models.Manager): def get_query_set(self): return TodoQuerySet(self.model, using=self._db) def incomplete(self): return self.get_query_set().incomplete() def high_priority(self): return self.get_query_set().high_priority()
这个能更好地提供我们想要的API:
Todo.objects.incomplete().high_priority() # yay!
除上面那些输入部分、且非常不DRY,每次你新增一个文件到QuerySet,或是更改现有的方法标记,你必须记住在你的Manager中做相同的更改,否则它可能不会正常工作。这是配置的问题。
方法3b: django-model-utils
Python 是一种动态语言。 我们就一定能避免所有模块?一个名叫Django-model-utils的第三方应用带来的一点小忙,就会有点不受控制了。先运行 pip install django-model-utils ,然后……
from model_utils.managers import PassThroughManager class TodoQuerySet(models.query.QuerySet): def incomplete(self): return self.filter(is_done=False) def high_priority(self): return self.filter(priority=1) class Todo(models.Model): content = models.CharField(max_length=100) # other fields go here.. objects = PassThroughManager.for_queryset_class(TodoQuerySet)()
这要好多了。我们只是象之前一样 简单地定义了自定义QuerySet子类,然后通过django-model-utils提供的PassThroughManager类附加这些QuerySet到我们的model中。
PassThroughManager 是由__getattr__ 实现的,它能阻止访问到django定义的“不存在的方法”,并且自动代理它们到QuerySet。这里需要小心一点,检查确认我们没有在一些特性中没有无限递归(这是我为什么推荐使用django-model-utils所提供的用不断尝试测试的方法,而不是自己手工重复写)。
做这些有什么帮助?
记得上面早些定义的视图代码么?
def dashboard(request): todos = Todo.objects.filter( owner=request.user ).filter( is_done=False ).filter( priority=1 ) return render(request, 'todos/list.html', { 'todos': todos, })
加点小改动,我们让它看起来象这样:
def dashboard(request): todos = Todo.objects.for_user( request.user ).incomplete().high_priority() return render(request, 'todos/list.html', { 'todos': todos, })
希望你也能同意第二个版本比第一个更简便,清晰并且更有可读性。
Django能帮忙么?
让这整个事情更容易的方法,已经在django开发邮件列表中讨论过,并且得到一个相关票据(译注:associated ticket叫啥名更好?)。Zachary Voase则建议如下:
class TodoManager(models.Manager): @models.querymethod def incomplete(query): return query.filter(is_done=False)
通过这个简单的装饰方法的定义,让Manager和QuerySet都能使不可用的方法神奇地变为可用。
我个人并不完全赞同使用基于装饰方法。它略过了详细的信息,感觉有点“嘻哈”。我感觉好的方法,增加一个QuerSet子类(而不是Manager子类)是更好,更简单的途径。
或者我们更进一步思考。退回到在争议中重新审视Django的API设计决定时,也许我们能得到真实更深的改进。能不再争吵Managers和QuerySet的区别吗(至少澄清一下)?
我很确信,不管以前是否曾经有过这么大的重构工作,这个功能必然要在Django 2.0 甚至更后的版本中。
因此,简单概括一下:
在视图和其他高级应用中使用源生的ORM查询代码不是很好的主意。而是用django-model-utils中的PassThroughManager将我们新加的自定义QuerySet API加进你的模型中,这能给你以下好处:
啰嗦代码少,并且更健壮。
增加DRY,增强抽象级别。
対応するビジネス ロジックを対応するドメイン モデル層にプッシュします。