Бесплатный курс Django ORM урок 0 | Настройка окружения
Цель урока
Подготовить рабочее окружение для всего курса: поднять PostgreSQL и Django в Docker Compose, подключить Django Debug Toolbar, создать модели учебного e-commerce проекта и загрузить тестовые данные. После этого урока у вас будет готовая база, с которой мы будем работать во всех последующих уроках.
Что потребуется
- Docker и Docker Compose (установлены на машине)
- Python 3.12+
- Базовое понимание того, что такое проект Django и миграции
Структура проекта
На протяжении всего курса мы работаем с одним проектом, упрощенным интернет-магазином. Модели спроектированы так, чтобы демонстрировать все типы связей и паттерны, которые встречаются в реальных приложениях.
Модели проекта:
Category, категории товаров с иерархией (parent → child)Product, товары с ценой, описанием, остатком на складеUser, стандартный Django UserOrder, заказы пользователей со статусом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.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru