Типы полей¶
Поля hare.fields: аргументы, которые принимает любое поле, поля с обычными значениями,
поля-перечисления, вычисляемые колонки и составные первичные ключи, — и где находятся поля одной базы.
Базовый класс Field¶
Каждое поле наследует hare.fields.Field[TValue]. Аргументы его конструктора есть у всех типов полей
ниже:
class Field:
def __init__(
self,
*,
source_field: str | None = None,
generated: bool = False,
primary_key: bool | None = None,
null: bool = False,
default: Any = None,
db_default: Any = NOT_PROVIDED,
unique: bool = False,
db_index: bool | None = None,
description: str | None = None,
validators: list[Validator | Callable[[Any], None]] | None = None,
sensitive: bool = False,
**kwargs: Any,
) -> None: ...
| Аргумент | Что задаёт |
|---|---|
source_field |
Имя колонки в базе (по умолчанию — имя атрибута поля в Python). Если два поля одной модели указывают на одну колонку, будет ConfigurationError. |
generated |
Значение колонки создаёт сама база (SERIAL, AUTOINCREMENT и подобные) — см. generated |
primary_key |
Колонка — первичный ключ таблицы (с индексом и уникальностью через ограничение PRIMARY KEY). null=True вместе с primary_key=True даёт ConfigurationError. |
null |
Колонка допускает NULL. |
default |
Значение по умолчанию на стороне Python: само значение, обычная функция или асинхронная функция — см. default |
db_default |
Настоящее значение по умолчанию в базе (DEFAULT): постоянное значение или объект SqlDefault/Now/RandomHex/UuidV7 (hare.fields.db_defaults) — см. db_default |
unique |
Ограничение уникальности. |
db_index |
Отдельный индекс по этой колонке. У ForeignKeyField включён по умолчанию — см. Связи. |
description |
Записывается комментарием к колонке в базе. |
validators |
Список объектов Validator и/или обычных функций — см. Проверки значений. |
sensitive |
Помечает секретные данные (учётные данные, токены, персональные данные) — см. Чувствительные поля. На схему базы не влияет. |
Аргумент, которого поле не принимает, даёт TypeError с его именем. Аргументы, которые попадают в
схему, проверяются: description и source_field не текстом и max_length не int дают
ConfigurationError. Флаг (null, unique, generated, primary_key, db_index, sensitive)
читается по истинности, как его читает Python, а validators — это список; ни то, ни другое не
проверяется. Имя поля не может
содержать __, которое разделяет части пути поиска (author__name), и не может оканчиваться на _ —
from___gt читалось бы как поле from и оператор _gt: такое поле или такой же related_name дают
ConfigurationError при объявлении модели, как и пустое имя колонки или таблицы либо имя с нулевым
байтом, а также description, текстовый default или db_default с нулевым байтом: SQL-текст его
не вмещает, и миграция упала бы только там, где его пишет. Колонка с именем ключевого слова сохраняет имя
через source_field (from_field = fields.IntField(source_field="from")), как пишет его
inspectdb.
Кроме того, у каждого поля есть: to_db_value(value, instance), from_db_value(value),
validate(value), required (свойство: True, если поле не допускает NULL, у него нет значения по
умолчанию и db_default и его не генерирует база), deconstruct() (путь класса поля и аргументы
конструктора — то, что записывает файл миграции и сравнивает автоматическое создание миграций),
get_annotation() (тип Python значения поля для схемы: перечисление у поля-перечисления, | None, если
поле допускает NULL, — его читают создание моделей pydantic, DTO Litestar и параметры request-query) и
constraints — словарь ограничений в стиле JSON Schema, например max_length/ge/le.
Что это за поле, можно узнать без проверки его класса:
| Атрибут | Значение |
|---|---|
relation_type |
Вид связи — RelationType (hare.fields): FOREIGN_KEY, ONE_TO_ONE, MANY_TO_MANY, BACKWARD_FOREIGN_KEY, BACKWARD_ONE_TO_ONE; None у поля с обычным значением, в том числе у колонки ключа прямой связи (author_id). |
encrypted |
True у зашифрованного поля: в его колонке лежит шифротекст, поэтому его нельзя сравнивать по значению, сортировать, группировать и использовать в агрегатах. |
enum_type |
Класс перечисления у IntEnumField/CharEnumField; None у остальных полей. |
model / model_field_name |
Модель, к которой привязано поле, и имя его атрибута в ней. |
generated¶
Значение колонки создаёт сама база (SERIAL, AUTOINCREMENT и подобные). Если класс поля так не умеет,
будет ConfigurationError. Генерируемое целочисленное поле, которое не является первичным ключом, может
только описывать уже существующую колонку (например, колонку IDENTITY в PostgreSQL): создание таблиц и
миграции для него дают ConfigurationError, поэтому у такой модели должно быть Meta.managed = False.
default¶
Значение по умолчанию на стороне Python: само значение, обычная функция или асинхронная функция;
вычисляется при save(). В схему базы не попадает. Значение (или результат функции) преобразуется так
же, как значение, переданное явно, поэтому в памяти оказывается тот же тип, что вернёт последующее
чтение: default="red" у CharEnumField — это элемент перечисления, default=1.5 у DecimalField —
Decimal("1.50"), datetime без часового пояса получает настроенный пояс.
Файл миграции записывает значение кодом: литерал, список, кортеж, словарь, множество или frozenset из них,
Decimal, UUID, дату или время, IP-адрес или сеть (ipaddress), элемент перечисления, функцию или класс
уровня модуля либо объект с deconstruct(). Любое другое значение — Fraction, deque, экземпляр
собственного класса — заставляет makemigrations бросить ConfigurationError, а не записать файл, который
не загрузится; дайте такому значению deconstruct() или укажите по умолчанию функцию, которая его возвращает.
db_default¶
Настоящее значение по умолчанию в базе (DEFAULT): постоянное значение или объект
SqlDefault/Now/RandomHex/UuidV7 (hare.fields.db_defaults). Функцией быть не может. Если у поля
заданы и default=, и db_default=, выдаётся предупреждение RedundantDbDefaultWarning: при заданном
default значение всегда подставляется до отправки INSERT, и db_default не применяется никогда.
Исключение — ForeignKeyField/OneToOneField с on_delete=SET_DEFAULT и настоящим ограничением в базе:
там база сама берёт db_default при удалении, и предупреждения нет.
Значения по умолчанию в базе (hare.fields.db_defaults)¶
class Book(Model):
copies = fields.IntField(db_default=SqlDefault("0")) # SQL как есть, без изменений
added_at = fields.DatetimeField(db_default=Now()) # STATEMENT_TIMESTAMP() в PostgreSQL
barcode = fields.CharField(max_length=36, db_default=RandomHex()) # случайная hex-строка, своя для каждого диалекта
SqlDefault(sql: str) вставляет строку sql как есть в команды создания таблиц — в generate_schemas()
и в миграции. Никогда не собирайте её из данных, пришедших извне. В SQLite выражение, которое не
является литералом или числом со знаком (SqlDefault("40 + 2")), берётся в скобки — этого требует
DEFAULT в SQLite. Now() и RandomHex() — готовые варианты для двух самых частых случаев, одинаково
работающие на разных диалектах. RandomHex() сам выбирает выражение для диалекта:
(lower(hex(randomblob(16)))) в SQLite, md5(random()::text) в PostgreSQL. Свой подкласс — его SQL
пишет рендерер, который диалект для него регистрирует, как у RandomHex, — записывается в миграцию своим
классом и вызывается без аргументов; подкласс с аргументами переопределяет deconstruct(). В SQLite
Now() записывает такой же текст, каким хранится значение DatetimeField, записанное из Python (UTC с
суффиксом +00:00 при use_timezone=True, местное время без пояса при use_timezone=False), поэтому
фильтры по точному значению, __in, по диапазону и get_or_create() находят строку, заполненную этим
значением. У DateField и TimeField Now() записывает текущую дату или время на часах в настроенном
поясе (в местном поясе системы при use_timezone=False) на любом диалекте — то же значение, которое
записала бы из Python Timezone.localtime(); у TimeField — с тем же смещением, которое получает время
без пояса. Поэтому filter(day=today) находит строку. Пояс берётся в момент создания команд таблицы. В
SQLite пояс с переходом на летнее время применяет функция hare_local_now, которую hare регистрирует в
своих соединениях, поэтому строки, вставленные в такую таблицу другим клиентом SQLite, должны указывать
значение колонки явно.
UuidV7() заполняет UUIDField UUID версии 7, который база генерирует для каждой строки, —
упорядоченным по времени: строка, вставленная позже, получает больший UUID, и индекс по первичному
ключу-UUID растёт с конца, а не в случайных местах:
PostgreSQL 18 записывает его как uuidv7(); у более старого сервера PostgreSQL и у SQLite такой функции
нет, и создание колонки даёт UnSupportedError до любых команд DDL (Features.supports_uuid_v7). После
create() в экземпляре тот UUID, который сгенерировала база.
UUID версии 7 можно получить и в Python, на любой базе: default=uuid.uuid7 в Python 3.14 или функция
библиотеки в 3.12 и 3.13 — uuid_utils.uuid7, uuid6.uuid7. UUID создаётся до INSERT, поэтому он есть
в экземпляре ещё до возврата из create(), а база без генерируемых ключей — ClickHouse — принимает его
как любое другое значение. ClickHouse упорядочивает UUID сначала по младшим 64 битам, поэтому ключ версии
7 там не сортируется по времени; сортируйте по колонке с датой. Миграция записывает функцию по пути импорта:
Постоянное значение db_default для даты-времени или времени записывается так же, как то же значение,
записанное из Python: в SQLite значение по умолчанию DatetimeField хранится текстом в UTC
(2020-01-02 03:00:00+00:00); в PostgreSQL значение DatetimeField без пояса при use_timezone=False
получает местное смещение системы, а значение TimeField — своё смещение (время без пояса получает
стандартное смещение настроенного пояса при use_timezone=True и UTC при use_timezone=False).
Поле с db_default без собственного значения держит в новом объекте DatabaseDefault (hare.fields):
INSERT не пишет его колонку, и её заполняет база. Это значение ложно;
isinstance(value, DatabaseDefault) отличает его от других ложных значений. bulk_create() не пишет
колонку, которую все объекты оставляют базе, и бросает QueryError для колонки, которую оставляют только
некоторые.
Поля с данными (hare.fields)¶
| Поле | Тип в Python | Дополнительные аргументы | Тип в SQL (по умолчанию / PostgreSQL) |
|---|---|---|---|
IntField |
int |
с primary_key по умолчанию generated=True |
INT / INTEGER, SERIAL при генерации — ограничения ge=-2^31, le=2^31-1 |
BigIntField |
int |
— | BIGINT / BIGSERIAL при генерации — границы int64 |
SmallIntField |
int |
— | SMALLINT / SMALLSERIAL при генерации — границы int16 |
PositiveSmallIntField |
int |
— | ограничения ge=0, le=int16 max |
PositiveIntField |
int |
— | ограничения ge=0, le=int32 max |
PositiveBigIntField |
int |
— | ограничения ge=0, le=int64 max |
CharField |
str |
max_length: int (обязателен, ≥1) |
VARCHAR(max_length); сам добавляет MaxLengthValidator |
TextField |
str |
— | TEXT; допускает unique=True/db_index=True и может входить в UniqueConstraint/Meta.indexes (и PostgreSQL, и SQLite индексируют TEXT сами) |
EmailField |
str |
max_length=254, lowercase: bool |
VARCHAR(max_length); адрес почты, домен записывается в нижнем регистре — см. ниже |
URLField |
str |
max_length=2048, schemes=("http", "https") |
VARCHAR(max_length); URL одной из схем schemes — см. ниже |
SlugField |
str |
max_length=50, allow_unicode: bool |
VARCHAR(max_length), с индексом по умолчанию; буквы, цифры, - и _ — см. ниже |
PhoneField |
str |
region: str |
VARCHAR(16); номер телефона, записывается в виде E.164 — см. ниже |
BooleanField |
bool |
— | BOOL (SQLite: INT); принимает только bool — 0/1 или текст дают ValidationError |
DecimalField |
Decimal |
max_digits: int, decimal_places: int (оба обязательны, max_digits ≥ 1, 0 ≤ decimal_places ≤ max_digits) |
DECIMAL(max_digits, decimal_places) в PostgreSQL и ClickHouse, в SQLite колонки нет (ниже); текст разбирается, bool даёт ValidationError |
DatetimeField |
datetime.datetime |
auto_now: bool, auto_now_add: bool (только один из двух) |
TIMESTAMP (PostgreSQL: TIMESTAMPTZ) |
DateField |
datetime.date |
— | DATE |
TimeField |
datetime.time |
auto_now, auto_now_add (местное время на часах со стандартным смещением настроенного пояса — тем же, что получает время без пояса) |
TIME (PostgreSQL: TIMETZ); timedelta или текст 24:00 дают ValidationError |
TimeDeltaField |
datetime.timedelta |
— | BIGINT (хранится в микросекундах); число вместо timedelta даёт ValidationError |
FloatField |
float |
— | DOUBLE PRECISION (SQLite: REAL) |
JSONField[T] |
dict / list (или класс модели Pydantic) |
encoder, decoder, field_type (для документации OpenAPI) |
JSON (PostgreSQL: JSONB); обычный индекс-дерево (btree: unique=True/db_index=True/Index(fields=...)) не допускается, но индекс другого вида в Meta.indexes, например GinIndex(fields=[...]), принимается |
JSONTextField |
dict / list / любое значение JSON |
как у JSONField |
колонка JSONField, но на ClickHouse — String с текстом JSON: его тип JSON теряет null у ключа и список на верхнем уровне; любое значение читается таким, каким записано (payload и headers outbox) |
UUIDField |
uuid.UUID |
version (UUID другой версии RFC 9562 отклоняется везде, где hare передаёт его в базу, — в записи, в фильтре, в ключе существующей строки и в ссылке на неё, ValidationError — значение, которое вычисляет сама база, например F(...) или db_default, не проверяется; primary_key=True без явного default сам задаёт default=uuid4, а с version=7 — Uuid7.new) |
CHAR(36) (PostgreSQL: UUID); читается как обычный uuid.UUID на любом драйвере |
BinaryField |
bytes (и bytearray, memoryview; иное даёт ValidationError) |
— | BLOB (PostgreSQL: BYTEA); фильтруется по равенству, __in, __isnull и сравнениям (порядок байтов) — текстовый lookup (__contains, ...) даёт FieldError, — обновляется через update(), принимает unique=True/db_index=True (индекс B-tree — в PostgreSQL значение больше примерно 2,7 КБ проиндексировать нельзя) |
class Book(Model):
id = fields.UUIDField(primary_key=True)
title = fields.CharField(max_length=300)
price = fields.DecimalField(max_digits=10, decimal_places=2)
tags = fields.JSONField(default=list, field_type=list[str])
added_at = fields.DatetimeField(auto_now_add=True)
updated_at = fields.DatetimeField(auto_now=True)
auto_now и auto_now_add проставляет запись, один момент на вызов: все объекты одного
bulk_create()/bulk_update() и все строки одного update()/merge()/insert_from() получают один и
тот же момент. auto_now ставит каждая запись, auto_now_add — вставка объекта, у которого значения ещё
нет. Объект получает отметку до того, как пишутся его значения: неудачная запись возвращает прежнее
значение, а объект, не загрузивший поле auto_now (.only()/.defer()), получает отметку всё равно.
Запись заданных значений (update(), merge(), insert_from()) сохраняет значение, данное такому полю.
Текст¶
Текстовое поле (CharField, TextField, SlugField, EncryptedTextField, текстовые поля PostgreSQL
CIText и фиксированной длины, текстовые FixedString/LowCardinality ClickHouse) принимает только
str: число, UUID, bytes или любой другой объект при любой записи и в фильтре дают ValidationError
— иначе записался бы или сравнивался его str(). Текстовый оператор фильтра (__contains, __icontains,
__startswith, __endswith, __iexact, __posix_regex, __search и др.) у поля любого типа принимает
только str: number__contains=5 даёт ошибку, а number__contains="5" ищет в тексте числа. Значение,
прочитанное из колонки, берётся таким, каким его вернул драйвер.
Числа¶
Целочисленное поле принимает только int: bool, float или Decimal (и 5.0 тоже) и текст ("5")
при любой записи и в фильтре на равенство или вхождение (=, __not, __in, __not_in) дают
ValidationError — int() прочитал бы "1_000" или "٥" как числа, а дробь не бывает значением
колонки. Сравнение (__gt, __gte, __lt, __lte, __range) принимает и float или Decimal и
сравнивает точно: number__gt=2.5 совпадёт с 3.
FloatField принимает int, float и Decimal; bool, текст, а также int или конечный Decimal за
пределом диапазона float (Decimal("1e500")) дают ValidationError. NaN он
отклоняет в SQLite при любой записи и как операнд фильтра — SQLite сохранил бы и подставил бы его как NULL;
PostgreSQL и ClickHouse его хранят. Фильтр по NaN сравнивает так, как сравнивает база: в PostgreSQL NaN
равен NaN и больше любого числа, в ClickHouse он не равен ничему (__not=NaN берёт все строки).
Отрицательный ноль
(-0.0) сохраняет знак в PostgreSQL и ClickHouse и читается как 0.0 в SQLite, который так его хранит;
они везде равны друг другу.
max_digits и decimal_places у DecimalField проверяются для каждого значения при save(), а не
только при объявлении поля: Decimal("12345.67") в поле с max_digits=6 даёт ValidationError, а не
тихое обрезание или неожиданную ошибку базы. Значение с большим числом знаков после запятой на любой базе
округляется от нуля: Decimal("2.345") в поле с decimal_places=2 — Decimal("2.35"). update() из
выражения записывает то, что из него делает база: PostgreSQL округляет от нуля, ClickHouse отбрасывает
лишние знаки. Отрицательный ноль (Decimal("-0.00")) читается как Decimal("0.00"): у
нуля в десятичной колонке знака нет.
DecimalField (SQLite)¶
В SQLite нет точного десятичного типа, поэтому колонки для DecimalField там нет: модель с таким полем
даёт UnSupportedError при привязке к соединению SQLite, как и запрос к SQLite со значением Decimal
или с Cast() к десятичному типу — до любого SQL. Храните деньги в целых младших единицах (IntField
или BigIntField копеек) в SQLite либо используйте PostgreSQL или ClickHouse, которые хранят точные
десятичные числа. inspectdb читает колонку SQLite DECIMAL или NUMERIC
как FloatField: у такой колонки в SQLite числовое сродство (NUMERIC affinity), которое хранит целое
или 64-битное число с плавающей точкой.
Дата и время¶
DatetimeField принимает datetime, date (её первый момент в настроенном поясе при use_timezone=True —
полночь или конец пропущенного часа при переводе часов, если он начинается в полночь, как 2024-09-08 в
America/Santiago; так же и при сравнении даты с DatetimeField), строку ISO 8601, начинающуюся с полной
даты (2024-05-01, 2024-05-01T10:00:00+03:00). DateField принимает date, datetime (берётся его дата)
или такую строку. Всё остальное — число (секунды от начала эпохи Unix тоже), просто "2024", неразборчивый
текст — даёт ValidationError при создании объекта и при любой записи (create(), bulk_create(), save(),
update(), bulk_update(), update_or_create()), одинаково на всех диалектах. Так же ведёт себя datetime,
который выходит за пределы диапазона после перевода в настроенный пояс или в UTC. Операторы фильтра вроде
__year=2024 сравнивают извлечённое число с int (текст, float или Decimal дают UnSupportedError, как
любой оператор фильтра, получивший аргумент другого вида). TimeField работает и в SQLite и ClickHouse:
значение хранится текстом ISO (12:30:45, у времени с поясом — со своим смещением, 12:30:45+03:00), а
сравнения, сортировка и Min/Max сравнивают этот текст, как это делает база: там 10:00+03:00 идёт после
08:00+00:00, хотя это более ранний момент, который TIMETZ PostgreSQL упорядочивает по времени в UTC. Чтобы
упорядочивать моменты, храните их в DatetimeField.
При use_timezone=False дата-время раньше 1970 года переводится в местное время системы и обратно даже в
Windows, где datetime.astimezone() этого не умеет (там используется пояс, который называет необязательный
пакет tzlocal, а без него — смещение системы на 1970-01-02). В PostgreSQL значение без пояса при
use_timezone=False передаётся как момент, который соответствует этому местному времени на часах, поэтому
asyncpg и rust_pg записывают и читают одно и то же. DatetimeField, описывающий уже существующую колонку
PostgreSQL типа timestamp (без пояса), читает её как время на часах в UTC и записывает время значения на
часах в UTC на обоих драйверах — так же, как сам PostgreSQL переводит timestamp в timestamptz в сеансе
hare, где действует пояс UTC.
Крайние даты (PostgreSQL)¶
datetime.min/datetime.max и date(1, 1, 1)/date(9999, 12, 31) хранятся на любой базе как те дни и
моменты, которыми они являются, — и в PostgreSQL (через asyncpg и через rust_pg), а не как
-infinity/infinity, — поэтому __year/__month/__day, Extract/Trunc и
F("dt") ± timedelta(...) с ними работают. infinity/-infinity в PostgreSQL, записанные иначе,
читаются как те же крайние значения Python: date.max/date.min, datetime.max/datetime.min. В поясе
восточнее UTC datetime.max (UTC) при показе в нём выходит за диапазон datetime и отклоняется с
ValidationError.
При use_timezone=False naive datetime.min/datetime.max — этот момент в UTC, как и aware, и читается
обратно как naive крайнее значение, а не как местное время, которое восточнее или западнее UTC вышло бы
за диапазон.
Значения JSON¶
JSONField даёт ValidationError (а не просто ValueError/TypeError), если значение не удаётся
преобразовать в JSON, если из колонки прочитан испорченный JSON или если значение не соответствует
объявленному field_type. Объявленный field_type (модель Pydantic, list[Model], list[str] и т. п.)
применяется через TypeAdapter из Pydantic при присваивании, при save() и при чтении: словарь (или
список словарей) сразу становится моделью, поэтому в памяти лежит то же значение, что вернёт последующее
чтение, а элементы списка проверяются по одному. pydantic_model_creator() добавляет этот тип в схему.
Без field_type (в том числе с обобщённым JSONField[T], который действует только для проверки типов)
значения остаются обычным JSON.
Миграция пишет field_type кодом, который его импортирует: класс — по имени, под которым его отдаёт
модуль, параметризованный тип — по исходному типу и аргументам (dict[str, list[int]], int | None,
typing.Literal["a"], typing.List[int]). Тип, до которого не доходит ни одна инструкция import, —
класс, который Pydantic создаёт для GenericModel[int], псевдоним из инструкции type, —
makemigrations отклоняет с ConfigurationError; объявите подкласс
(class IntItems(GenericModel[int]): pass) и передайте его.
Значение, которое JSON не может хранить, даёт ValidationError при любой записи (create(), save(),
update(), bulk_create(), в том числе с use_copy=True, bulk_update()): число NaN или Infinity где
угодно внутри значения. Нулевой байт внутри значения JSON хранится так, как его хранит база, там, где она его
вмещает; PostgreSQL его не принимает, и hare даёт ValidationError. Целое число любого размера сохраняется и
читается точно — число вне 64-битного диапазона, с которым работает orjson, кодирует и раскодирует стандартная
библиотека. datetime, date, time без пояса и UUID внутри значения записываются одинаковым текстом
ISO/UUID по обоим путям, в том числе когда orjson не установлен вовсе.
exact/not по всему JSONField сравнивают так, как сравнивает база: PostgreSQL сравнивает значения
jsonb (порядок ключей и разница между 1 и 1.0 не важны), SQLite сравнивает хранимый текст (они
важны), колонку ClickHouse JSON сравнивает сам ClickHouse после приведения значения к JSON (порядок
ключей не важен), а колонка String с текстом JSON сравнивается как текст.
В SQLite колонка JSONField объявляется как JSON_TEXT (текстовое сродство), поэтому число на верхнем
уровне хранится своим текстом JSON: 12345678901234567890 и 1.0 читаются без изменений.
JSONB в PostgreSQL хранит число как numeric и пишет его без экспоненты, поэтому вещественное число в
экспоненциальной записи без дробной части читается обратно как int того же значения: 1e22 — это
10000000000000000000000, 1e300 — int из 301 цифры. 1.5e-7 и 3.0 читаются как вещественные.
GeneratedField¶
class GeneratedField:
def __init__(
self,
expression: Expression | RawSQLTerm | dict[str, RawSQLTerm],
output_field: Field[Any],
stored: bool = True,
**kwargs: Any,
) -> None: ...
Колонка, которую база вычисляет по выражению над строкой, — GENERATED ALWAYS AS (...) STORED или
VIRTUAL, — поэтому её значение верно после любой записи, которую видит база, в том числе сделанной не
через hare:
class Book(Model):
price = fields.DecimalField(max_digits=10, decimal_places=2)
quantity = fields.IntField()
stock_value = fields.GeneratedField(
F("price") * F("quantity"), output_field=fields.DecimalField(max_digits=12, decimal_places=2)
)
expression— выражение hare над полями модели (F, функции, арифметика; собственная колонка связи —F("owner_id"), а выражение, читающее связанную модель, даётConfigurationErrorпри привязке модели): DDL каждой базы пишет его своим SQL, миграция, переименовывающая поле, которое оно читает, переименовывает его и здесь, а миграция, меняющая тип колонки, которую оно читает, удаляет вычисляемую колонку до изменения и добавляет после. ИлиRawSQLTermс SQL, который записывается в команды DDL как есть (никогда не собирайте его из данных, пришедших извне), или своё выражение для каждого диалекта:{"sqlite": RawSQLTerm(...), "postgresql": RawSQLTerm(...)}; диалект, которого нет в словаре, даётConfigurationError, как и обычный текст. Готовый SQL — ваш: hare не переименовывает в нём поле и не читает, какие колонки он использует, — миграцию, меняющую такую колонку, отклоняет база там, где она не меняет колонку под вычисляемой.output_field— поле, задающее тип колонки и то, как её значение читается, сравнивается, фильтруется и описывается в схеме. Его JSON-форма и правила сравнения тоже сохраняются, в том числе сортировка JSON-значений и запрет сортировки колонокDynamicFieldиVariantFieldв ClickHouse. Сортировка аннотацииF()этой колонки следует тем же правилам, что и сортировка самой колонки.stored=Trueвычисляет значение при записи;stored=False— колонкаVIRTUAL, вычисляемая при чтении, — работает в SQLite и PostgreSQL 18, а более старый сервер PostgreSQL даётUnSupportedError(Features.supports_virtual_generated_columns).
hare никогда не пишет в эту колонку: INSERT читает вычисленное значение обратно, UPDATE читает его заново
(на базе с RETURNING). Значение для этого поля даёт QueryError — в конструкторе, create(),
get_or_create(), update_or_create(defaults=...), save(update_fields=[...]) и update(); копию
экземпляра делает obj.clone(). У нового экземпляра — и у копии — значения этого поля нет, пока его строка не
записана: чтение даёт NoValuesFetched, как и чтение после записи, которая ничего не читает обратно
(bulk_create() без returning=True, база без RETURNING), пока его не прочитает refresh_from_db().
Значение, присвоенное загруженному экземпляру, при save() заменяется
вычисленным; bulk_update() перечитывает новое значение только с returning=True. Оно не может быть
первичным ключом и не принимает validators= (ConfigurationError — задайте их у output_field);
sensitive оно наследует от output_field, если не передано своё значение.
Поля адреса почты, URL, слага и телефона¶
class Author(Model):
email = fields.EmailField(unique=True)
homepage = fields.URLField(null=True)
slug = fields.SlugField(unique=True)
phone = fields.PhoneField(null=True, region="US")
class EmailField:
def __init__(self, max_length: int = 254, *, lowercase: bool = False, **kwargs: Any) -> None: ...
class URLField:
def __init__(
self,
max_length: int = 2048,
*,
schemes: Sequence[str] = ("http", "https"),
**kwargs: Any,
) -> None: ...
class SlugField:
def __init__(self, max_length: int = 50, *, allow_unicode: bool = False, **kwargs: Any) -> None: ...
class PhoneField:
def __init__(self, *, region: str | None = None, **kwargs: Any) -> None: ...
Каждое из них — CharField (VARCHAR(max_length), MaxLengthValidator), которое проверяет формат
значения и приводит его к одному виду записи на каждом пути записи — create(), save(), update(),
bulk_create() (и с use_copy=True), bulk_update(), update_or_create() — и в каждом значении
фильтра, которое сравнивается с ним целиком (email=, email__in=, email__not=). Значение, не
прошедшее проверку, даёт ValidationError с именем поля: email: Invalid email address. Текстовые
операторы (__contains, __startswith, __icontains, ...) принимают любой текст. Валидаторы из
validators= тоже выполняются, до проверки формата. Объект хранит значение таким, каким его присвоили,
пока оно не прочитано из базы заново; в базе лежит записанный вид.
| Поле | Принимает | Записывает |
|---|---|---|
EmailField |
то, что принимает EmailValidator: локальную часть из слов через точку (ann.lee+news) или строку в кавычках ("ann@home"), @, затем имя хоста с точкой (example.com) или адрес в квадратных скобках ([192.0.2.1]); не длиннее 320 символов |
домен в нижнем регистре — Ann@Example.COM становится Ann@example.com; с lowercase=True весь адрес, ann@example.com |
URLField |
то, что принимает URLValidator, со схемой из schemes (сравнивается в нижнем регистре, HTTPS:// — это https): имя хоста с точкой, localhost, адрес IPv4 или IPv6, необязательный порт, данные пользователя, путь, запрос и фрагмент |
как дано |
SlugField |
латинские буквы, цифры, дефис и подчёркивание (my-first_post); с allow_unicode=True — ещё любые буквы и цифры Юникода (привет-мир) |
как дано |
PhoneField |
без пакета phonenumbers — только номер в виде E.164: +, код страны не с 0, всего не больше 15 цифр; с ним (pip install hare-orm[phone]) — номер в любом виде, который пакет разбирает и считает верным: +1 (650) 253-0000, а с region — и номер внутри страны, (650) 253-0000 |
E.164: +16502530000 (VARCHAR(16)) |
Поэтому значение фильтра находится в каком бы виде его ни передали:
Author.objects.filter(email="Ann@EXAMPLE.com") находит Ann@example.com,
filter(phone="(650) 253-0000") находит +16502530000 у поля с region="US". Локальная часть адреса
сохраняет регистр — почтовый сервер может отличать Ann от ann; если ваши адреса от регистра не
зависят, задайте lowercase=True или сравнивайте через email__iexact=.
schemes— имена схем в нижнем регистре.SlugFieldиндексируется, если не переданdb_index=False. Файл миграции пишет для негоdb_index=True, а поле, объявленное в файле миграции безdb_index, индекса не имеет — то же правило, что уForeignKeyField.region— код региона, который знаетphonenumbers("US","GB", егоSUPPORTED_REGIONS);regionбез установленногоphonenumbersдаётConfigurationError. Какие номераphonenumbersсчитает верными, задают его собственные данные, поэтому это может меняться с его версией.
Схема Pydantic (pydantic_model_creator()) и построенный по ней OpenAPI описывают их через
format: email, format: uri, pattern символов слага, а PhoneField без phonenumbers — через
pattern номера E.164 (с ним номер принимается в любом виде).
Превращение CharField в одно из этих полей — AlterField, который оставляет колонку (SlugField
добавляет свой индекс): уже записанные строки не проверяются и не приводятся — это случится при их
следующей записи.
Поля-перечисления¶
Это не классы, а функции, которые возвращают IntEnumFieldInstance (это SmallIntField) или
CharEnumFieldInstance (это CharField), поэтому type(Model.status) is not IntEnumField:
def IntEnumField(
enum_type: type[IntEnumType],
description: str | None = None,
**kwargs: Any,
) -> IntEnumType: ...
def CharEnumField(
enum_type: type[CharEnumType],
description: str | None = None,
max_length: int = 0,
**kwargs: Any,
) -> CharEnumType: ...
IntEnumFieldпостроено наSmallIntField— значения перечисления должны помещаться вint16(1..32767 приgenerated=True, иначе весь диапазонint16).CharEnumFieldпостроено наCharField— приmax_length=0длина берётся по самому длинному значению перечисления. Если позже вы добавите более длинное значение, увеличьтеmax_lengthсами — оно не пересчитывается. Перечисление с нестроковыми значениями (например,ONE = 1) тоже работает: в колонку пишетсяstr(member.value)("1"), а при чтении текст превращается обратно в элемент перечисления.- Оба сами составляют
descriptionв виде"NAME: value"со всеми элементами, если вы не передали своё. - Оба отклоняют объявление, которое не может хранить своё перечисление (
ConfigurationError): класс, который не является перечислением, а уCharEnumField—max_lengthкороче самого длинного значения иdefault, который не элемент и не значение перечисления. - Число, которому не соответствует ни один элемент, —
7или дробное вроде1.5, — даётValidationErrorи при записи, и в фильтре; так же и целое вещественное (1.0) дляIntEnumField, которое, как целое поле, принимает толькоint. - Там, где колонка хранит любое целое или текст (PostgreSQL, SQLite),
CHECKограничивает её значениями, которыми хранятся элементы: значение без элемента, записанное в обход поля, — готовым SQL или вычисленноеupdate(level=F("level") + 1), — даётIntegrityError. ЭтоCheckConstraintсEnumMembersConditionпо имени<поле>_enum_members, который добавляется к ограничениям модели при её привязке:makemigrationsпишет его вместе с моделью и заменяет, когда меняются элементы. Колонке перечислимого типа он не нужен —Enum8/Enum16ClickHouse (Features.keeps_enum_columns),NativeEnumFieldPostgreSQL.
from enum import Enum
class Status(Enum):
DRAFT = "draft"
PUBLISHED = "published"
class Book(Model):
status = fields.CharEnumField(Status, default=Status.DRAFT)
CompositePrimaryKey¶
class CompositePrimaryKey:
def __init__(self, *field_names: str, without_overlaps: bool = False) -> None: ...
Это не настоящее поле — своей колонки у него нет. Оно объявляет первичным ключом таблицы два или больше уже объявленных полей:
class Shelf(Model):
store = fields.CharField(max_length=20)
number = fields.IntField()
pk = CompositePrimaryKey("store", "number")
Каждое названное поле должно существовать и само не должно быть primary_key=True, generated=True
или null=True, а других полей с primary_key=True у модели нет (ConfigurationError). Связь модели с
самой собой тоже не может быть частью ключа: её ключ содержал бы саму связь. Не может им быть и
значение-контейнер — ArrayField, MapField, TupleField, NestedField или JSONField, ни в
составном ключе, ни ключом целиком: ORM держит прочитанные и записанные строки по их ключу
(ConfigurationError); такие поля делайте уникальными через UniqueConstraint. Model.pk у такой
модели — кортеж. clone() вычисляет default=/db_default= каждой части ключа отдельно, поэтому в
pk=(...) явно указываются только части, у которых нет ни того, ни другого.
Частью ключа может быть внешний ключ — по своему имени или по имени колонки ключа:
CompositePrimaryKey("book_id", "number") и CompositePrimaryKey("book", "number") — один и тот же
ключ. Его место занимает колонка ключа связи (связь с моделью с составным ключом добавляет каждую его
колонку — edition_book_id, edition_number); Model.pk хранит значение (значения) ключа связанной
модели, их же принимают pk= и pk__in=. Связь не может быть null=True, а ManyToManyField не может
быть частью ключа (ConfigurationError):
class Edition(Model):
book = fields.ForeignKeyField("models.Book", related_name="editions")
number = fields.IntField()
pk = CompositePrimaryKey("book_id", "number")
await Edition.objects.get(pk=(book.id, 2))
without_overlaps=True делает последнее поле диапазоном (или мультидиапазоном), в котором две строки с
равными остальными полями не могут пересекаться, — одна бронь читального зала в каждый момент; это
проверяет база (PRIMARY KEY (room, booked WITHOUT OVERLAPS), PostgreSQL 18):
class RoomBooking(Model):
room = fields.CharField(max_length=20)
booked = DateRangeField() # hare.dialects.postgresql.fields
reader = fields.ForeignKeyField("models.Reader")
pk = CompositePrimaryKey("room", "booked", without_overlaps=True)
Более старый сервер PostgreSQL и SQLite дают UnSupportedError до любых команд DDL
(Features.supports_without_overlaps); скалярному полю перед диапазоном нужно расширение btree_gist,
его добавляют редактор схемы и автодетектор миграций. Внешний ключ не может ссылаться на такой ключ
(ConfigurationError), а смену without_overlaps у существующей таблицы автодетектор не находит — для
неё нужна миграция, написанная вручную. Цель ON CONFLICT по такому ключу PostgreSQL не выводит и
конфликтующую с ним строку не обновляет, поэтому bulk_create(on_conflict=[...]) с его полями и
update_fields с on_conflict_constraint="pk" дают QueryError; конфликтующие строки пропускает
ignore_conflicts=True — без цели или с on_conflict_constraint="pk". С
UniqueConstraint(without_overlaps=True) так же, по его name.
Примечание
Внешний ключ может ссылаться на модель с составным первичным ключом — см.
Связи — с настоящим ограничением FOREIGN KEY на
уровне таблицы, каскадами, соединением таблиц и prefetch_related().
Поля отдельных баз данных¶
HStoreField, TSVectorField, PostGISField, CitextField и поля диапазонов находятся в
hare.dialects.postgresql — см. Поля PostgreSQL. Из hare.fields
ArrayField работает в PostgreSQL и ClickHouse, MapField, TupleField и NestedField — в ClickHouse,
а IPAddressField — везде (inet в PostgreSQL, IPv6 в ClickHouse, текст в остальных). См. Типы
ClickHouse. Адреса сравниваются, сортируются и агрегируются
(Min/Max) так, как каждая база упорядочивает свою колонку: inet PostgreSQL ставит адрес IPv4 раньше
любого IPv6, затем упорядочивает по байтам адреса; SQLite сравнивает текст; ClickHouse упорядочивает свои
значения IPv6 по-своему. Текстовые операторы (__startswith) сопоставляют адрес IPv4 по его
тексту IPv4, и в форме ::ffff: ClickHouse тоже. Адрес IPv6 с зоной (fe80::1%eth0) даёт
ValidationError в PostgreSQL и ClickHouse, колонки адресов которых зону не хранят; SQLite сохраняет её
в тексте. VectorField находится в hare.vectors и работает в PostgreSQL и SQLite — см. Поиск по
векторам.
Свой класс поля¶
См. Свои поля.