• 3.1
  • 3.2
  • 5.0
  • Версия документации: 6.1

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

Введение

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

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

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

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

Creating custom fields requires a bit of attention to detail. To make things easier to follow, we’ll use a consistent example throughout this document: wrapping a Python object representing the deal of cards in a hand of Bridge. Don’t worry, you don’t have to know how to play Bridge to follow this example. You only need to know that 52 cards are dealt out equally to four players, who are traditionally called north, east, south and west. Our class looks something like this:

class Hand:
    """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().__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.

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

Метод 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().__init__(*args, **kwargs)

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

Если вы добавите новый аргумент ключевого слова, вам нужно написать код в deconstruct(), который помещает его значение в 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().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().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)

Атрибуты поля, не влияющие на определение столбца базы данных

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

К примеру:

class CommaSepField(models.Field):
    @property
    def non_db_attrs(self):
        return super().non_db_attrs + ("separator",)

Изменение базового класса собственного поля

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

class CustomCharField(models.CharField): ...

затем вы решили использовать TextField, вы не можете просто изменить класс:

class CustomCharField(models.TextField): ...

Вам следует создать новый класс поля и использовать его в вашей модели:

class CustomCharField(models.CharField): ...


class CustomTextField(models.TextField): ...

Как уже упоминалось в части про удаление полей, вы должны хранить начальный класс CustomCharField пока проект содержит миграции, которые ссылаются на этот класс.

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

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

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

Предположим вы создали собственный тип поля для 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.vendor == "mysql":
            return "datetime"
        else:
            return "timestamp"

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

Some database column types accept parameters, such as CHAR(25), where the parameter 25 represents the maximum column length. In cases like these, it’s more flexible if the parameter is specified in the model rather than being hardcoded in the db_type() method. For example, it wouldn’t make much sense to have a CharMaxlength25Field, shown here:

# This is a silly example of hardcoded 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().__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 пропустит создание этого поля в базе данных. Вам придется создать поле каким либо другим способом.

Метод rel_db_type() вызывается полями ForeignKey и OneToOneField, которые указывают на другие поля, чтобы узнать тип поля в базе данных. Например, у вас есть поле UnsignedAutoField, и вам необходимо, чтобы поле, которое ссылаются на ваше поле, использовали такой же тип поля:

# MySQL unsigned integer (range 0 to 4294967295).
class UnsignedAutoField(models.AutoField):
    def db_type(self, connection):
        return "integer UNSIGNED AUTO_INCREMENT"

    def rel_db_type(self, connection):
        return "integer UNSIGNED"

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

Если ваш подкласс 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
from django.utils.translation import gettext_lazy as _


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):
        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().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.

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

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

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

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

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

If you wish to exclude the field from the ModelForm, you can override the formfield() method to return None.

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

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

    def formfield(self, **kwargs):
        # Exclude the field from the ModelForm when some condition is met.
        some_condition = kwargs.get("some_condition", False)
        if some_condition:
            return None

        # Set up some defaults while letting the caller override them.
        defaults = {"form_class": MyFormField}
        defaults.update(kwargs)
        return super().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(). Вызов value_from_object() – лучший способ получить значение для сериализатора. Например, так как HandField использует строку для хранения в базе данных, мы можем использовать существующий код:

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

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

Несколько важных советов

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

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

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

Back to Top