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

Предисловие

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

This is an ordinary Python class, with nothing Django-specific about it. We’d like to be able to do things like this in our models (we assume the hand attribute on the model is an instance of 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 и мы укажем на все отличия.

Теория

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

Let’s start with model fields. If you break it down, a model field provides a way to take a normal Python object – string, boolean, datetime, or something more complex like Hand – and convert it to and from a format that is useful when dealing with the database. (Such a format is also useful for serialization, but as we’ll see later, that is easier once you have the database side under control).

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

Normally, you’re either writing a Django field to match a particular database column type, or you will need a way to convert your data to, say, a string.

Для нашего примера с 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 символа.

Примечание

Many of Django’s model fields accept options that they don’t do anything with. For example, you can pass both editable and auto_now to a django.db.models.DateField and it will ignore the editable parameter (auto_now being set implies editable=False). No error is raised in this case.

This behavior simplifies the field classes, because they don’t need to check for options that aren’t necessary. They pass all the options to the parent class and then don’t use them later on. It’s up to you whether you want your fields to be more strict about the options they select, or to use the more permissive behavior of the current fields.

Метод 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() returns a tuple of four items: the field’s attribute name, the full import path of the field class, the positional arguments (as a list), and the keyword arguments (as a dict). Note this is different from the deconstruct() method for custom classes which returns a tuple of three things.

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

If you add a new keyword argument, you need to write code in deconstruct() that puts its value into kwargs yourself. You should also omit the value from kwargs when it isn’t necessary to reconstruct the state of the field, such as when the default value is being used:

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

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

You can see the results of deconstruction by looking in migrations that include the field, and you can test deconstruction in unit tests by deconstructing and reconstructing the 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 пока проект содержит миграции, которые ссылаются на этот класс.

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

As always, you should document your field type, so users will know what it is. In addition to providing a docstring for it, which is useful for developers, you can also allow users of the admin app to see a short description of the field type via the django.contrib.admindocs application. To do this provide descriptive text in a description class attribute of your custom field. In the above example, the description displayed by the admindocs application for a HandField will be „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'

Методы db_type() и rel_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()

The better way of doing this would be to make the parameter specifiable at run time – i.e., when the class is instantiated. To do that, implement Field.__init__(), like so:

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

Продолжая наш пример, мы можем создать следующий метод 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.

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

To customize how the values are serialized by a serializer, you can override value_to_string(). Using value_from_object() is the best way to get the field’s value prior to serialization. For example, since HandField uses strings for its data storage anyway, we can reuse some existing conversion code:

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 и описан в разделе о файлах.

Once a subclass of File is created, the new FileField subclass must be told to use it. To do so, assign the new File subclass to the special attr_class attribute of the FileField subclass.

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

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

  1. Пример встроенного в Django поля ImageFielddjango/db/models/fields/files.py) - хороший пример переопределения FileField, изучите его.
  2. Кэшируйте объект файла при любой возможности. Так как файлы могут хранится во внешнем хранилище, получение их может потребовать дополнительного времени и даже денег. После получения файла закэшируйте как можно большую его часть, чтобы сократить количество операций необходимых для его последующего чтения.