Содержание


Эффективная работа с моделями Django

Используем управляющие классы моделей и улучшаем производительность запросов

Comments

В Django большинство взаимодействий с базой данных осуществляется посредством механизма объектно-ориентированного отображения (Object-Relational Mapper или ORM) – функциональности, имеющейся, помимо Django, и в других современных инфраструктурах Web-разработки, таких как Rails. Системы ORM обретают все большую популярность среди разработчиков, так как они автоматизируют множество типичных взаимодействий с базой данных и используют знакомый объектно-ориентированный подход вместо инструкций SQL.

Django-программисты могут отказаться от использования «родной» системы ORM в пользу популярного пакета SQLAlchemy, однако хотя SQLAlchemy является довольно мощным инструментом, он сложнее в использовании, и при работе с ним приходится писать больше кода. Приложения Django можно разрабатывать, используя SQLAlchemy вместо встроенного механизма ORM (некоторые так и делают), но имейте в виду, что некоторые из наиболее привлекательных особенностей Django, такие как автоматическая генерация интерфейса администрирования, требуют использования встроенного ORM.

Эта статья освещает некоторые малоизвестные возможности встроенного механизма ORM Django, но приводимые здесь сведения о типичных ошибках, приводящих к неэффективной генерации запросов, могут быть полезны и для пользователей SQLAlchemy.

В этой статье использовались следующие версии ПО:

  • Django 1.0.2 (в части 1 и 2)
  • Django 1.1 альфа версия (в части 3)
  • sqlite3
  • Python 2.4-2.6 (Django пока не поддерживает Python 3.)
  • IPython (для работы в консоли)

Механизм ORM Django поддерживает множество баз данных, среди которых проще всех в установке sqlite3. Кроме того, sqlite3 поставляется в комплекте со многими ОС. Примеры из этой статьи должны работать с любой базой данных. С полным списком баз данных, поддерживаемых Django, можно ознакомиться в разделе Ресурсы.

Учимся избегать типичных ошибок при генерации запросов с помощью ORM

Архитектура Django поощряет гибкий стиль разработки, предоставляющий возможности для быстрого создания прототипов и экспериментирования. На ранних стадиях разработки не стоит беспокоиться о производительности, лучше уделить больше внимания читаемости кода и простоте реализации.

Иногда обнаружить проблемы производительности удается довольно быстро. Часто они появляются при первой попытке запустить приложение с реальными данными. Это может стать очевидным, когда выполнение набора из нескольких тестов занимает больше 5 минут. В других случаях медленная работа приложения становится заметной визуально. К счастью, существуют некоторые шаблоны проблем производительности, которые легко идентифицировать и исправить. В листинге 1 (файл models.py приложения) и листинге 2 показан пример типичной ошибки.

Листинг 1. Базовые модели для приложения examples: файл models.py
from django.db import models

# Некоторый документ, такой как запись в блоге или wiki-страничка

class Document(models.Model):
    name = models.CharField(max_length=255)

# Генерируемый пользователем комментарий, похожий на комментарии на сайте
# Digg или Reddit

class Comment(models.Model):
    document = models.ForeignKey(Document, related_name='comments')
    content = models.TextField()

В листинге 2 показано, как можно осуществлять доступ к модели данных, показанной в листинге 1 неэффективным способом.

Листинг 2. Медленный доступ к моделям данных
from examples.model import *
import uuid

# Сначала создадим много документов и назначим им случайные имена

for i in range(0, 10000):
    Document.objects.create(name=str(uuid.uuid4()))

# Запрашиваем имена документов для последующего просмотра

names = Document.objects.values_list('name', flat=True)[0:5000]

# Медленный способ получения списка документов с 
# заданными именами

documents = []

for name in names:
    documents.append(Document.objects.get(name=name))

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

Приведенный выше наивный код выполняется около 65 секунд при использовании размещаемой в оперативной памяти базе данных sqlite3. С базой данных, зависящей от файловой системы, он бы работал еще дольше. В листинге 3 показано, как исправить этот медленный код. Вместо выполнения запроса к базе данных для каждого имени, следует использовать оператор fieldname__in, который сгенерирует SQL-запрос, выглядящий примерно так:

SELECT * FROM model WHERE fieldname IN ('1', '2', ...)

(Точный синтаксис запроса будет различаться в зависимости от используемой базы данных.)

Листинг 3. Быстрый запрос для получения списка элементов
from examples import models
import uuid

for i in range(0, 10000):
    Document.objects.create(name=str(uuid.uuid4()))

names = Document.objects.values_list('name', flat=True)[0:5000]

documents = list(Document.objects.filter(name__in=names))

Этот код выполняется всего 3 секунды. Обратите внимание, что для того, чтобы запрос был действительно выполнен, в этом коде результат запроса преобразуется в список. Без этого сравнение было бы некорректным, так как в Django реализован отложенный механизм выполнения запросов, в котором простого присваивания результата запроса переменной недостаточно для фактического обращения к базе данных.

Гуру баз данных, для которых написание SQL-кода обычное дело, сочтут этот пример очевидным, но многие программисты Python не имеют существенного опыта работы с базами данных. Иногда самые хорошие привычки разработчика могут сыграть против эффективности. В листинге 4 показан один из способов рефакторинга кода из листинга 2, который можно было бы применить, не понимая его ошибочности.

Листинг 4. Типичный шаблон кода, приводящего к медленной работе с базой данных
for name in names:
    documents.append(get_document_by_name(name))

def get_document_by_name(name):
    return Document.objects.get(name=name))

С первого взгляда создание отдельного метода для извлечения документов из базы данных может показаться хорошей идеей. При необходимости в него можно поместить операции, которые следует выполнить перед извлечением документов, например добавление данных в модель. Запомните этот шаблон, разбиение кода на отдельные методы часто может казаться хорошим рефакторингом кода. Написание юнит-тестов с самого начала разработки и включение, по крайней мере, нескольких тестов, работающих с большими объемами данных, может помочь выявить ситуации, когда рефакторинг приводит к резкому падению производительности.

Инкапсуляция типичных запросов с помощью управляющих классов моделей

Встроенный управляющий класс модели, называемый Manager, используют все разработчики Django: именно он вызывается для всех методов формы Model.objects.*. Базовый класс Manager доступен автоматически и предоставляет часто используемые методы, возвращающие объекты QuerySet (например, all()), простые значения (например, count()) и объекты класса Model (например, get_or_create()).

Платформа Django поощряет разработчиков переопределять базовый класс Manager. Чтобы проиллюстрировать, почему это может быть полезным, добавим в приложение examples новую модель Format, которая описывает формат хранимых в системе файлов документов, например, как это показано в листинге 5.

Листинг 5. Добавление модели в приложение examples
from django.db import models

class Document(models.Model):
    name = models.CharField(max_length=255)
    format = models.ForeignKey('Format')

class Comment(models.Model):
    document = models.ForeignKey(Document, related_name='comments')
    content = models.TextField()

class Format(models.Model):
    type = models.CharField(choices=( ('Text file', 'text'),
                                      ('ePub ebook', 'epub'),
                                      ('HTML file', 'html')),
                            max_length=10)

Далее воспользуемся измененной моделью и создадим несколько документов различных форматов(листинг 6).

Листинг 6. Создаем несколько документов различных форматов
# Сначала создадим набор объектов класса Format
# и сохраним их в базе данных

format_text = Format.objects.create(type='text')
format_epub = Format.objects.create(type='epub')
format_html = Format.objects.create(type='html')

# Создадим несколько документов в различных форматах
for i in range(0, 10):
    Document.objects.create(name='My text document',
                                   format=format_text)
    Document.objects.create(name='My epub document',
                                   format=format_epub)
    Document.objects.create(name='My HTML document', 
                                   format=format_html)

Допустим, нужно, чтобы приложение предоставляло возможность сначала фильтровать документы по формату, а затем фильтровать этот объект QuerySet по другим полям, например по названию. Следующий простой запрос возвращает только текстовые документы: Document.objects.filter(format=format_text).

