Показаны сообщения с ярлыком django orm. Показать все сообщения
Показаны сообщения с ярлыком django orm. Показать все сообщения

8 ноября 2012 г.

Валидация Django моделей, пять советов

Не думаю, что для кого-то открою Америку, сказав что в Django есть валидация не только для форм, а и для моделей :) Однако судя по моей практике это чуть спорное, но весьма полезное решение не спешат повсеместно использовать и городят свои велосипеды для проверки значений при создании/обновлении моделей. Поэтому хочу поделиться несколькими простыми советами по этой теме.

Во-первых, храните валидаторы отдельно от моделей. Хранение всех валидаторов в models.py неимоверно раздувает и без того не маленький модуль моделей (а иногда это и пакет), хранение же валидаторов в validators.py решает эту проблему и логично выносит все операции по проверке значений/уникальности модели в подходящее для этого место. Ну и сюда же, не пишите сложные проверки в Model.clean, Model.validate_unique методах, просто вызывайте необходимые функции валидации из validators.py, например:

models.py

from django.db import models

from .validators import validate_complex_case, validate_unique


class Card(models.Model):
    ...
    def clean(self):
        validate_complex_case(instance)

    def validate_unique(self, exclude=None):
        validate_unique(self)
        return super(Card, self).validate_unique(exclude)

validators.py

from django.core.exceptions import ValidationError


def validate_complex_case(instance):
    if instance.name == 'XXX' and instance.path != '/path/to/XXX':
        raise ValidationError('Please provide proper path for the card.')


def validate_unique(instance):
    manager = instance._default_manager
    other = manager.filter(name=instance.name)

    if other.count():
        message = 'Card with this Name already exists.'
        raise ValidationError({'__all__': [message]})

Во-вторых, непонятно зачем, но ошибки в проверке на уникальность модели нужно обворачивать в message_dict, использование простого сообщения приводит к AttributeError. Хотя с другой стороны это повышает возможности по написанию ошибок в разных полях моделей. Можно, например, подсветить какие именно поля не уникальны и уже сохранены в БД.

В-третьих, не забывайте, что валидаторов может быть сколько угодно много. Старайтесь разбивать большие валидаторы на маленькие атомарные функции и выносить их в core.validators или project.validators, чтоб иметь возможность использовать их не только для текущего приложения, а и для любых других приложений в проекте. Аттрибут validators у поля модели принимает список валидаторов, так что Вы вполне можете писать,

from django.core.validators import MaxLengthValidator, MinLengthValidator
from django.db import models

from .validators import validate_markup


class Card(models.Model):
    ...
    comment = models.TextField(validators=[
        MinLengthValidator(10), MaxLengthValidator(1000), validate_markup
    ])

В-четвертых, валидация модели автоматически не вызывается, когда вы хотите сохранить модель не из админ-панели или ModelForm'ы. Т.е., просто добавление валидаторов в поля модели и задание методов clean или validate_unique не гарантирует, что неправильные данные не сохранятся в базе данных после Model.objects.create или instance.save(). Для того, чтобы обезопасить себя и включить валидацию и для моделей, используйте сигналы:

from django.db import models
from django.db.models import signals
from django.dispatch import receiver


class Card(models.Model)
    ...


@receiver(signals.pre_save, sender=Card)
def validate_card(instance):
    instance.full_clean()

Ну и в-пятых,

  • Не забывайте про стандартные валидаторы Django
  • Импортируйте исключение ошибки валидации как from django.core.exceptions import ValidationError
  • Для моделей нельзя писать методы clean_FIELD как для форм
  • Валидация модели при вызове instance.full_clean() идет в следующей последовательности: валидация полей (instance.clean_fields()), вызов instance.clean(), проверка на уникальность (instance.validate_unique())

зы. Код и принципы данного материала актуальны для Django 1.3 ветки (да я так и не пересел на 1.4 или 1.5 потому что не вижу в этом особого смысла), может в новых версиях что-то поменялось, уточняйте в документации :)

23 июня 2011 г.

Уменьшаем кол-во запросов, которые генерируются Django ORM

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

Лирическое отступление: Для того, чтоб лучше понять как эти методы работают, я задам легкую схему для моделей, упоминаемых в статье. Во-первых, это будет встроенная auth.User, затем это будет модель статьи, которая будет версионироваться при помощи django-reversion. Иными словами, наш models.py будет выглядеть как-то так:

from django.db import models
from django.utils.translation import ugettext_lazy as _

import reversion


