Django ORM

Бесплатный курс Django ORM урок 0 | Настройка окружения

Бесплатный курс Django ORM урок 0 | Настройка окружения
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 46

Цель урока

Подготовить рабочее окружение для всего курса: поднять PostgreSQL и Django в Docker Compose, подключить Django Debug Toolbar, создать модели учебного e-commerce проекта и загрузить тестовые данные. После этого урока у вас будет готовая база, с которой мы будем работать во всех последующих уроках.

Что потребуется

  • Docker и Docker Compose (установлены на машине)
  • Python 3.12+
  • Базовое понимание того, что такое проект Django и миграции

Структура проекта

На протяжении всего курса мы работаем с одним проектом, упрощенным интернет-магазином. Модели спроектированы так, чтобы демонстрировать все типы связей и паттерны, которые встречаются в реальных приложениях.

Модели проекта:

  • Category, категории товаров с иерархией (parent → child)
  • Product, товары с ценой, описанием, остатком на складе
  • User, стандартный Django User
  • Order, заказы пользователей со статусом
  • OrderItem, позиции заказа (связь Order ↔ Product с количеством и ценой)
  • Review, отзывы пользователей на товары с рейтингом

Этот набор покрывает: OneToMany, ManyToMany, самореференцию, денормализованные поля и достаточно данных для демонстрации проблем производительности.


Шаг 1. Docker Compose

Создайте структуру проекта:

django-orm-course/
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── .env
└── src/
    └── 

docker-compose.yml:

services:
  db:
    image: postgres:17
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./src:/app
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      - DEBUG=True
    depends_on:
      - db
    working_dir: /app

volumes:
  postgres_data:

Dockerfile:

FROM python:3.12-slim

WORKDIR /app

RUN apt-get update && apt-get install -y \
    libpq-dev \
    gcc \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/ .

requirements.txt:

Django>=6.0.3
psycopg[binary]==3.3.3
django-debug-toolbar==6.2.0

.env:

POSTGRES_DB=ormcourse
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

.dockerignore:

.venv/
.git/
.gitignore
__pycache__/
*.pyc
*.pyo
.env
.env.*
*.sqlite3
.DS_Store

Без .dockerignore Docker копирует .venv в образ - это сотни мегабайт ненужных файлов. Также исключаем .env.


Шаг 2. Создание проекта Django

Виртуальное окружение и зависимости

Создайте виртуальное окружение и установите зависимости локально - это нужно для работы IDE и запуска команд вне Docker:

cd django-orm-course
python -m venv .venv
source .venv/bin/activate  
# Windows: .venv\Scripts\activate

Установите зависимости:

pip install -r requirements.txt

Создание проекта

Создайте проект локально перед сборкой образа:

django-admin startproject config src/
cd src
python manage.py startapp shop

Структура src/ после этого:

src/
├── manage.py
├── config/
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── shop/
    ├── models.py
    ├── admin.py
    └── ...

Шаг 3. Настройка settings.py

изменения вsrc/config/settings.py:

import os

# Приложения
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "debug_toolbar",  # Django Debug Toolbar
    "shop",           # наше приложение
]

# Middleware - Debug Toolbar должен быть первым
MIDDLEWARE = [
    "debug_toolbar.middleware.DebugToolbarMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]


# База данных
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ.get("POSTGRES_DB", "ormcourse"),
        "USER": os.environ.get("POSTGRES_USER", "postgres"),
        "PASSWORD": os.environ.get("POSTGRES_PASSWORD", "postgres"),
        "HOST": os.environ.get("DB_HOST", "db"),
        "PORT": "5432",
    }
}


# Debug Toolbar показывается только для этих IP
INTERNAL_IPS = ["127.0.0.1"]

Connection pooling (production)

По умолчанию Django открывает новое соединение с PostgreSQL на каждый HTTP запрос и закрывает его в конце. Для учебного окружения это нормально.

В production при нескольких воркерах gunicorn/uvicorn количество соединений к PostgreSQL растет пропорционально числу воркеров. Django 5.1 добавил встроенный пул соединений через psycopg[pool]:

При прохождении курса можно не применять.

# Только для production, требует psycopg[pool] в requirements.txt
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        # ...
        "OPTIONS": {
            "pool": {
                "min_size": 2,
                "max_size": 10,
                "timeout": 30,
            },
        },
    }
}

STATIC_URL = "/static/"

src/config/urls.py:

from django.contrib import admin
from django.urls import path, include
import debug_toolbar

urlpatterns = [
    path("admin/", admin.site.urls),
    path("__debug__/", include(debug_toolbar.urls)),
]

Шаг 4. Модели учебного проекта

