При работе с Django ORM, вы рано или поздно сталкиваетесь с тем, что часть условий для запросов дублируется в разных местах проекта снова и снова.
В Rails для решения этой проблемы есть named scopes.
Django не предлагает подобного инструмента «из коробки», но его можно легко реализовать самому.
К сожалению, запрос в google вида «django named scopes» выдает очень мало решений, и большая часть не слишком хороша. Самое корректное, на мой взгляд, можно прочитать здесь, и именно оно стало базой для кода под хабракатом.
Код примера -> https://github.com/tony-brewerio/django-scopes-example
В поставку входит небольшая sqlite бд с десятком записей и пример модели с использованием scopes.
Запустите python в папке с проектом.
Все работает.
Как?
Pet отличается от обычной модели Django по двум пунктам — наследуется не от models.Model, а от ScopedModel, а также содержит дополнительный внутренний класс Scopes.
Именно метод класса
Как видно из примера,
В python есть функция
эти две записи идентичны, а верхний вариант является не больше чем «синтаксическим сахаром»
С помощью функции
Про python и метапрограммирование информации прилично ( в частности http://habrahabr.ru/blogs/python/65625/ ), поэтому углубляться в теорию я не буду. Вместо этого я детально разберу код
Начнем.
Для начала, мы создаем
Далее мы подменяем
Таким образом, в
, определенного в
Например, в своем проекте я использую
, работающий аналогично
В Django есть 2 класса ( из тех что представляют для нас интерес ), поддерживающих один API запросов (
Как видно из кода выше,
Для того, чтобы методы из
Этим и занимается нижеследующий код.
Если вы посмотрите пример в блоге по ссылке, которую я приводил до хабраката, то там для аналогичной задачи автор создает
Используя в качестве примеси собранный ранее
Также, у
Далее мы устанавливаем созданный ScopedManager в качестве базового менеджера для нашей модели, и передаем управление коду Django.
В Rails для решения этой проблемы есть named scopes.
Django не предлагает подобного инструмента «из коробки», но его можно легко реализовать самому.
К сожалению, запрос в google вида «django named scopes» выдает очень мало решений, и большая часть не слишком хороша. Самое корректное, на мой взгляд, можно прочитать здесь, и именно оно стало базой для кода под хабракатом.
Show me the code
Код примера -> https://github.com/tony-brewerio/django-scopes-example
В поставку входит небольшая sqlite бд с десятком записей и пример модели с использованием scopes.
Запустите python в папке с проектом.
>>> from scopes.models import *
>>> Pet.objects.dead
[<Pet: Bowser, Diane, dog, m>]
>>> Pet.objects.cats()
[<Pet: Fluffy, Harold, cat, f>, <Pet: Claws, Gwen, cat, m>]
>>> Pet.objects.mammals()
[<Pet: Buffy, Harold, dog, f>, <Pet: Fang, Benny, dog, m>, <Pet: Bowser, Diane, dog, m>]
>>> Pet.objects.mammals().dead
[<Pet: Bowser, Diane, dog, m>]
>>> Pet.objects.alive.cats()
[<Pet: Fluffy, Harold, cat, f>, <Pet: Claws, Gwen, cat, m>]
>>> Pet.objects.male().alive.mammals()
[<Pet: Fang, Benny, dog, m>]
Все работает.
Как?
Разбор кода
Pet отличается от обычной модели Django по двум пунктам — наследуется не от models.Model, а от ScopedModel, а также содержит дополнительный внутренний класс Scopes.
class Scopes:
def male(self):
return self.filter(sex = 'm')
def female(self):
return self.filter(sex = 'f')
def cats(self):
return self.filter(species = 'cat')
@property
def dead(self):
return self.exclude(death = None)
@property
def alive(self):
return self.filter(death = None)
Именно метод класса
Scopes
мы вызываем, когда пишем Pet.objects.male()
>>> type(Pet.objects)
<class 'scopes.models.ScopedManagerForPet'>
>>> type(Pet.objects.male())
<class 'scopes.models.ScopedQuerySetForPet'>
>>> type(Pet.objects.alive)
<class 'scopes.models.ScopedQuerySetForPet'>
Как видно из примера,
Pet.objects
вместо models.Manager
подменяется на ScopedManagerForPet
, но определения этого класса в модуле scopes.models
нет. Откуда берется класс ScopedManagerForPet
?type
В python есть функция
type
, которая позволяет нам создавать классы, т.е. >>> class X(object):
... a = 1
...
>>> X = type('X', (object,), dict(a=1))
эти две записи идентичны, а верхний вариант является не больше чем «синтаксическим сахаром»
С помощью функции
type
метакласс ScopedModelBase
«на лету» создает новый Manager
и QuerySet
для каждой модели-подкласса ScopedModel
.Метаклассы
Про python и метапрограммирование информации прилично ( в частности http://habrahabr.ru/blogs/python/65625/ ), поэтому углубляться в теорию я не буду. Вместо этого я детально разберу код
ScopedModelBase
, в котором и происходит «магия», формирующая на выходе ScopedManagerFor*
Начнем.
scopes_bases = filter(None, [attrs.get('Scopes')] +
[getattr(b, 'Scopes', None) for b in bases])
attrs['Scopes'] = type('ScopesFor' + name, tuple(scopes_bases), {})
Для начала, мы создаем
ScopesForPet
, используя в качестве суперкласса определенный в нашей модели class Scopes:
( [attrs.get('Scopes')]
), а также Scopes
суперклассов модели ( [getattr(b, 'Scopes', None) for b in bases]
).Далее мы подменяем
Scopes
на ScopesForPet
с целью поддержки наследования — если вы используете абстрактные классы ORM в своем проекте, то Scopes
, определенные в суперклассе будут наследоваться потомками.Таким образом, в
ScopesForPet
будут также методы из класса class Scopes(object):
pass
, определенного в
ScopedModel
, и вы можете использовать его для добавления «глобальных» scopes.Например, в своем проекте я использую
def first(self, *args, **kwargs):
result = self.filter(*args, **kwargs)[:1]
if result: return result[0]
, работающий аналогично
.first
из Rails.Manager и QuerySet
>>> type(Pet.objects)
<class 'scopes.models.ScopedManagerForPet'>
>>> type(Pet.objects.alive)
<class 'scopes.models.ScopedQuerySetForPet'>
В Django есть 2 класса ( из тех что представляют для нас интерес ), поддерживающих один API запросов (
.filter
, .exclude
, и т.д. ) — models.Manager
и models.query.QuerySet
Как видно из кода выше,
Pet.objects
это инстанс Manager'а, который при использовании запросов будет возвращать QuerySet
. В дальнейшем, QuerySet
, при вызове методов вроде .filter()
, будет клонировать себя и возвращать свою копию. Для того, чтобы методы из
Scopes
работали и в случае Pet.objects.alive
, и в случае Pet.objects.all().alive
, необходимо добавить методы Scopes
и в Manager
, и в QuerySet
.Этим и занимается нижеследующий код.
ScopedQuerySet = type('ScopedQuerySetFor' + name, (QuerySet, attrs['Scopes']), {})
ScopedManager = type('ScopedManagerFor' + name, (models.Manager, attrs['Scopes']), {
'use_for_related_fields': True,
'get_query_set': lambda self: ScopedQuerySet(self.model, using = self._db)
})
Если вы посмотрите пример в блоге по ссылке, которую я приводил до хабраката, то там для аналогичной задачи автор создает
PostQuerySet
и PostManager
с использованием примеси PostMixin
, и это именно то, что делает мой код.Используя в качестве примеси собранный ранее
ScopesForPet
, мы создаем подклассы Manager
и QuerySet
.Также, у
Manager
есть специальный метод ( get_query_set
), возвращающий «базовый» QuerySet
, и мы должны переопределить его так, чтобы он возвращал не models.query.QuerySet
, а созданный нами подкласс.use_for_related_fields
, установленный в True
, означает, что Django будет использовать ScopedManagerForPet
в т.ч. при работе со связанными через ForeignKey
моделями. Далее мы устанавливаем созданный ScopedManager в качестве базового менеджера для нашей модели, и передаем управление коду Django.
attrs['objects'] = ScopedManager()
return ModelBase.__new__(cls, name, bases, attrs)
Заключение
- В случае, если вы уже используете нестандартные
QuerySet
илиManager
, просто измените суперклассы дляScopedManager
иScopedQuerySet
- Данное решение не вмешивается в код Django, никакого monkey patching.
- Вся работа
ScopedModelBase
выполняется при запуске приложения, нет переопределения__getattribute__
и прочей совсем уж «черной магии». - Получившиеся
Scoped*
классы ничем особенным не выделяются, и вы можете делать с ними что хотите. Например добавлять методы вPet.Scopes
на лету, что продемонстрировано в коде примера. Или использовать@property
, что в примере тоже есть.