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

Предисловие

Раздел о моделях описывает, как использовать стандартные поля модели 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 будет сохранен в текстовом поле базы данных.

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

Все поля в 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__() принимает следующие параметры:

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

Деконструкция поля

Добавлено в Django 1.7:

deconstruct() – часть приложения миграций в Django 1.7 и выше. Если вы используете поля из предыдущих версий, вам необходимо добавить этот метод перед тем, как использовать их в миграциях.

Метод deconstruct() является дополнением к методу __init__(). Этот метод указывает Django как сериализировать экземпляр поля, а точнее – какие аргументы передать в __init__() чтобы воссоздать его.

Если вы не добавляли аргументы в дочерний класс встроенного поля, вам не нужно переопределять метод deconstruct(). Однако, если вы изменили аргументы __init__() (как мы сделали это в поле HandField), вам необходимо добавить их.

Формат ответа deconstruct() простой. Он должен возвращать кортеж из четырех элементов: имя атрибута поля, полный путь импорта класса поля, позиционные аргументы (списком) и именованные аргументы (словарем). Обратите внимание, это отличается от метода deconstruct() для собственного класса, которые должен вернуть кортеж из трех элементов.

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

Например, в нашем классе HandField мы определяем max_length в __init__(). Метод deconstruct() базового класса Field вернет его в именованных аргументах. Но мы можем удалить его для читабельности:

from django.db import models

class HandField(models.Field):

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

    def deconstruct(self):
        name, path, args, kwargs = super(HandField, self).deconstruct()
        del kwargs["max_length"]
        return name, path, args, kwargs

Если вы добавили именованный аргумент, вам следует вернуть его в kwargs:

from django.db import models

class CommaSepField(models.Field):
    "Implements comma-separated storage of lists"

    def __init__(self, separator=",", *args, **kwargs):
        self.separator = separator
        super(CommaSepField, self).__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(CommaSepField, self).deconstruct()
        # Only include kwarg if it's not the default
        if self.separator != ",":
            kwargs['separator'] = self.separator
        return name, path, args, kwargs

Более сложные примеры выходят за рамки этой документации, но помните - для любой конфигурации поля, deconstruct() должен вернуть аргументы, которые можно передать в __init__, чтобы воссоздать экземпляр поля.

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

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

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

name, path, args, kwargs = my_field_instance.deconstruct()
new_instance = MyField(*args, **kwargs)
self.assertEqual(my_field_instance.some_attribute, new_instance.some_attribute)

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

Конечно же вам необходимо задокументировать ваше поле, чтобы пользователи знали как его использовать. В дополнение к 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__, можно переходить к переопределению методов, которые определяют поведение вашего поля. Методы описанные ниже идут в порядке убывания важности.

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

Предположим вы создали собственный тип поля для 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 запросов – когда вы создаете таблицы в базе данных для приложения. Также при создании условий в WHERE, которые используют поле – это когда вы используете методы QuerySet для получения данных, такие как get(), filter() или exclude(), и используете ваше поле в качестве аргумента. Больше нигде этот метод не используется, вы можете использовать достаточно сложный код, как проверка 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()

Лучше позволить указывать параметр при определении поля – то есть при создании класса модели. Для этого переопределите метод 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

Изменено в Django 1.8:

Исторически Django предоставляет метакласс SubfieldBase, который вызывает to_python() при каждом указании значения поля. Такой подход не очень хорошо работал с преобразованиями на уровне базы данных, агрегациями, или получением данных через values(), и был заменен методом from_db_value().

Если ваш подкласс Field работает со структурами более сложными, чем строка, дата и число, вам следует переопределить from_db_value() и to_python().

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

to_python() вызывается при десериализации и при вызове метода clean() в формах.

Метод to_python() должен корректно обрабатывать следующие типы значения:

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

  • Строка

  • None (если поле содержит null=True)

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

import re

from django.core.exceptions import ValidationError
from django.db import models

def parse_hand(hand_string):
    """Takes a string of cards and splits into a full hand."""
    p1 = re.compile('.{26}')
    p2 = re.compile('..')
    args = [p2.findall(x) for x in p1.findall(hand_string)]
    if len(args) != 4:
        raise ValidationError("Invalid input for a Hand instance")
    return Hand(*args)

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

    def from_db_value(self, value, expression, connection, context):
        if value is None:
            return value
        return parse_hand(value)

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

        if value is None:
            return value

        return parse_hand(value)

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

Если to_python() не может выполнить преобразование значения, вызовите исключение ValidationError.

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

Т.к. использование базы данных требует преобразования значения в оба ннаправления, если вы переопределили to_python() ва следует переопределить и get_prep_value() чтобы преобразовать объект Python обратно в значение для запроса.

Например:

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)])

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

Если ваше поле использует типы CHAR, VARCHAR или TEXT MySQL, метод get_prep_value() всегда должен возвращать строку. MySQL выполняет довольно непредсказуемое сравнение типов, если передать число, что может привести к неожиданными результатам запроса. Этой проблемы можно избежать возвращая всегда строку из get_prep_value().

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

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

Например, Django использует следующий метод для BinaryField:

def get_db_prep_value(self, value, connection, prepared=False):
    value = super(BinaryField, self).get_db_prep_value(value, connection, prepared)
    if value is not None:
        return connection.Database.Binary(value)
    return value

Если ваше поле требует дополнительного преобразования данных при сохранении, переопределите для этого метод get_db_prep_save().

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

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

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

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

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

get_prep_lookup() выполняет первую фазу подготовки параметров фильтрации: преобразование типа и проверку данных.

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

Добавлено в Django 1.7:

Если вы используете собственные фильтры lookup_type может быть любой lookup_name, который используется дополнительными фильтрами.

Ваш метод должен учитывать все возможные значения 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)

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

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

Чтобы переопределить поле формы, которое будет использоваться ModelForm, вы можете переопределить formfield().

Класс поля формы можно указать аргументами 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 (который содержит свой собственный виджет). Этот раздел не описывает создание собственного поля формы.

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

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

Например:

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

    def get_internal_type(self):
        return 'CharField'

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

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

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

Чтобы указать как значения сериализуются сериализатором, переопределите метод value_to_string(). Вызов 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__() в Python 2) в класс, который вы используете для значений вашего поля. Во многих случаях используется функция force_text() при обработке значений. (В нашем примере, value будет объект Hand, не HandField). Если метод __str__()``(``__unicode__ для Python 2) преобразует объект 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. Кэшируйте объект файла при любой возможности. Так как файлы могут хранится во внешнем хранилище, получение их может потребовать дополнительного времени и даже денег. После получения файла закэшируйте как можно большую его часть, чтобы сократить количество операций необходимых для его последующего чтения.