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

Writing custom model fields

Введение

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

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

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

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

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

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) в формат удобный для хранения в базе данных и обратно (и сериализация, но, как мы увидим далее, это решается естественным способом при решении проблем преобразования данных для базы данных).

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

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

For our Hand example, we could convert the card data to a string of 104 characters by concatenating all the cards together in a pre-determined order – say, all the north cards first, then the east, south and west cards. So Hand objects can be saved to text or character columns in the database.

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

Все поля в 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.

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

The counterpoint to writing your __init__() method is writing the deconstruct() method. It’s used during model migrations to tell Django how to take an instance of your new field and reduce it to a serialized form - in particular, what arguments to pass to __init__() to re-create it.

Если вы не добавляли аргументы в дочерний класс встроенного поля, вам не нужно переопределять метод 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)

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

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

If you aim to build a database-agnostic application, you should account for differences in database column types. For example, the date/time column type in PostgreSQL is called timestamp, while the same column in MySQL is called datetime. You can handle this in a db_type() method by checking the connection.settings_dict['ENGINE'] attribute.

К примеру:

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

The db_type() and rel_db_type() methods are called by Django when the framework constructs the CREATE TABLE statements for your application – that is, when you first create your tables. The methods are also called when constructing a WHERE clause that includes the model field – that is, when you retrieve data using QuerySet methods like get(), filter(), and exclude() and have the model field as an argument. They are not called at any other time, so it can afford to execute slightly complex code, such as the connection.settings_dict check in the above example.

Некоторые типы полей принимают параметры, например 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().__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)

In our HandField class, we’re storing the data as a VARCHAR field in the database, so we need to be able to process strings and None in the from_db_value(). In to_python(), we need to also handle Hand instances:

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

Продолжая наш пример, мы можем создать следующий метод 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().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. Look at the existing Django fields (in django/db/models/fields/__init__.py) for inspiration. Try to find a field that’s similar to what you want and extend it a little bit, instead of creating an entirely new field from scratch.

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

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

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

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

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

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

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

  1. The source for Django’s own ImageField (in django/db/models/fields/files.py) is a great example of how to subclass FileField to support a particular type of file, as it incorporates all of the techniques described above.

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

Back to Top