Django ORM

Специфичные для PostgreSQL возможности | Курс Django ORM урок 7.2

Специфичные для PostgreSQL возможности | Курс Django ORM урок 7.2
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 2

Цель урока

Разобрать специфичные для PostgreSQL поля и операции Django ORM: JSONField, ArrayField, полнотекстовый поиск. Понять когда эти инструменты дают преимущество перед стандартными подходами.

Необходимые знания


JSONField

JSONField хранит данные в PostgreSQL типе jsonb. Это не просто TEXT, PostgreSQL понимает структуру JSON и может индексировать и запрашивать по вложенным полям.

from django.db.models import JSONField

class Product(models.Model):
    name = models.CharField(max_length=200)
    attributes = JSONField(default=dict, blank=True)
    # Хранит: {"color": "black", "memory": "256GB", "warranty_years": 2}


CREATE TABLE shop_product (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(200),
    attributes JSONB DEFAULT '{}'
);

jsonb (binary JSON), PostgreSQL хранит данные в бинарном формате: более быстрые запросы, дедупликация ключей, поддержка GIN индексов.

Сохранение и чтение

# Сохранить
product = Product.objects.create(
    name="iPhone 15",
    attributes={
        "color": "black",
        "memory": "256GB",
        "warranty_years": 2,
        "features": ["5G", "USB-C", "Face ID"],
    }
)

# Прочитать
product = Product.objects.get(id=1)
print(product.attributes["color"])  # "black"
print(product.attributes["features"])  # ["5G", "USB-C", "Face ID"]

Django автоматически сериализует Python dict/list в JSON при сохранении и десериализует при чтении.

Фильтрация по JSON полям

# Точное совпадение значения по ключу
Product.objects.filter(attributes__color="black")
SELECT * FROM shop_product
WHERE attributes ->> 'color' = 'black';


# Числовое сравнение
Product.objects.filter(attributes__warranty_years__gte=2)
WHERE (attributes ->> 'warranty_years')::numeric >= 2


# Вложенные ключи через __
Product.objects.filter(attributes__specs__ram="16GB")
# attributes = {"specs": {"ram": "16GB"}}
WHERE attributes -> 'specs' ->> 'ram' = '16GB'


# Содержит ключ
from django.db.models import Q
Product.objects.filter(attributes__has_key="warranty_years")
WHERE attributes ? 'warranty_years'


# Содержит все ключи
Product.objects.filter(attributes__has_keys=["color", "memory"])
WHERE attributes ?& ARRAY['color', 'memory']


# Содержит хотя бы один ключ
Product.objects.filter(attributes__has_any_keys=["color", "size"])
WHERE attributes ?| ARRAY['color', 'size']


# Содержит подмножество (attributes содержит все пары из переданного dict)
Product.objects.filter(attributes__contains={"color": "black"})
WHERE attributes @> '{"color": "black"}'::jsonb

Обновление JSON поля

# Перезаписать весь объект
product.attributes = {"color": "white", "memory": "128GB"}
product.save(update_fields=["attributes"])


# Обновить один ключ без загрузки объекта
from django.db.models.functions import JSONObject
from django.db.models import Value

# Django не имеет встроенного оператора для обновления одного ключа
# Нужен raw SQL или F() с jsonb_set
from django.db import connection

with connection.cursor() as cursor:
    cursor.execute(
        "UPDATE shop_product SET attributes = jsonb_set(attributes, %s, %s::jsonb) WHERE id = %s",
        ["{color}", '"white"', product.id]
    )

GIN индекс для JSONField

from django.contrib.postgres.indexes import GinIndex

class Product(models.Model):
    attributes = JSONField(default=dict)

    class Meta:
        indexes = [
            GinIndex(fields=["attributes"], name="product_attributes_gin"),
        ]


CREATE INDEX product_attributes_gin ON shop_product USING gin(attributes);

GIN индекс ускоряет операции contains, has_key, has_keys. Для = по конкретному ключу обычный B-tree на выражении эффективнее. Для индекса на конкретный JSON ключ используйте функциональный индекс через Index(expressions=...):

from django.db.models.expressions import RawSQL

class Meta:
    indexes = [
        models.Index(
            RawSQL("(attributes->>'color')", []),
            name="product_attributes_color_idx",
        ),
    ]


CREATE INDEX product_attributes_color_idx ON shop_product ((attributes->>'color'));

ArrayField

