Как стать автором
Поиск
Написать публикацию
Обновить

Python и метапрограммирование, реализуем named scopes для Django

При работе с Django ORM, вы рано или поздно сталкиваетесь с тем, что часть условий для запросов дублируется в разных местах проекта снова и снова.
В 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'Nonefor 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, что в примере тоже есть.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.