В этом примере смысл запроса достаточно ясен, однако в крупных приложениях, возможно, на результат придется накладывать гораздо больше ограничений. Может быть, вы захотите выводить только документы, помеченные как публичные, или документы созданные не позднее, чем 30 дней назад. Если в приложении вам придется вызывать эти запросы в различных местах, то поддержка всех условий фильтрации в синхронизованном состоянии может стать источником ошибок и головной болью при сопровождении кода.

В таких ситуациях может помочь создание собственного управляющего класса модели. Собственные управляющие классы позволяют задавать неограниченное количество «шаблонных» запросов подобно методам встроенного управляющего класса модели, таким как latest() (который возвращает только последний экземпляр модели) или distinct() (который добавляет к сгенерированному запросу инструкцию SELECT DISTINCT). Управляющие классы не только сокращают дублирование кода в приложении, но также улучшают читаемость кода. Согласитесь, что спустя некоторое время по сравнению с кодом:

Documents.objects.filter(format=format_text,publish_on__week_day=todays_week_day, 
  is_public=True).distinct().order_by(date_added).reverse()

вам или новому разработчику будет гораздо проще понять следующий код:

Documents.home_page.all()

Создать собственный управляющий класс модели чрезвычайно просто. В листинге 7 показан пример создания для модели документа управляющего класса get_by_format.

Листинг 7. Собственный управляющий класс модели, предоставляющий методы для каждого типа формата документа
from django.db import models
                            
class DocumentManager(models.Manager):

    # Класс модели всегда доступен управляющему классу через
    # self.model, но в этом примере мы используем только метод
    # filter(), унаследованный от models.Manager.

    def text_format(self):
        return self.filter(format__type='text')

    def epub_format(self):
        return self.filter(format__type='epub')

    def html_format(self):
        return self.filter(format__type='html')

class Document(models.Model):
    name = models.CharField(max_length=255)
    format = models.ForeignKey('Format')

    # Новый управляющий класс модели
    get_by_format = DocumentManager()

    # Управляющий класс по умолчанию теперь нужно определять явно
    objects = models.Manager()


class Comment(models.Model):
    document = models.ForeignKey(Document, related_name='comments')
    content = models.TextField()

class Format(models.Model):
    type = models.CharField(choices=( ('Text file', 'text'),
                                      ('ePub ebook', 'epub'),
                                      ('HTML file', 'html')),
                            max_length=10)
    def __unicode__(self):
        return self.type

Несколько замечаний относительно этого кода.

  • Если вы создаете собственный управляющий класс модели, Django автоматически исключает управляющий класс по умолчанию. Я предпочитаю оставлять как управляющий класс модели по умолчанию, так и собственный управляющий класс, чтобы другие разработчики (или я сама, если забуду) могли все так же использовать objects, которые будут вести себя в точности так, как ожидается. Однако так как управляющий класс, доступный по имени get_by_format, является просто подклассом встроенного класса models.Manager, в нем доступны все методы по умолчанию, такие как all(). Делать или не делать одновременно доступными управляющий класс по умолчанию и собственный управляющий класс, зависит от личных предпочтений.
  • Также есть возможность напрямую назначать для objects новый управляющий класс. Единственный недостаток проявится, если вы захотите вручную переопределить изначальный класс QuerySet. В таком случае ваши новые objects могут вести себя неожиданным для других разработчиков образом.
  • Вам необходимо определить управляющий класс в models.py перед определением вашего класса модели, иначе он не будет доступным для Django. Это похоже на ограничения, имеющиеся у класса ForeignKey.
  • Можно было бы просто реализовать класс DocumentManager с единственным методом, принимающим аргумент, например with_format(format_name). Однако в общем случае я предпочитаю создавать методы управляющего класса с подробными именами, но не принимающие никаких аргументов.
  • Не существует технического ограничения на количество собственных управляющих классов, которые можно назначать модели, но маловероятно, что вам понадобится больше чем один или два.

Использовать методы нового управляющего класса модели достаточно просто.

In [1]: [d.format for d in Document.get_by_format.text_format()][0]
Out[1]: <Format: text>

In [2]: [d.format for d in Document.get_by_format.epub_format()][0]
Out[2]: <Format: epub>

