Вчера отвечал на пост в моем Django'вском форуме и заодно вспомнил написать про относительно новую штуку в Django, которая появилась после включения ветки magic-removal — менеджеры моделей.
Что это такое
Сам по себе менеджер модели — это скомпонованные в один класс методы, которые отвечают за формирование SQL запросов к таблице из вызовов ORM. Причем помимо вещей, которые видно невооруженным глазом, вроде перевода "filter(pk=5)
" в "where id = 5
" они еще, например, формируют нужные order by
для указанного в модели порядки сортировки и нужные join'ы для связанных таблиц.
Но эти внутренности не так интересны как то, что эти менеджеры подходят для того, чтобы от них наследоваться, чтобы эти выборки изменять. Вот неполный список того, зачем это может быть надо:
У вас есть таблица, из которой нельзя стирать записи, потому что на них есть сылки, но в программе функция удаления нужна. Тогда можно добавить к модели признак "удаленности" и прикрутить к ней менеджер, который не будет выбирать такие помеченные записи. В базе они будут, но в программе не появятся.
Нужно отсортировать объекты модели не по своему полю, а по полю какой-то его lookup'ной таблицы (с которой он связан через ForeignKey). Менеджером можно расширить запросы к модели так, чтобы они включали и lookup'ную таблицу и order by по нужному полю.
Если к модели практически всегда нужно подтягивать данные из lookup-таблиц, то чтобы Django не делал это в несколько запросов, можно заставить менеджер модели вытягивать их сразу одним запросом.
Другими словами, это все те второстепенные добавки в запросы, которые обычно не связаны непосредственно с целью запроса, но просто должны там быть всегда (или почти всегда).
В документации по 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
из таблицы скидок нельзя, потому что менеджер при запросах из клиентов ничего не знает про таблицу скидок, и ее полей там не появится. Вот здесь и пишется свой менеджер, который решит две задачи:
- присоединит в запрос таблицу скидок
- сделает сортировку по полю
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
. В самом методе сначала получается запрос из базового менеджера, а потом изменяется:
select_related
включает в запрос все таблицы, с которыми модель связана по ForeignKey (и правильно связывает их по ключам, конечно).order_by
задает сортировку, используя реальные имена таблиц для спецификации полей (что само по себе слегка некрасиво)
Сам принцип изменения поведения запросов построен на том, что 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
Отличный пост, спасибо!
Будем надеяться, что со временем на основе model managers появится больше полезных расширений - таких, например, как аудит изменений данных в таблице (эта идея обсуждалась некоторое время назад на django-developers)
Гм. Этот пост появился ровно в то время, когда я начал размышлять, а нельзя ли одним запросом схватить связанные таблицы. :)
Интересное совпадение. ;)
Иван, наверно уже пора выпустить русскоязычную книгу по Djaamgo ;)
Спасибо за помощь в форуме, и блог.
Интересно, но, подозреваю, SQLAlchemy может все это и без дополнительного программирования.
А как можно сделать прицепляемые кастомные методы для кастомной фильтрации и последующей кастомной же сортировки? (касмтомная сортировка сначала подцепляет доп. таблицы которые нужны сугубо только для этой сортировки)
Чтобы было вот так:
entities = Entity.faceted().by_region(current_region).by_topic(current_topic).sorted_by_activity()
Подробнее я вопрос раскрыл в форуме: http://softwaremaniacs.org/forum/django/6586/#22096