ArrayField хранит массив значений в PostgreSQL типе ARRAY:

from django.contrib.postgres.fields import ArrayField

class Product(models.Model):
    tags = ArrayField(
        base_field=models.CharField(max_length=50),
        default=list,
        blank=True,
    )
    # Хранит: ["wireless", "bluetooth", "premium"]
CREATE TABLE shop_product (
    ...
    tags VARCHAR(50)[] DEFAULT '{}'
);

Фильтрация по ArrayField

# Содержит элемент
Product.objects.filter(tags__contains=["wireless"])
WHERE tags @> ARRAY['wireless']


# Содержится в (product.tags является подмножеством переданного массива)
Product.objects.filter(tags__contained_by=["wireless", "bluetooth", "premium"])
WHERE tags <@ ARRAY['wireless', 'bluetooth', 'premium']


# Пересечение непустое (хотя бы один элемент совпадает)
Product.objects.filter(tags__overlap=["wireless", "usb-c"])
WHERE tags && ARRAY['wireless', 'usb-c']


# Длина массива
Product.objects.filter(tags__len__gte=3)
WHERE array_length(tags, 1) >= 3


# Конкретный элемент по индексу (1-based в PostgreSQL)
Product.objects.filter(tags__0="wireless")  # первый элемент
WHERE tags[1] = 'wireless'

GIN индекс для ArrayField

from django.contrib.postgres.indexes import GinIndex

class Meta:
    indexes = [
        GinIndex(fields=["tags"], name="product_tags_gin"),
    ]


CREATE INDEX product_tags_gin ON shop_product USING gin(tags);

Полнотекстовый поиск

Django предоставляет интерфейс к PostgreSQL full-text search через django.contrib.postgres.search.

SearchVector и SearchQuery

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank

# Простой поиск по одному полю
Product.objects.annotate(
    search=SearchVector("name", "description"),
).filter(search=SearchQuery("wireless headphones"))


SELECT shop_product.*,
       to_tsvector('english', name || ' ' || description) AS search
FROM shop_product
WHERE to_tsvector('english', name || ' ' || description) @@ plainto_tsquery('english', 'wireless headphones')
ORDER BY shop_product.name;

SearchVector, преобразует текст в tsvector (список лексем). SearchQuery, запрос для поиска.

Русский язык

# Указать язык
Product.objects.annotate(
    search=SearchVector("name", "description", config="russian"),
).filter(search=SearchQuery("беспроводные наушники", config="russian"))
WHERE to_tsvector('russian', ...) @@ plainto_tsquery('russian', 'беспроводные наушники')

PostgreSQL применяет стемминг: "наушников" и "наушники", это одна лексема.

SearchRank - ранжирование результатов

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank

query = SearchQuery("wireless headphones")
vector = SearchVector("name", weight="A") + SearchVector("description", weight="B")

products = (
    Product.objects
    .annotate(rank=SearchRank(vector, query))
    .filter(rank__gte=0.1)
    .order_by("-rank")
)


SELECT shop_product.*,
       ts_rank(
           setweight(to_tsvector('english', name), 'A') ||
           setweight(to_tsvector('english', description), 'B'),
           plainto_tsquery('english', 'wireless headphones')
       ) AS rank
FROM shop_product
WHERE ts_rank(...) >= 0.1
ORDER BY rank DESC;

weight="A" дает больший вес совпадениям в name, чем в description (weight="B").

SearchVectorField - индексированный поиск

Вычислять SearchVector при каждом запросе дорого. Для production нужно хранить предвычисленный tsvector в отдельном поле:

from django.contrib.postgres.search import SearchVectorField