In [3]: [d.format for d in Document.get_by_format.html_format()][0]
Out[3]: <Format: html>

Теперь появилось удобное место, в котором можно размещать любую функциональность, относящуюся к этим запросам, также сюда можно добавлять дополнительные ограничения, не засоряя код. Такой подход согласуется с видением в Django шаблона модель–вид–контроллер (model-view-controller или MVC), в соответствии с которым функциональность подобного рода следует размещать в models.py, а не скапливать ее в представлениях или шаблонах

Переопределяем изначальный класс QuerySet, возвращаемый пользовательским управляющим классом модели

В другом шаблоне реализации управляющих классов модели собственные методы вообще не создаются. Вместо определения нового метода, возвращающего только HTML-документы, можно определить управляющий класс, который будет работать только на этом ограниченном множестве данных. Пример показан в листинге 8.

Листинг 8. Пользовательский управляющий класс для HTML-документов
class HTMLManager(models.Manager):
    def get_query_set(self):
        return super(HTMLManager, self).get_query_set().filter(format__type='html')
    
class Document(models.Model):
    name = models.CharField(max_length=255)
    format = models.ForeignKey('Format')
html = HTMLManager()
    get_by_format = DocumentManager()
    objects = models.Manager()

В этом примере метод get_query_set() наследуется из models.Manager и переопределяется. В этом методе за основу берется базовый запрос (тот же самый, который генерируется методом all()), к которому затем применяется дополнительный фильтр. Все методы, которые мы будем впоследствии добавлять в этот управляющий класс, будут сначала вызывать метод get_query_set(), а затем применять к результату дополнительные методы запросов, как показано в листинге 9.

Листинг 9. Использование управляющего класса, работающего только с документами формата html
# Наш запрос HTML-документов возвращает то же количество
# документов, что и управляющий класс по умолчанию, явно выполняющий фильтрацию 
# данных.

In [1]: Document.html.all().count() 
Out[1]: 10

In [2]: Document.get_by_format.html_format().count()
Out[2]: 10

# Можно доказать, что они возвращают в точности один и тот же результат

In [3]: [d.id for d in Document.get_by_format.html_format()] == 
    [d.id for d in Document.html.all()]
Out[3]: True

# В HTMLManager() уже нельзя работать с нефильтрованными данными

In [4]: Document.html.filter(format__type='epub')
Out[4]: []

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

Использование классов и статических методов в моделях

Нет никаких ограничений на типы методов, которые можно добавлять в управляющий класс. Методы могут возвращать объекты QuerySet, как показано выше, или экземпляры соответствующего класса модели (доступного через self.model).

Возможно, в некоторых в случаях вам захочется выполнять операции, относящиеся к модели, но не возвращающие экземпляры класса модели или объекты QuerySets. Документация Django утверждает, что все методы, не работающие с экземплярами модели, следует помещать в управляющий класс модели. Однако есть и другая возможность: использовать для них классы Python и статические методы.

Вот простой пример вспомогательного метода, который относится ко всему классу Format, а не к какому-либо конкретному его экземпляру:

# Возвращает каноническое имя формата для некоторых часто 
# встречающихся в реальной жизни расширений

def check_extension(extension):
    if extension == 'text' or extension == 'txt' or extension == '.csv':
        return 'text'
    if extension.lower() == 'epub' or extension == 'zip':
        return 'epub'
    if 'htm' in extension:
        return 'html'
    raise Exception('Did not get known extension')

Этот код не принимает и не возвращает экземпляр класса Format, поэтому он не может быть методом экземпляра класса. Его можно было бы поместить в класс FormatManager, но так как этот метод вообще не обращается к базе данных, это место для него также не совсем подходит.

В качестве решения можно добавить этот метод в класс Format и объявить его статическим методом с помощью декоратора @staticmethod, как показано в листинге 10.