src/shop/models.py:

from django.contrib.auth.models import User
from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    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 __str__(self):
        return self.name


class Product(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    description = models.TextField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.PositiveIntegerField(default=0)
    category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,
        related_name="products",
    )
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["name"]

    def __str__(self):
        return self.name


class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending", "Pending"
        PROCESSING = "processing", "Processing"
        SHIPPED = "shipped", "Shipped"
        DELIVERED = "delivered", "Delivered"
        CANCELLED = "cancelled", "Cancelled"

    user = models.ForeignKey(
        User,
        on_delete=models.PROTECT,
        related_name="orders",
    )
    status = models.CharField(
        max_length=20,
        choices=Status,
        default=Status.PENDING,
    )
    # Денормализованное поле - итоговая сумма хранится в БД,
    # не вычисляется каждый раз через агрегацию
    total_price = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return f"Order #{self.pk} ({self.user})"


class OrderItem(models.Model):
    order = models.ForeignKey(
        Order,
        on_delete=models.CASCADE,
        related_name="items",
    )
    product = models.ForeignKey(
        Product,
        on_delete=models.PROTECT,
        related_name="order_items",
    )
    quantity = models.PositiveIntegerField(default=1)
    # Цена фиксируется на момент заказа - не тянем актуальную цену из Product
    price = models.DecimalField(max_digits=10, decimal_places=2)

    def __str__(self):
        return f"{self.quantity}x {self.product.name}"


class Review(models.Model):
    product = models.ForeignKey(
        Product,
        on_delete=models.CASCADE,
        related_name="reviews",
    )
    user = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name="reviews",
    )
    rating = models.PositiveSmallIntegerField()  # 1-5
    text = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        # Один пользователь - один отзыв на товар
        unique_together = [("product", "user")]
        ordering = ["-created_at"]

    def __str__(self):
        return f"Review by {self.user} on {self.product}"

Шаг 5. Миграции и запуск

# Поднять контейнеры
docker compose up -d

# Применить миграции
docker compose exec web python manage.py makemigrations
docker compose exec web python manage.py migrate

# Создать суперпользователя
docker compose exec web python manage.py createsuperuser

Проверьте, что PostgreSQL создал таблицы. Подключитесь через psql:

docker compose exec db psql -U postgres -d ormcourse

# Внутри psql - список таблиц
\dt

# Схема конкретной таблицы
\d shop_product

Вывод \dt должен показать таблицы:

 Schema |         Name          | Type  |  Owner
--------+-----------------------+-------+----------
 public | auth_group            | table | postgres
 public | auth_user             | table | postgres
 public | shop_category         | table | postgres
 public | shop_order            | table | postgres
 public | shop_orderitem        | table | postgres
 public | shop_product          | table | postgres
 public | shop_review           | table | postgres

Шаг 6. Тестовые данные

Создайте файл src/shop/management/commands/seed_data.py:

src/shop/management/
├── __init__.py
└── commands/
    ├── __init__.py
    └── seed_data.py

seed_data.py:

import random
from decimal import Decimal

from django.contrib.auth.models import User
from django.core.management.base import BaseCommand

from shop.models import Category, Order, OrderItem, Product, Review


