Свои поля¶
Своё поле — подкласс hare.fields.Field.
Что переопределяет поле¶
Обычно переопределяют:
field_type— тип значения поля в Python, атрибут класса:field_type = int.SQL_TYPE— строка или@property.to_db_value(self, value, instance)/from_db_value(self, value)— своё преобразование при записи в базу и чтении из неё;to_lookup_value(self, value, instance)преобразует значение фильтра (по умолчанию — черезto_db_value()).to_db_value()преобразует значение и не меняетinstance: то, что запись ставит самому объекту (отметкуauto_now, слепой индекс), ставится до преобразования значений.to_python(self, value)— приводит к нужному виду значение, только что переданное вModel(**kwargs). Нужен, только если такое значение должно проходить ту же обработку, чтоfrom_db_valueделает для прочитанного из базы (см. ниже); если параto_db_value/from_db_valueнесимметрична (шифрование, маскирование и т. п.), не трогайте его.get_json_value(self, value)— форма значения в JSON, то, что хранит событие outbox у захваченного изменения. По умолчанию пишет собственные значения JSON и значения из таблицы OutboxJsonEncoding, в том числе элементы списков и словарей; поле, у значений которого там нет формы (свой объект), возвращает форму, которую прочтут потребители, — иначеTypeError, и падает запись, а не событие.get_computed_field(self, computation)— поле, которым читается результат, вычисленный из значений поля;computation—ValueComputation(hare.fields.enums:SUM,AVERAGE,STATISTIC,ARITHMETIC). По умолчанию само поле; поле, с которым база считает в более широком типе, возвращает поле этого типа —Float32FieldClickHouse даётFloatFieldдля суммы, среднего и арифметики.get_column_type_options(self)— что, кроме класса, задаёт тип колонки поля, кортежем: приведение к полю (Cast(value, field)) хранит план отдельно для каждого набора этих параметров. По умолчанию();ClickhouseDatetimeFieldClickHouse отдаёт свою точность и часовой пояс.deconstruct()— для автоматического создания миграций, если поле принимает аргументы конструктора помимо базовых.migration_import_path— неизменный путь к классу через точку, если модуль поля может переехать.
from hare.fields import Field
class MoneyField(Field[int]):
"""Хранит сумму в копейках целым числом."""
field_type = int
SQL_TYPE = "BIGINT"
def to_db_value(self, value, instance):
self.validate(value)
return None if value is None else int(value)
def from_db_value(self, value):
return None if value is None else int(value)
Только что присвоенные значения: to_python¶
При создании объекта через Model(**kwargs)/Model.objects.create(**kwargs) каждое значение проходит
через to_python(self, value), а не через from_db_value(): значение, прочитанное из базы, и значение,
которое только что передал вызывающий код, — разные вещи, хотя обрабатывать их часто нужно одинаково. По
умолчанию метод возвращает значение без изменений — это безопасно для любого поля, у которого пара
to_db_value/from_db_value не является точным и повторяемым без вреда кодированием и раскодированием
уже готового значения Python.
Переопределите to_python() (обычно достаточно вызвать self.from_db_value(value) — то же
преобразование, что получает значение из базы), если только что присвоенному значению действительно нужно
такое же приведение, например разбор строки ISO в настоящий datetime.
Не переопределяйте его (оставьте поведение по умолчанию), если to_db_value/from_db_value
несимметричны — кодирование и раскодирование, которые не отменяют друг друга для уже раскодированного
значения, как при шифровании или маскировании. Если применить from_db_value (раскодирование) к
значению, которое никогда не проходило через to_db_value, оно тихо испортится в памяти, а следующий
save() закодирует уже испорченное значение; у поля с шифрованием это значит, что в базу запишется
открытый текст вместо шифротекста.
class EncryptedField(Field[str]):
field_type = str
SQL_TYPE = "TEXT"
def to_db_value(self, value, instance):
self.validate(value)
return None if value is None else encrypt(value)
def from_db_value(self, value):
return None if value is None else decrypt(value)
# to_python() не переопределён, и это правильно: Model(secret="hello") должен
# хранить в памяти "hello", а не вызывать decrypt() для значения, которое никто не шифровал.
Хранение на разных диалектах¶
У каждого диалекта есть реестр типов: для класса поля он задаёт, как поле хранится, — тип колонки,
команду создания первичного ключа, который генерирует база, и приведение типа, которым колонка
оборачивается везде, где её сравнивают или сортируют. Класс поля использует запись ближайшего
зарегистрированного базового класса, поэтому MoneyField выше везде получает BIGINT, пока диалект не
зарегистрирует для него что-то другое:
from hare.dialects.base.types import TypeMapping
from hare.dialects.dialect_registry import DialectRegistry
DialectRegistry.get_dialect("sqlite").types.register(MoneyField, TypeMapping(column_type="INTEGER"))
Поле с COLUMN_TYPE_FROM_DIALECT = True существует там, где типы диалекта дают ему тип колонки
(TypeMapping.column_type, зарегистрированный для его класса или класса между ним и тем, что задаёт этот
флаг), и больше нигде; модель с ним, привязанная к подключению другого диалекта, даёт UnSupportedError
(Hare.init()), как и поле, объявление которого тип диалекта не принимает (PointField(null=True) на
ClickHouse: точка там не хранит NULL). Поле одной базы — такое поле, его тип регистрирует диалект этой
базы; назвать тип само поле при этом может:
class LtreeField(Field[str]):
SQL_TYPE = "ltree"
COLUMN_TYPE_FROM_DIALECT = True
DialectRegistry.get_dialect("postgresql").types.register(
LtreeField, TypeMapping(column_type=lambda field: field.SQL_TYPE, extension="ltree")
)
Поле, которое хранят несколько диалектов, каждый в своём типе, не называет ни один из них: оно задаёт
COLUMN_TYPE_FROM_DIALECT = True и существует там, где типы диалекта дают ему тип колонки
(TypeMapping.column_type), — таковы GeometryField и VectorField. Диалект, построенный на типах
другого, убирает такое поле пустым описанием: types.register(GeometryField, TypeMapping()).
Всё, чего TypeMapping не задаёт, остаётся за полем:
| Атрибут | Что задаёт |
|---|---|
column_type |
Тип колонки или функция (field) -> str, которая его строит. |
generated_sql |
DDL первичного ключа, который генерирует база. |
function_cast |
Функция (field, term) -> term, которая оборачивает колонку везде, где её сравнивают, сортируют или копируют. |
to_db |
Функция (field, value, instance) -> value вместо to_db_value() поля. |
to_db_refuses_only |
to_db только отклоняет значение — то, которое native_write_check поля оставляет ему, — а любое другое пишет так же, как поле: ускоритель записи сам пишет все остальные значения (так FloatField в SQLite отклоняет NaN). |
to_lookup |
Функция (field, value, instance) -> value вместо to_lookup_value() поля. |
to_python |
Функция (field, value) -> value вместо from_db_value() поля. |
json_term |
Функция (field, term) -> term, дающая текст, которым значение записывается в объект JSON, — для значения, хранящегося в виде, который JSON не вмещает (UUID в 16 байтах). |
naive_datetime_is_utc |
Собственный from_db_value() поля, а дата-время без пояса от драйвера читается как момент в UTC. |
extension |
Расширение базы, которое нужно типу колонки, — создаётся везде, где используется поле этого класса (postgis для GeometryField, vector для VectorField в PostgreSQL). |
inserted_by_select |
INSERT, записывающий колонку, пишет строки как SELECT из них, а не VALUES, — для значения, записанного выражением, которое VALUES диалекта не читает; или (field) -> bool, решающая это для каждого поля. |
Результат читают field.get_column_type(dialect), field.get_generated_sql(dialect) и
field.get_function_cast(dialect); поле, которое вычисляет их из собственных настроек, переопределяет
эти методы.
Операторы фильтра, пути внутри значения и вычисляемые колонки¶
Поле, значение которого — не простое скалярное значение, объясняет hare, как по нему фильтровать,
переопределяя эти методы Field. Так устроены собственные поля массивов, диапазонов и hstore в hare,
поэтому стороннее поле получает те же возможности без изменений в самом hare:
| Метод / атрибут | Что даёт |
|---|---|
get_lookups() |
Операторы фильтра поля по окончанию ("" — обычное равенство), каждый — FieldLookup, как у register_lookup(); по умолчанию общий набор. Поле со своим набором возвращает его вместо общего, и фильтр с окончанием не из него даёт FieldError. Операторы из register_lookup() добавляются к нему. |
get_path_transform(segment) |
Как часть пути после имени поля читается внутри его значения — (function building the term from the field's term, field of the value read) или None. Этому следуют фильтры (tags__0__startswith), F(), values() и order_by(). |
get_lookup_value_description(lookup) |
(LookupValueShape, type) значения оператора, если это не значение типа поля (contains у массива принимает список элементов), — для get_lookup_info(); иначе None. |
get_like_text_function() |
Как значение превращается в текст для contains/icontains/startswith/... и операторов по шаблону; None приводит его к VARCHAR. |
get_generated_from_field_names() / with_renamed_generated_from_field(old, new) |
Другие поля модели (по имени), из которых вычисляется генерируемая колонка: миграция не даст удалить такое поле, пока эта колонка его читает, а переименование поля переименовывает его и здесь. |
holds_container_value |
True для массива, диапазона или значения вроде JSON: вычисляемое значение такого поля получает его собственные операторы фильтра. |
holds_intervals |
True для колонки интервального типа базы: длительность прибавляется к моменту так, как это делает база, а не как число микросекунд. |
import operator
from functools import partial
from hare.fields import CharField
from hare.query.filters import FieldLookup
from hare.sql.terms import Function
class CodeField(CharField):
def get_lookups(self):
def starts_with(term, value):
return term.like(f"{value}%")
return {"": FieldLookup(operator.eq), "prefix": FieldLookup(starts_with)}
def get_path_transform(self, segment):
if segment == "upper":
return partial(Function, "UPPER"), self
return None
Диалект может добавить часть пути к классам полей, которые определил не он, — как name__unaccent у
текстовых полей в PostgreSQL, — через
FieldClass.register_transform(segment, get_transform, required_extension=...) — get_transform(field)
возвращает ту же пару, что и get_path_transform(), — в своём Dialect.install(), который выполняется
один раз при регистрации диалекта.