Вчера отвечал на пост в моем Django'вском форуме и заодно вспомнил написать про относительно новую штуку в Django, которая появилась после включения ветки magic-removal — менеджеры моделей.

Что это такое

Сам по себе менеджер модели — это скомпонованные в один класс методы, которые отвечают за формирование SQL запросов к таблице из вызовов ORM. Причем помимо вещей, которые видно невооруженным глазом, вроде перевода "filter(pk=5)" в "where id = 5" они еще, например, формируют нужные order by для указанного в модели порядки сортировки и нужные join'ы для связанных таблиц.

Но эти внутренности не так интересны как то, что эти менеджеры подходят для того, чтобы от них наследоваться, чтобы эти выборки изменять. Вот неполный список того, зачем это может быть надо:

Другими словами, это все те второстепенные добавки в запросы, которые обычно не связаны непосредственно с целью запроса, но просто должны там быть всегда (или почти всегда).

В документации по custom-менеджерам описана пара примеров, как с этим всем работать. Я же хочу вынести сюда то, на что отвечал в форуме, потому что в свое время очень долго бился об это головой (потом все придумал, а потом прочитал в мейл-листе, что так делают еще многие люди :-) ).

Пример: сортировка по полю из связанной таблицы

Пусть у нас есть модель Клиента, у которого есть скидочная категория, которая реализована как ссылка в lookup-таблицу:

class Discont(models.Model):
  title = models.CharFild(maxlength=20)
  amount = models.FloatField(max_digits=5, decimal_places=2)

class Client(models.Model):
  name = models.CharField(maxlength=100)
  discount = models.ForeignKey(Discount)

  class Meta:
    ordering = ['name']

... и надо вывести клиентов в порядке по скидкам и по именам. То есть примено так:

Элитная 40% Иванов Ф.В.
Элитная 40% Калинин М.Е.
Блатная 15% Чурина Е.Е.
Блатная 15% Янкевич О.Ю.
Издевательская 1% Бедный Д.

Напрямую задать в ordering клиентов поле amount из таблицы скидок нельзя, потому что менеджер при запросах из клиентов ничего не знает про таблицу скидок, и ее полей там не появится. Вот здесь и пишется свой менеджер, который решит две задачи:

В коде все выглядит очень просто:

class DiscountClientManager(models.Manager):
  def get_query_set(self):
    default_qs = super(DiscountClientManager, self).get_query_set()
    return default_qs.select_related().order_by('-app_discount.amount', 'app_client.name')

class Client(models.Model):
  # поля

  objects = DiscountClientManager()

Здесь перекрывается метод менеджера get_query_set, который и возвращает базовый запрос, который потом будет доступен из Client.objects. В самом методе сначала получается запрос из базового менеджера, а потом изменяется:

Сам принцип изменения поведения запросов построен на том, что Django'вский ORM оперирует так называемыми "query set" — объектами, которые хранят все условия, из которых потом будет составлен запрос. Query set имеет набор методов, которые добавляют к нему новое поведение и возвращают в виде нового query set'а. Поэтому из них возможно писать такие цепочки: Client.objects.filter(name='...').order_by('name').count().

Следующий кусочек в коде — это собственно назначение DiscountClientManager в качестве менеджера модели по умолчанию, чтобы все операции, начинающиеся с Client.objects делались именно через него. Это, однако, может быть не совсем тем, что хочется. В данном конкретном случае сортировка по скидкам будет нужна реже, чем просто вывод списка клиентов по именам. Поэтому Django позволяет задавать несколько менеджеров:

class Client(models.Model):
  # поля

  objects = models.Manager()
  by_discount = DiscountClientManager()

...

Client.objects.all() # сортировка по умолчанию
Client.by_discount.all() # сортировка по скидкам

Последнее мелкое замечание. Менеджер по умолчанию — тот, который определен первым. Он, кстати, даже не обязан называться "objects". Хотя я в любом случае советую не выдумывать излишне красивых названий, а пользоваться принятым соглашением.

Комментарии: 7

  1. AlexL

    Отличный пост, спасибо!

    Будем надеяться, что со временем на основе model managers появится больше полезных расширений - таких, например, как аудит изменений данных в таблице (эта идея обсуждалась некоторое время назад на django-developers)

  2. Alexander Solovyov

    Гм. Этот пост появился ровно в то время, когда я начал размышлять, а нельзя ли одним запросом схватить связанные таблицы. :)
    Интересное совпадение. ;)

  3. greg

    Иван, наверно уже пора выпустить русскоязычную книгу по Djaamgo ;)
    Спасибо за помощь в форуме, и блог.

  4. Max Ischenko

    Интересно, но, подозреваю, SQLAlchemy может все это и без дополнительного программирования.

  5. Руслан

    А как можно сделать прицепляемые кастомные методы для кастомной фильтрации и последующей кастомной же сортировки? (касмтомная сортировка сначала подцепляет доп. таблицы которые нужны сугубо только для этой сортировки)

    Чтобы было вот так:

    entities = Entity.faceted().by_region(current_region).by_topic(current_topic).sorted_by_activity()

    Подробнее я вопрос раскрыл в форуме: http://softwaremaniacs.org/forum/django/6586/#22096

  6. Вобщем решил проблему. Напишу для тех, кто тоже столкнется.Во первых читаем http://softwaremaniacs.org/blog/2006/07/20/custom-managers/Описание модели.Менеджер выглядит так....class PublishedManager(models.Manager):........def get_query_set(self):............return super(PublishedManager, self).get_query_set().filter(published=True)Сам класс....class Games(models.Model)........name=............published=.............objects = models.Manager()........published_objects = PublishedManager()Теперь при обращении Games.public_objects.all() будут отбираться только опубликованные Games, в то время как админка работает по стандартному Games.objects.all()P.S.3. Не нашел, как у вас тут код обрамлять.

  7. Менеджеры моделей

Добавить комментарий