class Command(BaseCommand):
    help = "Seed database with test data"

    def handle(self, *args, **options):
        self.stdout.write("Seeding database...")

        # Пользователи
        users = []
        for i in range(1, 11):
            user, _ = User.objects.get_or_create(
                username=f"user{i}",
                defaults={"email": f"user{i}@example.com"},
            )
            users.append(user)

        # Категории
        electronics, _ = Category.objects.get_or_create(
            slug="electronics", defaults={"name": "Electronics"}
        )
        phones, _ = Category.objects.get_or_create(
            slug="phones",
            defaults={"name": "Phones", "parent": electronics},
        )
        laptops, _ = Category.objects.get_or_create(
            slug="laptops",
            defaults={"name": "Laptops", "parent": electronics},
        )
        clothing, _ = Category.objects.get_or_create(
            slug="clothing", defaults={"name": "Clothing"}
        )
        books, _ = Category.objects.get_or_create(
            slug="books", defaults={"name": "Books"}
        )

        categories = [phones, laptops, clothing, books]

        # Продукты
        products_data = [
            ("iPhone 15", "phones", "999.99"),
            ("Samsung Galaxy S24", "phones", "899.99"),
            ("Google Pixel 8", "phones", "699.99"),
            ("MacBook Pro 14", "laptops", "1999.99"),
            ("Dell XPS 15", "laptops", "1599.99"),
            ("Lenovo ThinkPad X1", "laptops", "1399.99"),
            ("T-Shirt Basic", "clothing", "19.99"),
            ("Jeans Classic", "clothing", "59.99"),
            ("Django for Beginners", "books", "39.99"),
            ("Two Scoops of Django", "books", "49.99"),
            ("Clean Code", "books", "44.99"),
            ("Xiaomi 13 Pro", "phones", "799.99"),
            ("ASUS ZenBook", "laptops", "1199.99"),
            ("Hoodie Premium", "clothing", "79.99"),
            ("Fluent Python", "books", "54.99"),
        ]

        products = []
        for name, cat_slug, price in products_data:
            slug = name.lower().replace(" ", "-")
            category = Category.objects.get(slug=cat_slug)
            product, _ = Product.objects.get_or_create(
                slug=slug,
                defaults={
                    "name": name,
                    "price": Decimal(price),
                    "stock": random.randint(0, 100),
                    "category": category,
                    "description": f"Description for {name}",
                },
            )
            products.append(product)

        # Заказы
        statuses = [s.value for s in Order.Status]
        orders = []
        for user in users:
            for _ in range(random.randint(1, 5)):
                order = Order.objects.create(
                    user=user,
                    status=random.choice(statuses),
                )
                # Позиции заказа
                order_products = random.sample(products, random.randint(1, 4))
                total = Decimal("0")
                for product in order_products:
                    qty = random.randint(1, 3)
                    OrderItem.objects.create(
                        order=order,
                        product=product,
                        quantity=qty,
                        price=product.price,
                    )
                    total += product.price * qty
                order.total_price = total
                order.save()
                orders.append(order)

        # Отзывы
        for product in products:
            reviewers = random.sample(users, random.randint(2, 6))
            for user in reviewers:
                Review.objects.get_or_create(
                    product=product,
                    user=user,
                    defaults={
                        "rating": random.randint(1, 5),
                        "text": f"Review of {product.name} by {user.username}",
                    },
                )

        self.stdout.write(self.style.SUCCESS(
            f"Done. Created: "
            f"{User.objects.count()} users, "
            f"{Category.objects.count()} categories, "
            f"{Product.objects.count()} products, "
            f"{Order.objects.count()} orders, "
            f"{Review.objects.count()} reviews."
        ))

Запустите:

docker compose exec web python manage.py seed_data

Шаг 7. Django Debug Toolbar

Toolbar отображается в браузере при запросах к представлениям Django. Для проверки в интерактивном режиме (в shell) используем другой способ, логирование SQL.

Добавьте в settings.py для вывода SQL в консоль:

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
        },
    },
    "loggers": {
        "django.db.backends": {
            "handlers": ["console"],
            "level": "DEBUG",
        },
    },
}

Теперь каждый запрос SQL будет виден в консоли контейнера. Это удобно при работе в shell:

docker compose exec web python manage.py shell
>>> from shop.models import Product
>>> list(Product.objects.all()[:3])
# В консоли Docker появится:
# (0.001) SELECT "shop_product"."id", ... FROM "shop_product" ... LIMIT 3; args=()

В settings.py DEBUG Toolbar подключен к запросам из браузера, он покажет SQL, время запроса, количество запросов на страницу. Мы подробно разберем его в уроке 4.3, когда будем диагностировать N+1.


Что получилось

После этого урока у вас есть:

  • PostgreSQL в Docker, изолированный от системы
  • Проект Django подключен к PostgreSQL
  • Модели e-commerce с тремя типами связей: FK, OneToOne (через User), самореференция в Category
  • ~350-400 объектов тестовых данных для работы
  • Логирование SQL в консоли для отладки запросов

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


Возможные проблемы

django.db.utils.OperationalError: could not connect to server

  • контейнер PostgreSQL еще не поднялся. Подождите 5-10 секунд и повторите.
  • Проверьте: docker compose ps, статус контейнера db должен быть running.

ModuleNotFoundError: No module named 'psycopg'

  • Образ не пересобран после изменения requirements.txt.
  • Запустите: docker compose build web и повторите.

Debug Toolbar не показывается

  • Убедитесь что INTERNAL_IPS = ["127.0.0.1"] в settings.py.
  • При работе в Docker браузер обращается к localhost, Toolbar может не определить IP корректно. Добавьте в settings.py: python import socket hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + ["127.0.0.1"]

Следующий урок

В уроке 1.1 разбираем поля моделей, типы, параметры, что каждый из них делает на уровне PostgreSQL. Посмотрим, во что превращаются CharField, DecimalField, ForeignKey после миграции, и разберем новые возможности Django 5.x: db_default и GeneratedField.


Урок 1.1 >>


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

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

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

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

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

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

Пишите info@aisferaic.ru

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