Abstract
이 글에서는 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 도구. 이를 바탕으로 추상화 수준을 높이고 대화형 코드를 줄입니다. 이것의 이점은 코드를 더 읽기 쉽고 재사용 가능하며 강력하게 만드는 것입니다.
이를 별도의 예에서 수행했으며 아래 데이터베이스 정보를 얻는 예에서 이를 사용합니다.
이 방법을 설명하기 위해 간단한 앱(할 일 목록)을 사용하여 설명합니다.
참고: 이는 예시입니다. 적은 양의 코드로는 실제 예제를 보여주기 어렵기 때문입니다. 할 일 목록 자체를 상속하는 것에 대해 너무 걱정하지 말고 이 메서드를 실행하는 방법에 집중하세요.
다음은 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
이 데이터를 전달하고 현재의 불완전한 데이터를 표시하는 뷰를 생성한다고 상상해 보세요. 사용자. 우선순위가 높은 Todos입니다. 코드는 다음과 같습니다.
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, 우선순위=1). 하지만 이것은 단지 실험일 뿐이다.
왜 이렇게 쓰면 안 좋을까요?
우선 코드가 장황하다. 완료하려면 7줄의 코드가 필요하며, 공식 프로젝트에서는 더 복잡해집니다.
둘째, 구현 내용 유출입니다. 예를 들어 코드의 is_done은 BooleanField입니다. 해당 유형이 변경되면 코드가 작동하지 않습니다.
그러면 의도가 불분명하고 이해하기 어렵네요.
마지막으로 중복사용이 발생하게 됩니다. 예: cron을 통해 매주 모든 사용자에게 할 일 목록을 보내려면 명령 줄을 작성해야 합니다. 이 경우 코드 7줄을 복사하여 붙여넣어야 합니다. 이는 DRY와 일치하지 않습니다(반복하지 마세요)
과감하게 추측해 보겠습니다. 저수준 ORM 코드를 직접 사용하는 것은 안티 패턴입니다.
어떻게 개선할 수 있나요?
Manager 및 QuerySet 사용
먼저 개념을 이해해 봅시다.
Django에는 테이블 수준 작업과 관련하여 밀접하게 관련된 두 가지 구조인 관리자와 쿼리 세트가 있습니다.
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가 Manager인 경우에는 문제가 있습니다. 객체를 시작점으로 호출한 다음 결과 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()
질문이 있는 메소드는 몇 개입니까? .
첫째, 이 구현 방법은 다소 번거롭습니다. 각 쿼리 사용자 정의 함수에 대한 클래스를 정의해야 합니다.
둘째, 네임스페이스가 엉망이 됩니다. 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,增强抽象级别。
해당 비즈니스 로직을 해당 도메인 모델 계층으로 푸시합니다.