Настройка Django ORM для Python веб-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Настройка Django ORM для Python веб-приложения
Средняя
~1 рабочий день
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Настройка Django ORM для Python веб-приложения

Django ORM встроен в фреймворк и не требует отдельной установки, но его правильная конфигурация существенно влияет на производительность и масштабируемость. Рассматриваем Django 4.2+ с PostgreSQL.

Подключение к базе данных

В settings.py определяем несколько баз при необходимости:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': env('DB_NAME'),
        'USER': env('DB_USER'),
        'PASSWORD': env('DB_PASSWORD'),
        'HOST': env('DB_HOST', default='127.0.0.1'),
        'PORT': env('DB_PORT', default='5432'),
        'CONN_MAX_AGE': 60,           # persistent connections
        'OPTIONS': {
            'connect_timeout': 10,
            'options': '-c search_path=public',
        },
        'TEST': {
            'NAME': 'test_myapp',
        },
    },
    'replica': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': env('DB_REPLICA_NAME'),
        'USER': env('DB_REPLICA_USER'),
        'PASSWORD': env('DB_REPLICA_PASSWORD'),
        'HOST': env('DB_REPLICA_HOST'),
        'PORT': '5432',
        'CONN_MAX_AGE': 60,
        'TEST': {
            'MIRROR': 'default',
        },
    },
}

CONN_MAX_AGE включает persistent connections — Django не будет закрывать соединение с БД после каждого HTTP-запроса, что заметно снижает latency. Для gunicorn с несколькими воркерами это стандартная практика.

Модели

# catalog/models.py
from django.db import models
from django.utils.text import slugify


class Category(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, max_length=220)
    parent = models.ForeignKey(
        'self',
        null=True, blank=True,
        on_delete=models.SET_NULL,
        related_name='children',
    )

    class Meta:
        verbose_name_plural = 'categories'
        ordering = ['name']

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)


class Product(models.Model):
    class Status(models.TextChoices):
        DRAFT = 'draft', 'Draft'
        PUBLISHED = 'published', 'Published'
        ARCHIVED = 'archived', 'Archived'

    title = models.CharField(max_length=500)
    slug = models.SlugField(unique=True, max_length=520)
    category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,
        related_name='products',
    )
    price = models.DecimalField(max_digits=12, decimal_places=2)
    status = models.CharField(
        max_length=10,
        choices=Status.choices,
        default=Status.DRAFT,
    )
    tags = models.ManyToManyField('Tag', blank=True, related_name='products')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        indexes = [
            models.Index(fields=['status', '-created_at']),
            models.Index(fields=['category', 'status']),
        ]

Менеджеры и QuerySet

Кастомный менеджер — главный инструмент для инкапсуляции логики выборок:

class PublishedProductQuerySet(models.QuerySet):
    def published(self):
        return self.filter(status=Product.Status.PUBLISHED)

    def with_category(self):
        return self.select_related('category')

    def with_tags(self):
        return self.prefetch_related('tags')

    def in_price_range(self, min_price, max_price):
        return self.filter(price__gte=min_price, price__lte=max_price)


class ProductManager(models.Manager):
    def get_queryset(self):
        return PublishedProductQuerySet(self.model, using=self._db)

    def published(self):
        return self.get_queryset().published()


# В модели:
# objects = ProductManager()

Использование:

products = (
    Product.objects.published()
    .with_category()
    .with_tags()
    .in_price_range(100, 5000)
    .order_by('-created_at')[:20]
)

Один вызов .with_tags() добавит prefetch_related и уберёт N+1 проблему при рендеринге тегов.

Аннотации и агрегации

from django.db.models import Count, Avg, F, Q, ExpressionWrapper, DecimalField

# Количество продуктов и средняя цена по категориям
stats = (
    Category.objects
    .annotate(
        product_count=Count('products', filter=Q(products__status='published')),
        avg_price=Avg('products__price', filter=Q(products__status='published')),
    )
    .filter(product_count__gt=0)
    .order_by('-product_count')
)

# Условное обновление через F-выражения (без Python round-trip)
Product.objects.filter(status='published').update(
    price=ExpressionWrapper(F('price') * 1.1, output_field=DecimalField())
)

Маршрутизация запросов на реплику

# myapp/db_router.py
class ReadReplicaRouter:
    READ_DB = 'replica'
    WRITE_DB = 'default'

    READ_MODELS = frozenset()  # ограничить, если нужно

    def db_for_read(self, model, **hints):
        return self.READ_DB

    def db_for_write(self, model, **hints):
        return self.WRITE_DB

    def allow_relation(self, obj1, obj2, **hints):
        return True

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        return db == self.WRITE_DB
# settings.py
DATABASE_ROUTERS = ['myapp.db_router.ReadReplicaRouter']

Миграции в продакшн

Несколько правил, которые сберегут от простоя:

  • Добавление nullable-колонки не блокирует таблицу в PostgreSQL 11+.
  • Добавление индекса — только CONCURRENTLY: migrations.AddIndex использует его автоматически для PostgreSQL.
  • Переименование колонки — всегда в два этапа: добавить новую → скопировать данные → убрать старую.
  • --fake миграция нужна только для синхронизации состояния без повторного применения SQL.
python manage.py migrate --plan   # показать, что будет применено
python manage.py migrate          # применить
python manage.py showmigrations   # статус всех миграций

Сроки

Начальная настройка нового Django-проекта с нуля (модели, миграции, роутер на реплику, базовые менеджеры): 1 день. Рефакторинг существующего проекта под устранение N+1 запросов (аудит select_related/prefetch_related, добавление индексов): 1–2 дня.