Zusammenfassung
In diesem Artikel werde ich die Verwendung der Low-Level-ORM-Abfragemethode von Django aus der Perspektive von Anti-Mustern direkt diskutieren. Alternativ müssen wir Abfrage-APIs erstellen, die sich auf bestimmte Felder in der Modellebene beziehen, die Geschäftslogik enthält. Dies ist in Django nicht sehr einfach, aber wenn ich die Inhaltsprinzipien von ORM genau verstehe, werde ich Ihnen einige einfache Möglichkeiten nennen um dieses Ziel zu erreichen.
Übersicht
Beim Schreiben von Django-Anwendungen sind wir es gewohnt, Methoden zu Modellen hinzuzufügen, um Geschäftslogik zu kapseln und Implementierungsdetails zu verbergen. Dieser Ansatz erscheint sehr natürlich und wird tatsächlich in den integrierten Anwendungen von Django verwendet.
>>> from django.contrib.auth.models import User >>> user = User.objects.get(pk=5) >>> user.set_password('super-sekrit') >>> user.save()
Das set_password hier ist eine im django.contrib.auth.models.User-Modell definierte Methode, die das Hashing des Passworts verbirgt. Die spezifische Implementierung von die Operation. Der entsprechende Code sollte wie folgt aussehen:
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)
Wir verwenden Django, um eine universelle Schnittstelle auf oberster Ebene in einem bestimmten Bereich zu erstellen, auf niedriger Ebene ORM-Tool. Erhöhen Sie auf dieser Grundlage den Abstraktionsgrad und reduzieren Sie den interaktiven Code. Dies hat den Vorteil, dass der Code lesbarer, wiederverwendbar und robuster wird.
Wir haben dies in einem separaten Beispiel getan und werden es im folgenden Beispiel zum Abrufen von Datenbankinformationen verwenden.
Um diese Methode zu beschreiben, verwenden wir zur Veranschaulichung eine einfache App (Todo-Liste).
Hinweis: Dies ist ein Beispiel. Weil es schwierig ist, mit einer kleinen Menge Code ein echtes Beispiel zu zeigen. Machen Sie sich keine allzu großen Sorgen darüber, dass sich die Aufgabenliste selbst erbt, sondern konzentrieren Sie sich darauf, wie Sie diese Methode ausführen.
Das Folgende ist die Datei 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
Stellen Sie sich vor, wir übergeben diese Daten und erstellen eine Ansicht, um unvollständige Daten für den aktuellen anzuzeigen Benutzer. Todos mit hoher Priorität. Hier ist der Code:
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, })
Hinweis: Dies kann als request.user.todo_set.filter( geschrieben werden is_done=False , Priorität=1). Aber das ist nur ein Experiment.
Warum ist es nicht gut, so zu schreiben?
Zuallererst ist der Code ausführlich. Für die Fertigstellung sind sieben Codezeilen erforderlich, und in einem formellen Projekt wird es komplizierter.
Zweitens: Details zur Implementierung durchsickern. Beispielsweise ist is_done im Code BooleanField. Wenn der Typ geändert wird, funktioniert der Code nicht.
Dann ist die Absicht unklar und schwer zu verstehen.
Schließlich wird es zu Duplikaten kommen. Beispiel: Sie müssen eine Befehlszeile schreiben, um jede Woche eine Aufgabenliste über Cron an alle Benutzer zu senden. In diesem Fall müssen Sie sieben Codezeilen kopieren und einfügen. Dies steht nicht im Einklang mit DRY (wiederholen Sie sich nicht)
Lassen Sie uns eine mutige Vermutung anstellen: Die direkte Verwendung von Low-Level-ORM-Code ist ein Anti-Pattern.
Wie kann man es verbessern?
Verwenden von Managern und QuerySets
Lassen Sie uns zunächst die Konzepte verstehen.
Django verfügt über zwei eng verwandte Konstrukte im Zusammenhang mit Operationen auf Tabellenebene: Manager und Querysets
Manager (eine Instanz von django.db.models.manager.Manager) wird beschrieben als „via Query the Datenbank, die vom Django-Plugin bereitgestellt wird. Manager ist das Tor zu ORM für Funktionen auf Tabellenebene. Jedes Modell verfügt über einen Standardmanager namens Objekte.
Quesyset (django.db.models.query.QuerySet) ist „eine Sammlung von Objekten in der Datenbank“. Es handelt sich im Wesentlichen um eine SELECT-Abfrage. Filterung, Sortierung usw. (gefiltert, geordnet) können auch verwendet werden, um die abgefragten Daten einzuschränken oder zu ändern. Wird zum Erstellen oder Bearbeiten von django.db.models.sql.query.Query-Instanzen und zum anschließenden Abfragen in echtem SQL über das Datenbank-Backend verwendet.
Hä? Du verstehst es immer noch nicht?
Wenn Sie ORM langsam und gründlich verstehen, werden Sie den Unterschied zwischen Manager und QuerySet verstehen.
Menschen sind von der bekannten Manager-Oberfläche verwirrt, weil sie nicht das ist, was sie zu sein scheint.
Manager-Schnittstelle ist eine Lüge.
QuerySet-Methoden sind verkettbar. Jedes Mal, wenn eine QuerySet-Methode (z. B. Filter) aufgerufen wird, wird ein kopierter Abfragesatz zurückgegeben, der auf den nächsten Aufruf wartet. Dies ist Teil der fließenden Schönheit von Django ORM.
Aber wenn Model.objects ein Manager ist, gibt es ein Problem. Wir müssen Objekte als Ausgangspunkt aufrufen und dann eine Verknüpfung zum resultierenden QuerySet herstellen.
Wie löst Django das Problem?
Die Funktionsweise der Schnittstelle wird hier erläutert. Alle QuerySet-Methoden basieren auf Manager. Bei dieser Methode können wir durch die Verwendung von
sofort zur Aufgabenliste zurückkehren und das Problem der Abfrageschnittstelle lösen. Die von Django empfohlene Methode besteht darin, die Manager-Unterklasse anzupassen und sie zu Modellen hinzuzufügen.
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...
Sie können dem Modell auch mehrere Manager hinzufügen, Objekte neu definieren oder einen einzelnen Manager verwalten. Fügen Sie benutzerdefinierte Methoden hinzu.
Lass uns diese Methoden ausprobieren:
Methode 1: Mehrere Manager
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()
Diese Schnittstelle wird folgendermaßen angezeigt:
>>> Todo.incomplete.all() >>> Todo.high_priority.all()
Wie viele Methoden hat diese Frage? .
Erstens ist diese Implementierungsmethode ziemlich umständlich. Sie müssen für jede benutzerdefinierte Abfragefunktion eine Klasse definieren.
Zweitens wird dies Ihren Namespace durcheinander bringen. Django-Entwickler betrachten Model.objects als Einstiegspunkt in die Tabelle. Dies würde gegen die Namenskonventionen verstoßen.
第三,不可链接的。这样做不能将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,增强抽象级别。
Schieben Sie die entsprechende Geschäftslogik auf die entsprechende Domänenmodellebene.