class Product(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()
    search_vector = SearchVectorField(null=True, editable=False)

    class Meta:
        indexes = [
            GinIndex(fields=["search_vector"], name="product_search_vector_gin"),
        ]

Обновлять search_vector при изменении name или description, через сигнал или через UPDATE:

from django.contrib.postgres.search import SearchVector

# Обновить search_vector для всех продуктов
Product.objects.update(
    search_vector=SearchVector("name", "description", config="russian")
)


UPDATE shop_product
SET search_vector = to_tsvector('russian', name || ' ' || description);

Поиск по индексу:

from django.contrib.postgres.search import SearchQuery

Product.objects.filter(
    search_vector=SearchQuery("наушники", config="russian")
)


SELECT * FROM shop_product
WHERE search_vector @@ plainto_tsquery('russian', 'наушники')
ORDER BY name;
-- GIN индекс используется автоматически

TrigramSimilarity - поиск по схожести

TrigramSimilarity ищет строки похожие на запрос, полезно для "Вы имели в виду?" или поиска с опечатками:

from django.contrib.postgres.search import TrigramSimilarity

Product.objects.annotate(
    similarity=TrigramSimilarity("name", "iPhone")
).filter(similarity__gte=0.3).order_by("-similarity")


SELECT *, similarity(name, 'iPhone') AS similarity
FROM shop_product
WHERE similarity(name, 'iPhone') >= 0.3
ORDER BY similarity DESC;

Требует расширения PostgreSQL pg_trgm:

CREATE EXTENSION IF NOT EXISTS pg_trgm;

Или через миграцию:

from django.contrib.postgres.operations import TrigramExtension

class Migration(migrations.Migration):
    operations = [
        TrigramExtension(),
        # ...
    ]

Типы SearchQuery - search_type

SearchQuery поддерживает несколько режимов поиска через параметр search_type:

from django.contrib.postgres.search import SearchQuery

# plain (по умолчанию) - стемминг, слова объединяются AND
SearchQuery("wireless headphones")

# phrase - точная фраза в том же порядке
SearchQuery("wireless headphones", search_type="phrase")

# raw - сырой tsquery синтаксис PostgreSQL
SearchQuery("wireless & headphones", search_type="raw")

# websearch - Google подобный синтаксис (Django 3.2+)
SearchQuery('"wireless headphones" OR earbuds', search_type="websearch")


-- plain
plainto_tsquery('english', 'wireless headphones')

-- phrase
phraseto_tsquery('english', 'wireless headphones')

-- raw
to_tsquery('english', 'wireless & headphones')

-- websearch
websearch_to_tsquery('english', '"wireless headphones" OR earbuds')

search_type="plain" применяет стемминг: "headphones" найдет "headphone". search_type="raw" дает полный контроль над tsquery синтаксисом PostgreSQL.


Практическое задание

  1. Добавьте поле attributes = JSONField() в модель Product. Наполните данными: для телефонов - {"color": str, "memory": str}, для ноутбуков - {"ram": str, "cpu": str}. Напишите запрос: все черные телефоны с памятью 256GB.

  2. Добавьте поле tags = ArrayField(CharField) в Product. Найдите все продукты с тегами "wireless" и "bluetooth" одновременно (оба тега должны присутствовать).

  3. Реализуйте функцию search_products(query, lang="russian") через SearchVector и SearchRank. Результаты должны быть отсортированы по релевантности. Имя продукта должно иметь больший вес чем описание.

  4. Добавьте поле search_vector = SearchVectorField() и GIN индекс. Напишите сигнал post_save который обновляет search_vector при изменении name или description.

  5. Сравните скорость поиска по name ILIKE '%phone%' vs SearchVector на таблице с 10 000 продуктов. Используйте EXPLAIN ANALYZE.


Возможные ошибки

JSONField без GIN индекса при частых contains запросах

# Без индекса будет Seq Scan на каждый запрос
Product.objects.filter(attributes__contains={"color": "black"})

Добавьте GinIndex если такие запросы частые.

ArrayField для M2M отношений

# Плохо, денормализованные данные, нет целостности
class Product(models.Model):
    tag_ids = ArrayField(IntegerField())  # [1, 2, 3]

# Правильно M2M связь
class Product(models.Model):
    tags = ManyToManyField(Tag)

ArrayField подходит для данных которые не имеют собственных атрибутов (простые строки, числа). Для связей с другими моделями, M2M.

Полнотекстовый поиск без конфига языка

# Без config, PostgreSQL использует язык по умолчанию (обычно english)
# Для русского текста, стемминг не работает
SearchVector("description")

# Для русского
SearchVector("description", config="russian")

TrigramSimilarity без расширения

# ProgrammingError: function similarity(text, unknown) does not exist
Product.objects.annotate(sim=TrigramSimilarity("name", "iPhone"))

# Нужно установить расширение

Связь со следующим уроком

В уроке 7.3 разберем кастомные lookup'ы и функции. Как добавить собственные lookup типы __unaccent, __similarity и как обернуть произвольные SQL функции через Func() для использования в ORM.


<< Урок 7.1

Урок 7.3 >>


Подписывайтесь на мой Telegram канал

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

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы