Создание собственных полей для модели

Предисловие

Раздел о моделях описывает, как использовать стандартные поля модели Django – CharField, DateField, и т.д. В большинстве случаев эти классы - все что вам будет нужно. Однако в некоторых случаях предоставленные Django поля модели не предоставляют необходимый функционал.

Встроенные поля не покрывают все возможные типы полей базы данных – только стандартные типы, такие как VARCHAR и INTEGER. Для остальных типов полей, такие как хранящие географические полигоны или собственные типы полей в PostgreSQL, вы можете создать собственный подкласс для Field.

Также вы можете создать поле для хранения сложного Python объекта в стандартном поле. Это другая проблема, которую помогает решить подкласс Field.

Описание примера

Создание собственного поля требует внимания к деталям. Для простоты понимания мы будем использовать один и тот же пример в этом разделе: объект, который содержит состояние карт на руках для карточной игры Бридж. Не беспокойтесь, вам не обязательно знать правила этой игры. Все что вам необходимо знать – 52 делятся поровну между четырьмя игроками, которых традиционно называют north, east, south и west. Наш класс выглядит следующим образом:

class Hand(object):
    """A hand of cards (bridge style)"""

    def __init__(self, north, east, south, west):
        # Input parameters are lists of cards ('Ah', '9s', etc)
        self.north = north
        self.east = east
        self.south = south
        self.west = west

    # ... (other possibly useful methods omitted) ...

Это простой класс Python, ничего Django-специфического. Мы хотим использовать нашу модель следующим образом (предполагается, что атрибут модели hand это объект Hand):

example = MyModel.objects.get(pk=1)
print(example.hand.north)

new_hand = Hand(north, east, south, west)
example.hand = new_hand
example.save()

Получение и назначение значений атрибута hand нашей модели аналогично любому другому классу в Python. Хитрость заключается в том, чтобы научить Django сохранять и загружать наш объект.

Для использования класса Hand в наших моделях, мы не должны изменять этот класс. Таким образом можно использовать в моделях существующие классы, которые мы не можем изменить.

Примечание

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

Теория

Хранение в базе данных

Основное предназначение поля модели – это преобразование объекта Python (строка, булево значение, datetime, или что-либо более сложное, как Hand) в формат удобный для хранения в базе данных и обратно (и сериализация, но, как мы увидим далее, это решается естественным способом при решении проблем преобразования данных для базы данных).

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

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

Для нашего примера с Hand, мы можем преобразовать данные о картах в строку из 104 символов соединив все карты вместе в определенном порядке – скажем, сначала все карты north, затем карты east, south и west. Таким образом объект Hand будет сохранен в текстовом поле базы данных.

Что делает класс поля?

class Field

Все поля в Django(и когда мы говорим поля в этом разделе, мы всегда подразумеваем поля модели, а не поля формы) являются подклассами django.db.models.Field. Большинство информации о поле, которую хранит Django, общая для всех типов полей – название, описание, уникальность и др. Вся эта информация хранится в Field. Мы рассмотрим возможности Field чуть позже, сейчас же запомним, что все поля наследуются от Field и переопределяют поведение этого класса.

Важно понять, что класс поля – это не то, что хранится в атрибуте модели. Атрибуты модели содержат объекты Python. Классы полей, которые вы указали в модели, на самом деле сохраняются в классе Meta при создании класса модели. Вот почему мы не используем классы полей при редактировании атрибутов экземпляра модели, их задача преобразовывать значение атрибутов в данные сохраняемые в базе данных или передаваемые в сериализатор.

Будьте внимательны при создании собственного поля. Подкласс Field предоставляет несколько способов преобразования объектов Python в значение для базы/сериализации (например, сохраняемое значение и значение для фильтра по полю отличаются). Не волнуйтесь если звучит слишком сложно – мы во всем разберемся на примере чуть ниже. Просто запомните, что скорее всего вам придется создавать два класса:

  • Первый класс будет использоваться пользователями. Экземпляр этого класса будет использоваться для отображения, работы с данными, при изменении значения поля модели. В нашем примере это класс Hand.

  • Второй класс – это подкласс Field. Это класс, который отвечает за преобразование вашего первого класса в значение для хранения в базе данных и обратно в объект Python.

Создание подкласса поля

При создании подкласса Field, сначала подумайте, не похож ли он на уже существующее поле. Можете ли унаследоваться от существующего поля Django и сэкономить этим свое время? Если нет, создавайте подкласс Field.

При создании конструктора важно разделить аргументы специфические для вашего поля и те, которые следует передать в метод __init__() :class:`~django.db.models.Field`(или вашего родительского класса).

Назовем наше поле HandField. (Хорошая практика называть подклассы Field как <Something>Field, таким образом легко определить, какой класс является подклассом Field.) Оно не похоже ни на одно встроенное в Django поле, по этому мы создаем подкласс Field:

from django.db import models

class HandField(models.Field):

    description = "A hand of cards (bridge style)"

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 104
        super(HandField, self).__init__(*args, **kwargs)

HandField принимает большинство стандартных аргументов (смотрите список ниже), но мы явно указываем длину поля так как нам необходимо хранить только значения 52 карт и их принадлежность, всего 104 символа.

Примечание

Большинство полей модели в Django принимают параметры, которые они совсем не используют. Например, вы можете передать editable и auto_now в django.db.models.DateField, аргумент editable будет проигнорирован (auto_now`устанавливает  ``editable=False`). Вы не получите ошибку.

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

Field.__init__()

Метод __init__() принимает следующие параметры:

Аргументы без описания аналогичны соответствующим аргументам стандартных полей, смотрите раздел о полях модели for examples and details.

Метакласс SubfieldBase

class django.db.models.SubfieldBase

Как уже упоминалось в Предисловии, собственные поля используются в двух случаях: для использования типа поля для определенной базы данных и для работы со сложными объектами Python. Если вам необходимо работать с определенным типом поля в базе данных и использовать встроенные типы данных Python, вы можете пропустить этот раздел.

Если мы используем собственный тип Python, такой как наш класс Hand, мы должны убедиться, что Django при создании экземпляра модели и добавления значения из базы данных в атрибут поля, преобразует значение в правильный объект Python. Полное описание этого процесса сложное для понимания, но код, который вам необходимо написать в классе поля, простой: убедитесь, что ваш подкласс использует специальный метакласс:

Например, для Python 2:

class HandField(models.Field):

    description = "A hand of cards (bridge style)"

    __metaclass__ = models.SubfieldBase

    def __init__(self, *args, **kwargs):
        ...

В Python 3, вместо атрибута __metaclass__ в определении класса добавьте metaclass:

class HandField(models.Field, metaclass=models.SubfieldBase):
    ...

Если вы хотите, чтобы ваш код работал на Python 2 & 3, можно использовать six.with_metaclass():

from django.utils.six import with_metaclass

class HandField(with_metaclass(models.SubfieldBase, models.Field)):
    ...

Теперь мы можем быть уверены, что метод to_python(), описанный ниже, будет всегда вызываться при инициализации атрибута.

ModelForms и собственные поля

Если вы используете SubfieldBase, to_python() будет вызываться при присваивании полю значения. Это означает, что каждый раз при присваивании полю значения, вы должны проверить правильность его типа и обработать все ошибки.

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

Поэтому вы должны убедиться, что поле формы, которое отображает ваше поле модели, выполняет все необходимые проверки данных и преобразует их в тип аналогичный используемому методом to_python() вашего поля модели. Для этого, возможно, вам придется создать собственный класс поля формы и/или переопределить метод formfield() поля модели, чтобы метод to_python() класса поля формы возвращал данные правильного типа.

Документирование собственного поля

Field.description

Конечно же вам необходимо задокументировать ваше поле, чтобы пользователи знали как его использовать. В дополнение к docstring, который удобен для разработчиков, вы можете предоставить описание поля, которое будет отображаться в разделе документации в интерфейсе администратора, созданном с django.contrib.admindocs. Для этого укажите описание в атрибуте description класса поля. В нашем примере описание поля HandField в приложении admindocs будет - ‘A hand of cards (bridge style)’.

На страницах django.contrib.admindocs описание поля включает field.__dict__, что позволяет включить описание аргументов. Например, описание CharField выглядит следующим образом:

description = _("String (up to %(max_length)s)")

Полезные методы

После того как вы создали свой подкласс Field и указали __metaclass__, можно переходить к переопределению методов, которые определяют поведение вашего поля. Методы описанные ниже идут в порядке убывания важности.

Типы полей базы данных

Field.db_type(self, connection)

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

Предположим вы создали собственный тип поля для PostgreSQL - mytype. Вы можете использовать его в Django, унаследовав Field и добавив следующий метод db_type():

from django.db import models

class MytypeField(models.Field):
    def db_type(self, connection):
        return 'mytype'

Создав MytypeField вы можете использовать его в моделях так же, как и другие подтипы Field:

class Person(models.Model):
    name = models.CharField(max_length=80)
    something_else = MytypeField()

Если вы создаете приложение независимое от используемой базы данных, учитывайте, что разные базы данных используют различные типа полей. Например, поле даты/времени в PostgreSQL называется timestamp, а в MySQL – datetime. Самый простой способ: проверять значение connection.settings_dict[‘ENGINE’]` в методе db_type().

Например:

class MyDateField(models.Field):
    def db_type(self, connection):
        if connection.settings_dict['ENGINE'] == 'django.db.backends.mysql':
            return 'datetime'
        else:
            return 'timestamp'

Метод db_type() используется Django только при создании CREATE TABLE запросов – когда вы создаете таблицы в базе данных для приложения. Больше нигде этот метод не используется, вы можете использовать достаточно сложный код, как проверка connection.settings_dict в примере выше.

Некоторые типы полей принимают параметры, например CHAR(25), где 25 указывают максимальный размер колонки. В этом случае лучше указывать параметр в модели, чем хардкодить в методе db_type(). Например, глупо создавать поле CharMaxlength25Field:

# This is a silly example of hard-coded parameters.
class CharMaxlength25Field(models.Field):
    def db_type(self, connection):
        return 'char(25)'

# In the model:
class MyModel(models.Model):
    # ...
    my_field = CharMaxlength25Field()

Лучше позволить указывать параметр при определении поля – то есть при создании класса модели. Для этого переопределите метод django.db.models.Field.__init__():

# This is a much more flexible example.
class BetterCharField(models.Field):
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length
        super(BetterCharField, self).__init__(*args, **kwargs)

    def db_type(self, connection):
        return 'char(%s)' % self.max_length

# In the model:
class MyModel(models.Model):
    # ...
    my_field = BetterCharField(25)

В конце концов, если поле требует действительно сложный SQL код при создании, верните None в методе db_type(). В этом случае Django пропустит создание этого поля в базе данных. Вам придется создать поле каким либо другим способом.

Преобразование значений базы данных в объекты Python

Field.to_python(self, value)

Преобразует значение, которое вернула база данных (или сериализатор), в объект Python.

Реализация по умолчанию возвращает value без изменений, так как в большинстве случаев бэкенд базы данных возвращает значение в нужном формате (например, строка Python string).

Если ваш подкласс Field работает со структурами более сложными чем строка, дата и числа, вам следует переопределить этот метод. Метод должен корректно работать со следующими аргументами:

  • Объект нужного типа (например, Hand в нашем примере).

  • Строка (например, при десериализации).

  • Значение, возвращаемое базой данных.

В нашем HandField мы сохраняем значение в поле VARCHAR и должны обрабатывать строки и объекты Hand в методе to_python():

import re

class HandField(models.Field):
    # ...

    def to_python(self, value):
        if isinstance(value, Hand):
            return value

        # The string case.
        p1 = re.compile('.{26}')
        p2 = re.compile('..')
        args = [p2.findall(x) for x in p1.findall(value)]
        if len(args) != 4:
            raise ValidationError("Invalid input for a Hand instance")
        return Hand(*args)

Помните, что мы всегда возвращаем объект Hand из этого метода. Это объект Python, который мы хотим сохранить в модели. Если преобразование значения не может быть выполнено, вызовите исключение ValidationError.

Помните: если вашему полю необходим вызов to_python() при создании, используйте вышеупомянутый `метакласс The SubfieldBase`_. Иначе метод to_python() не будет автоматически вызван.

Предупреждение

Если ваше поле позволяет указать null=True, все методы поля, которые принимают value, такие как to_python() и get_prep_value(), должны учитывать ситуацию, когда value равно None.

Преобразование объектов Python в значения в запросе

Field.get_prep_value(self, value)

Есть метод обратный to_python() при работе с бэкэндом базы данных (но не сериализатором). Аргумент value - это значение атрибута модели (поле не содержит ссылку на модель и не может получить значение самостоятельно), метод должен вернуть данные для подстановки в запрос.

Это преобразование не должно выполнять ничего, что зависит от типа базы данных. Если необходимо преобразование специфическое для какой либо базы данных, его необходимо выполнить в методе get_db_prep_value().

Например:

class HandField(models.Field):
    # ...

    def get_prep_value(self, value):
        return ''.join([''.join(l) for l in (value.north,
                value.east, value.south, value.west)])

Преобразование значения из запроса в значение базы данных

Field.get_db_prep_value(self, value, connection, prepared=False)

Некоторые типы данных (например, даты) должны быть в определенном формате при передаче в бэкэнд базы данных. Эти преобразования должны быть выполнены в get_db_prep_value(). Объект подключения к базе данных передается в аргументе connection. Это позволяет выполнить преобразование, которое зависит от используемой базы данных.

Аргумент prepared указывает, было ли значение обработано get_prep_value(). При prepared равном False get_db_prep_value() по умолчанию вызовет get_prep_value() перед дальнейшим преобразованием.

Field.get_db_prep_save(self, value, connection)

Аналогичен предыдущему методу, но вызывается когда значение Field сохраняется в БД. По умолчанию вызывается метод get_db_prep_value(), вы не должны ничего менять, если нет необходимости выполнять дополнительное преобразование значения именно при сохранении, а не каких либо других запросах (что выполняется в get_db_prep_value()).

Обработка данных перед сохранением

Field.pre_save(self, model_instance, add)

Этот метод вызывается перед get_db_prep_save() и должен вернуть значение атрибута из model_instance для этого поля. Название атрибута хранится в self.attname (устанавливается в Field). При сохранении данных в базу данных первый раз, аргумент add будет равен True, иначе - False.

Вы должны переопределить этот метод, если хотите изменить значение перед сохранением. Например, поле DateTimeField использует этот метод для установки значения при auto_now или auto_now_add.

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

Подготовка значений при поиске в базе данных

Как и преобразование значения поля, преобразование значения для поиска(WHERE) в базе данных выполняется в две фазы.

Field.get_prep_lookup(self, lookup_type, value)

get_prep_lookup() выполняет первую фазу, проверяя значение

Подготавливает value для передачи в фильтр запроса (WHERE). lookup_type содержит один из фильтров Django: exact, iexact, contains, icontains, gt, gte, lt, lte, in, startswith, istartswith, endswith, iendswith, range, year, month, day, isnull, search, regex и iregex.

Ваш метод должен учитывать все возможные значения lookup_type и вызвать исключение ValueError, если value содержит неверное значение (например, список, в то время, когда вы ожидаете объект) или TypeError, если ваше поле не поддерживает данный тип фильтра. Для большинства полей вы можете добавить обработку определенных фильтров, для всех остальных использовать метод get_db_prep_lookup() родительского класса.

Если вы переопределяете get_db_prep_save(), скорее всего вам необходимо переопределить и метод get_prep_lookup(). Если этого не сделать, будет использовать реализация get_prep_value по умолчанию для обработки фильтров exact, gt, gte, lt, lte, in и range.

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

Заметьте, что для range и in метод get_prep_lookup принимает список объектов (предположительно правильного типа) и должен вернуть список параметров для запроса. В большинстве случаев вы можете использовать get_prep_value() для объектов списка.

Например, следующий код реализует метод get_prep_lookup, ограничивая используемые фильтры до exact и in:

class HandField(models.Field):
    # ...

    def get_prep_lookup(self, lookup_type, value):
        # We only handle 'exact' and 'in'. All others are errors.
        if lookup_type == 'exact':
            return self.get_prep_value(value)
        elif lookup_type == 'in':
            return [self.get_prep_value(v) for v in value]
        else:
            raise TypeError('Lookup type %r not supported.' % lookup_type)
Field.get_db_prep_lookup(self, lookup_type, value, connection, prepared=False)

Выполняет преобразование параметров фильтра с учетом типа базы данных. Как и в метод get_db_prep_value() передается аргумент connection. Параметр prepared указывает, было ли значение преобразовано методом get_prep_lookup().

Определение поля формы для поля модели

Field.formfield(self, form_class=None, choices_form_class=None, **kwargs)

Возвращает поле формы, которое будет использовано при генерации формы для модели. Этот метод используется в ModelForm.

Класс поля формы можно указать аргументами form_class и ``choices_form_class``(используется, если для поля указан список возможных значений). Если аргументы не указаны, будут использоваться CharField или TypedChoiceField.

Словарь kwargs передается в конструктор __init__() поля формы. Скорее всего вам понадобится определить необходимые аргументы для form_class``(и возможно ``choices_form_class) и передать дальнешую обработку в метод родительского класса. Возможно вам понадобиться создать собственный тип поля формы (и возможно даже свой виджет). Смотрите раздел о формах.

Продолжая наш пример, мы можем создать следующий метод formfield():

class HandField(models.Field):
    # ...

    def formfield(self, **kwargs):
        # This is a fairly standard way to set up some defaults
        # while letting the caller override them.
        defaults = {'form_class': MyFormField}
        defaults.update(kwargs)
        return super(HandField, self).formfield(**defaults)

Подразумевается, что мы уже импортировали класс поля MyFormField (который содержит свой собственный виджет). Этот раздел не описывает создание собственного поля формы.

Эмуляция встроенных полей

Field.get_internal_type(self)

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

Если вы определили метод db_type(), нет необходимости использовать get_internal_type() – он не будет использоваться. Иногда одни типы полей работают так же, как и другие на уровне базы данных, в таких случаях вы можете использовать этот метод.

Например:

class HandField(models.Field):
    # ...

    def get_internal_type(self):
        return 'CharField'

Без разницы какую базу данных мы используем, syncdb и другие SQL выберут правильный тип поля в базе данных.

Если get_internal_type() возвращает название, неизвестное Django – то есть его нет в django.db.backends.<db_name>.creation.DATA_TYPES – оно будет использовано сериализатором, но метод db_type() по умолчанию вернет None. Смотрите описание db_type() чтобы понять, в каких случаях это может быть полезно. Возвращение строки, описывающей поле для сериализатора, может быть хорошей практикой.

Преобразование значения поля для сериалайзера

Field.value_to_string(self, obj)

Этот метод используется сериализатором. Вызов Field._get_val_from_obj(obj) - лучший способ получить значение для сериализатора. Например, так как HandField использует строку для хранения в базе данных, мы можем использовать существующий код:

class HandField(models.Field):
    # ...

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_prep_value(value)

Несколько советов

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

  1. Посмотрите на существующие поля в Django (в django/db/models/fields/__init__.py). Постарайтесь найти поле, похожее на то, что вам необходимо, это лучше, чем создавать свое поле с нуля.

  2. Добавьте метод __str__() или __unicode__() в класс, который вы используете для значений вашего поля. Во многих случаях используется функция force_text() при обработке значений. (В нашем примере, value будет объект Hand, не HandField). Если метод __unicode__()``(``__str__ для Python 3) преобразует объект Python в строку, это сохранит вам много времени.

Создание подкласса FileField

В дополнение к вышеописанным методам, поля, которые работают с файлами, требуют дополнительной работы. Основной функционал FileField, такой как сохранение и получения данных в БД, можно оставить без изменений, определив лишь операции, необходимые для работы с различными типами файлов.

Django предоставляет класс File, который используется как прокси при работе с файлами. Можно унаследоваться от него и переопределить работу с файлом. Он находится в django.db.models.fields.files и описан в разделе о файлах.

После создания подкласса File новый подкласс FileField может использовать его. Просто укажите подкласс File в атрибуте attr_class подкласса FileField.

Несколько советов

В дополнение ко всему сказанному выше, вот несколько советов, которые помогут улучшить ваш код.

  1. Пример встроенного в Django поля ImageFielddjango/db/models/fields/files.py) - хороший пример переопределения FileField, изучите его.

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