Листинг 10. Добавляем вспомогательную функцию в виде статического метода класса модели
class Format(models.Model):
    type = models.CharField(choices=( ('Text file', 'text'),
                                      ('ePub ebook', 'epub'),
                                      ('HTML file', 'html')),
                            max_length=10)
    @staticmethod
    def check_extension(extension):
        if extension == 'text' or extension == 'txt' or extension == '.csv':
            return 'text'
        if extension.lower() == 'epub' or extension == 'zip':
            return 'epub'
        if 'htm' in extension:
            return 'html'
        raise Exception('Did not get known extension')

    def __unicode__(self):
        return self.type

Этот метод можно вызывать в виде Format.check_extension(extension), и для этого не требуется иметь экземпляр класса Format или создавать управляющий класс.

В Python также имеется декоратор @classmethod, который генерирует методы, оперирующие над классами. Такие методы принимают в качестве первого аргумента сам класс. Это может быть полезно, когда вы хотите выполнить какую-либо интроспекцию класса, не создавая экземпляр этого класса.

Агрегирующие запросы в Django

В Django 1.1 в механизм ORM включены мощные методы запросов, позволяющие реализовывать функциональность, ранее доступную только через использование SQL напрямую. Для разработчиков Python, остерегающихся работать с SQL, и для всех, кто хочет поддерживать работоспособность своего Django-приложения с различными базами данных, это является действительно ценным нововведением.

В современных приложениях, ориентированных на общение пользователей, очень часто данные сортируются не по статическому полю, например по алфавиту или времени создания, а на основе динамических данных. Допустим, в приложении examples мы хотим сортировать документы по популярности, определяемой по количеству комментариев к документу. До Django 1.1 такое можно было сделать либо написав собственный SQL-код, либо реализовав непереносимую хранимую процедуру, либо, что хуже всего, написав несколько неэффективных объектно-ориентированных запросов. При другом подходе можно было бы определить в базе данных фиктивное поле для хранения желаемого значения (например, количества комментариев к документу) и обновлять это поле вручную, переопределив метод save() документа.

Механизм агрегации Django устраняет необходимость прибегать к таким хитростям. Теперь можно упорядочивать документы по количеству комментариев, используя лишь один метод QuerySet: annotate(). Пример приведен в листинге 11.

Листинг 11. Использование агрегации для упорядочения результатов по количеству комментариев
from django.db.models import Count

# Создадим несколько документов
unpopular = Document.objects.create(name='Unpopular document', format=format_html)
popular = Document.objects.create(name='Popular document', format=format_html)

# Документу "popular" добавим больше комментариев, чем документу "unpopular"
for i in range(0,10):
    Comment.objects.create(document=popular)

for i in range(0,5):
    Comment.objects.create(document=unpopular)

# Если мы возвращаем результаты, сортируя их по времени создания (по умолчанию по id),
# первым будет выведен документ "unpopular".
In [1]: Document.objects.all()
Out[1]: [<Document: Unpopular document>, <Document: Popular document>]

# Если же вместо этого мы аннотируем результат общим количеством комментариев
# у каждого документа и затем упорядочим его по этому вычисленному значению,
# то первым будет выведен документ "popular".

In [2]: Document.objects.annotate(Count('comments')).order_by('-comments__count')
Out[2]: [<Document: Popular document>, <Document: Unpopular document>]

Метод annotate() класса QuerySet сам по себе не выполняет никакой агрегации. Вместо этого он командует Django назначить значение переданного выражения псевдостолбцу в полученном результате. По умолчанию именем этого столбца является строка из названия предоставленного поля (здесь значение Comment.document.related_name()) и имени агрегирующего метода. В этом коде вызывается django.db.models.Count – одна из простых математических функций, доступных в библиотеке агрегации (с полным списком методов можно ознакомиться по ссылке в разделе Ресурсы).

Результатом вызова Document.objects.annotate(Count('comments')) является объект QuerySet, имеющий новое свойство comments__count. Чтобы переопределить имя по умолчанию, можно передать желаемое имя в качестве именованного аргумента.

Document.objects.annotate(popularity=Count('comments'))

Теперь, когда промежуточный объект QuerySet содержит количество комментариев, ассоциированных с каждым документом, можно упорядочить его по этому новому полю. Так как мы хотим вначале видеть документы с наибольшим количеством комментариев, задаем сортировку по убыванию: .order_by('-comments__count').

Использование агрегации не только сокращает количество кода, который приходится писать, но также гарантирует, что эти операции будут выполняться быстро, так как математические вычисления в них выполняются посредством базы данных. Такая реализация гораздо более эффективна, чем извлечение всех подходящих данных с помощью ORM с последующим выполнением вычислений вручную.

Другие типы агрегации в Django 1.1

Новая библиотека агрегации не просто позволяет возвращать более сложные результаты. Также можно возвращать в качестве результата данные, извлеченные напрямую из базы данных и не являющиеся объектами QuerySet. Например, чтобы получить среднее количество комментариев для всех документов в базе данных, используйте следующий код:

In [1]: from django.db.models import Avg
In [2]: Document.objects.aggregate(Avg('comments'))
Out[2]: {'comments__avg': 8.0}

Агрегацию можно применять как к отфильтрованным, так и неотфильтрованным запросам. Кроме того, можно фильтровать данные по столбцам, сгенерированным с помощью annotate, так же как по обычным столбцам. Также агрегирующие методы можно применять к объединениям данных. Например, можно агрегировать документы на основе рейтинга комментариев к ним, как это сделано в сайтах наподобие Slashdot. Больше информации об агрегации можно найти по ссылкам в разделе Ресурсы.

Заключение

Недостатком механизмов объектно-ориентированного отображения считается то, что они настолько сильно абстрагируют базу данных, что с их помощью невозможно писать эффективные масштабируемые приложения. Для определенного вида приложений, обслуживающих миллионы посетителей и имеющих тесно взаимосвязанные модели данных, это утверждение справедливо.

Однако подавляющее большинство Web-приложений никогда не добивается такой большой аудитории и не достигает такого уровня сложности. Механизмы ORM проектировались, чтобы ускорить разработку проектов и помочь программистам, не имеющим глубокого знания SQL, начать участвовать в проектах, основанных на базах данных. По мере того как ваше приложение становится все больше и популярнее, вам наверняка придется провести проверку его производительности, как описано в первом разделе этой статьи. Возможно, в итоге даже придется начать заменять ORM-код кодом SQL или хранимыми процедурами.

К счастью, возможности легкого в использовании механизма ORM Django продолжают развиваться. Библиотека агрегации, появившаяся в Django 1.1, является важным шагом вперед, позволяющим генерировать эффективные запросы, используя знакомый объектно-ориентированный синтаксис. Для достижения еще большей гибкости разработчики Python также могут попробовать работать с SQLAlchemy, особенно в написанных на Python Web-приложениях, которые не основаны на Django.


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: Better Django models (EN).
  • API запросов в Django: ознакомьтесь с полным руководством по API запросов в Django (EN).
  • Узнайте больше о том, что нового в Django 1.1 (EN).
  • Откройте для себя мощную альтернативу встроенному механизму ORM Django: SQLAlchemy. Этот инструмент может быть правильным выбором для очень больших приложений (EN).
  • Ознакомьтесь с полным списком баз данных, поддерживаемых Django. Начиная с версии 1.0, в Django также появилась возможность добавлять поддержку новых баз данных (EN).
  • Ознакомьтесь с полным руководством по библиотеке агрегации в Django 1.1 (EN).
  • SQLite V3: начиная с версии 2.5, в Python появилась поддержка SQLite 3 без необходимости в дополнительных драйверах. В ранних версиях Python для этого приходилось загружать пакет pysqlite (EN).
  • Интервью и дискуссии разработчиков в подкастах developerWorks (EN).
  • Следите за developerWorks в Twitter (EN).
  • Разработайте ваш следующий проект с помощью пробного ПО от IBM, доступного для загрузки и на DVD (EN).
  • Загрузите ознакомительные версии продуктов IBM или поработайте с онлайновой пробной версией IBM SOA Sandbox и получите практический опыт с инструментами разработки и связующим ПО от DB2®, Lotus®, Rational®, Tivoli® и WebSphere® (EN).

Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=630776
ArticleTitle=Эффективная работа с моделями Django
publish-date=03042011