class Article(models.Model):

    STATE_NEW, STATE_SUBMITTED, STATE_REJECTED, STATE_ACCEPTED = range(1, 5)
    STATE_CHOICES = (
        (STATE_NEW, _('New')),
        (STATE_SUBMITTED, _('Submitted')),
        (STATE_REJECTED, _('Rejected)),
        (STATE_ACCEPTED, _('Accepted)),
    )

    title = models.CharField(_('title'), max_length=64)
    content = models.TextField(_('content'))
    state = models.PositiveIntegerField(_('state'), choices=STATE_CHOICES,
        default=STATE_NEW)

    writer = models.ForeignKey('auth.User', related_name='articles_writer',
        verbose_name=_('writer'))
    editor = models.ForeignKey('auth.User', blank=True, null=True,
        related_name='articles_editor', verbose_name=_('editor'))

reversion.register(Article)

Метод 1. Использование select_related вместо all

Здесь все очень просто. Если у моделей есть не-нулевые FK поля Вы явно захотите получать данные в них без дополнительных запросов к БД. Потому если писатель статьи должен быть явно указан на сайте, то считывайте статьи перед паджинацией как,

articles = Article.objects.select_related()

а не .all(). На самом деле метод .all() хорош как по мне только для "нишевых" (с малым кол-вом или вообще без связей) моделей. В остальных случаях надо смотреть по ситуации, может где-то .defer() после .select_related(*fields) пригодится, может где и .annotate() будет использован.

В целом с .select_related() я думаю всем все давным давно ясно, не забывайте только, что для не-нулевых FK-полей этот метод ничего не даст. И следующий код сгенерит в худшем случае Articles.objects.count() + 1 запросов (если все поля editor заполнены), вместо 1:

articles = Article.objects.select_related()
for article in articles:
    print(article.title, article.state, article.writer, article.editor)

Метод 2. Использование model.fk_id вместо model.fk.id

На самом деле, с первого взгляда кажется, что метод несколько бесполезен. Ну что там можно узнать про объект только по его идентификатору? Однако, смею вас уверить, это кажется только на первый взгляд. На самом деле во всяких filter или map и прочих lambda-функциях без него не обойтись.

Простой пример, необходимо найти все версии, оставленные редактором статьи. Что сразу приходит в голову? Использовать простой запрос,

article = Article.objects.get(pk=XXX)
versions = Version.objects.get_for_object(article).\
                           exclude(revision__user=None)
versions = article.editor and versions.filter(revision__user=article.editor) \
                          or versions.none()

И вроде все просто и безоблачно. Но только до поры до времени, для начала это +1 запрос к БД, а потом код переместиться в цикл или надо будет расширить параметры поиска искать не только ревизии редактора, но и какого-то системного пользователя.

Выход прост, изначально помнить, что редактор может принимать нулевое значение, а значит обращение к .editor спровацирует +1 запрос к БД. А значит используем .editor_id и точно знаем, что здесь искать кита мы не будем.

article = Article.objects.get(pk=XXX)
versions = Version.objects.get_for_object(article).\
                           exclude(revision__user=None)
versions = \
    article.editor_id and versions.filter(revision__user__pk=article.editor_id) \
                      or versions.none()

Метод 3. Предопределение FK значения

Сразу предупреждаю, пользуйтесь этим методом осторожно и только если уверены, что знаете что делаете :)

Помните пример из первого метода? Когда каждый запрос к полю редактора статьи стоил нам одного запроса. Нехорошо это, ведь и .select_related() нам тут не помощник. Но, не все так печально, а иначе очень уж просто. Надо всего лишь считать всех пользователей, а потом подставить необходимое значение в аттрибут ._editor_cache (примечание: на месте editor может быть любое название вашего FK-поля). А теперь еще раз и в коде:

articles = Article.objects.select_related('writer')
users = User.objects.all()
users = dict([(user.pk, user) for user in users])

for article in articles:
    article._editor_cache = users.get(article.editor_id)
    print(article.title, article.state, article.writer, article.editor)

И мы в итоге получаем гарантировано два запроса к БД, вместо Article.objects.сount() + 2 в худшем случае (опять же при условии, что все поля editor заполнены). Просто и изящно.

Однако надо помнить, что установив ручками значение в ._{{ fk_field }}_cache именно вы в ответе за него, а никак не Django. И если вдруг редактором статьи вместо Васи Пупкина станет Джон Доу, то вы знаете что делать :)

Метод 4. Ручное исполнение запросов

Я думаю, многие обрадовались и подумали, ну наконец-то про .raw(), ну наконец-то про SQL, к черту тот ORM, он и медленный и вообще сложно с ним. Но нет, поспешу разочаровать. Я лишь про то, что во многих случаях может пригодится явное преобразование QuerySet объектов в что-то более питоновское, как-то list и в дальнейшем использование именно питоновских функций для выборки данных, например filter(), а не метода .filter(). Взвесьте все за и против и помните, что QuerySet преобразованный в list есть не попросит к базе уже не обратиться, а обыкновенный еще как сможет.

Ну и про .raw() и .extra() не забывайте. Как говорится, есть моменты когда ваше SQL кунг-фу будет сильнее SQL кунг-фу Django ORM.

Метод 5. Или прочее

Сразу оговорюсь, что я не пытался упомянуть о всех методах уменьшения кол-ва запросов, а лишь хотел преподнести вам методы наиболее используемые мною при работе с Django. Для дальнейшего просветления я рекомендую вам прочитать раздел про оптимизацию доступа к базе данных в документации Django (версия для 1.3 или 1.2), ну или напрямую обращаться к Google или StackOverflow с теми же ключевыми словами, Django database optimization.

На сим прощаюсь и до новых встреч. Если появились вопросы или я где-то сглупил - милости прошу в комменты!

22 февраля 2009 г.

Джанго дб моделс кью - ай лав ю!

Значится, дано следующий фрагмент models.py:

class Attribute(models.Model):
    ATTRIBUTE_PERMISSIONS = (
        ('public', _('Public')),
        ('semi-private', _('Semi-Private')),
        ('private', _('Private'))
    )

    property_ = models.ForeignKey('Property')
    type_ = models.ForeignKey('AttributeType')
    value = models.CharField(max_length=255)
    permission = models.PositiveIntegerField(choices=ATTRIBUTE_PERMISSIONS,
        default=ATTRIBUTE_PUBLIC)
    granted_users = models.ManyToManyField('auth.User', blank=True, null=True)

class AttributeType(models.Model):
    slug = models.SlugField(max_length=32)
    """
    Other implementation of AttributeType class was stripped
    """

class Property(models.Model):
    owner = models.ForeignKey('auth.User')
    """
    Other implementation of Property class was stripped
    """

Теперь, внимание, задание: надо найти все объекты Property, у которых:

  • значение публичного аттрибута attr1 равно "Public";
  • значение полу-приватного аттрибута attr2 равно "Semi-Private";
  • значение приватного аттрибута attr2 равно "Private".

Казалось бы, как сложно и тут будет много запросо к базе данных, ведь это ж Django, это ж никому не нужный ORM. Вот если бы настрочить большой и сложный SQL, эх. Но, вот именно тут на арену и выходит django.db.models.Q.

Судите сами,

  • для того чтобы отфильтровать проперти по значению публичного аттрибута, нам надо просто отфильтровать проперти по .filter(attribute_type___slug='attr1', attribute__permission='public', attribute__value='Public')
  • для фильтра по значению полу-приватного аттрибута, надо дополнительно ввести фильтр проверки на вхождение пользователя в список разрешенных пользователей или этот пользователь является автором проперти: .filter(attribute_type___slug='attr2', attribute__permission='semi-private', attribute__value='Semi-Private', attribute__granted_users=USER).filter(attribute_type___slug='attr2', attribute__permission='semi-private', attribute__value='Semi-Private', owner=USER)
  • для фильтра же по значению приватного аттрибута, мы просто оставляем вторую часть предыдущего фильтра: .filter(attribute_type___slug='attr2', attribute__permission='private', attribute__value='Private', owner=USER)

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

def filter_by_attribute(queryset, **kwargs):
    # Public attributes filter
    basequery = Q(attribute__permission='public')

    if 'user' in kwargs:
        user = kwargs.pop('user')

        # Semi-private attributes filter
        basequery |= Q(attribute__permission='semi-private') & \
                     Q(Q(attribute__granted_users=user) | \
                       Q(owner=user))

        # Private attributes filter
        basequery |= Q(attribute__permission='private') & Q(owner=user)

    for slug, value in kwargs.items():
        query = basequery & \
                Q(attribute__type___slug=slug) & \
                Q(attribute__value=value)

        queryset = queryset.filter(query)

    return queryset

queryset = Property.objects.all()
queryset = filter_by_attribute(queryset, attr1='Public', attr2='Semi-Private', user=USER)
queryset = filter_by_attribute(queryset, attr2='Private', user=USER)

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

зы. Я знаю, что filter_by_attribute лучше было бы прицепить к кастомному PropertyManager'у, но это выходит за рамки моей сегодняшней темы ;)