Перейти к содержанию

Django: модель заметки и история схемы

Оглавление курса · Скачать все состояния проекта.

Учебная задача: Описать модель Note и отличить изменение Python-кода от применения миграции базы.

Входное состояние: Снимок lesson-04: шаблоны и статические файлы; данные временные. Выходное состояние: Модель Note и начальная миграция таблицы; список ещё использует прежние словари. Эта глава продолжает «Редакционный стол» — небольшой сайт заметок. Нужны основы Python, HTML и устройство HTTP-запроса. Используем Python 3.12 и Django 5.2.18 LTS; номер ветки выбран для этой серии. Полный каталог lesson-05 в архиве содержит собственные настройки, шаблоны и историю миграций. При самостоятельном прохождении выберите один снимок и отдельную локальную базу; смешивание файлов разных глав меняет договор примера.

Исходники подготовлены редакционным чтением. Зависимости, миграции, сервер и команды работы с данными здесь не выполнялись. Все описанные ответы и изменения являются ожидаемыми, example_execution: not_run.

Заметки редакции пока создаются заново при каждом запросе. Если редактор изменит содержимое страницы, приложению негде сохранить правку. Модель Django описывает данные, которые должны жить независимо от функции представления. В этом уроке введём Note: заголовок, текст и дату создания. Затем рассмотрим миграцию — отдельное описание изменения структуры базы. До следующей главы список продолжит показывать прежние словари; появление класса ещё не переключает источник страницы автоматически.

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

Поля и ограничения

CharField(max_length=160) предназначен для короткого заголовка. Длина — часть договора данных: она нужна для форм и схемы хранения. TextField подходит для длинного текста заметки. Здесь не задаём ограничение длины тела, чтобы не смешивать ограничения редакционной политики с первым знакомством с моделью. Когда появится форма, можно добавить отдельное правило проверки. Смысл поля должен быть понятен независимо от конкретного виджета HTML.

created_at использует DateTimeField(auto_now_add=True). Значение заполняется при первоначальном сохранении записи. Оно не является датой каждой последующей правки. Если требуется время изменения, вводится отдельное поле и отдельно объясняется его поведение. Выбор USE_TZ в настройках позволяет сохранять временные значения с поддержкой часовых поясов. Отображение местной даты относится к шаблону, а хранение момента — к модели и базе.

Первичный ключ создаётся автоматически согласно DEFAULT_AUTO_FIELD. Он отличает одну запись от другой даже при одинаковых заголовках. Заголовок не объявлен уникальным: редакция может иметь несколько материалов с похожим названием. Не выбирайте человекочитаемый текст в качестве идентификатора только потому, что его удобно видеть в списке. Позднее у заметки появится slug для адреса, но первичный ключ сохранит отдельную роль внутренней связи с записью.

Метод __str__ возвращает заголовок и делает объект понятнее в административной панели и оболочке Python. Он не управляет HTML страницы и не влияет на фильтрацию. Внутренняя Meta задаёт порядок по убыванию даты и ключа. Дополнительный ключ делает порядок определённым для записей с одинаковой датой. Без такого уточнения два одинаковых момента создания могут приводить к меняющемуся порядку, особенно когда появятся страницы результата.

Миграция как отдельный файл

Начальная миграция содержит CreateModel. Она описывает поля и параметры модели на момент первого шага. Django использует цепочку миграций для перехода между состояниями схемы. Существующие миграции лучше понимать как историю, которую уже могли применить другие разработчики или сервер. Если после выпуска вы бездумно перепишете начальный файл, история на одном компьютере станет отличаться от истории на другом, хотя Python-класс будет выглядеть одинаково.

Обычно команда makemigrations получает изменения моделей и создаёт новые файлы, а migrate применяет ещё не выполненные операции к выбранной базе. В скачиваемом примере миграции подготовлены редакционно и не запускались. Мы показываем полный файл, чтобы вы могли проследить соответствие полей класса и операции создания. Это не журнал реально выполненной команды. Когда будете работать самостоятельно, различайте наличие файла миграции и её запись в истории конкретной базы.

Зависимости задают порядок выполнения. У начальной миграции нашего приложения нет предыдущей миграции notes. У будущих файлов будет ссылка на предшествующий номер, а поле автора позднее добавит зависимость от модели пользователя. Номер помогает читать цепочку, однако система учитывает именно граф зависимостей. Нельзя гарантировать правильную последовательность простым переименованием файлов. В этой серии граф остаётся линейным, чтобы сначала понять основной механизм.

Старые данные и новый столбец

При создании пустой таблицы достаточно определить поля. Изменение существующей таблицы требует дополнительного вопроса: что делать с уже сохранёнными строками. Обязательное новое поле не может просто получить осмысленное значение из прошлого. В главе об адресах мы сначала добавим допускающий пустое значение slug, затем заполним его различными значениями, а потом включим обязательность и уникальность. Такая последовательность показывает, почему миграция — задача данных, а не только синтаксиса.

Ожидается, что после самостоятельного применения начальной миграции база содержит таблицу заметок с описанными полями. Страница всё ещё покажет два прежних словаря, потому что note_list пока не обращается к Note. Это существенное наблюдение: изменение структуры хранения и изменение источника представления — разные шаги. Найдите в снимке оба файла и объясните, какой из них потребуется изменить, чтобы список начал читать сохранённые записи. Именно это выполним в следующей главе.

Разбор связи файлов

История миграций является последовательностью изменений схемы. Текущее определение модели описывает желаемое состояние Python, а файлы миграций — путь, которым базы разных сред приходят к этому состоянию. Если вручную удалить ранее применённую миграцию только потому, что модель выглядит правильно, две среды могут получить разную историю. В серии каждый снимок хранит нужные предыдущие файлы, а не только последнюю операцию.

У поля времени auto_now_add определена роль: заполнить дату создания при первой записи через обычный механизм модели. Оно не означает автоматическую дату последнего изменения. Такое различие важно для редакции, где время выпуска материала и время правки могут быть отдельными значениями. Пока в нашей схеме есть только создание; не описываем его как историю всех действий автора.

Сортировка -created_at, затем -pk даёт понятный порядок даже для записей с одинаковой датой. Второй ключ уточняет результат, а не доказывает уникальность времени. До выполнения миграции ожидаемого столбца в базе может не быть. Поэтому разберите отдельно чтение класса модели и фактическую доступность таблицы: первый факт проверяется по исходнику, второй относится к состоянию выбранной базы после самостоятельной подготовки.

Полные файлы этого шага

Следующие файлы показаны полностью. Остальные согласованные файлы находятся в том же снимке архива; переносите весь каталог при выборе исходного состояния.

notes/models.py

from django.db import models

class Note(models.Model):
    title = models.CharField(max_length=160)
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at", "-pk"]

    def __str__(self):
        return self.title

notes/migrations/0001_initial.py

from django.db import migrations, models

class Migration(migrations.Migration):
    initial = True
    dependencies = []
    operations = [migrations.CreateModel(name="Note", fields=[
        ("id",
            models.BigAutoField(auto_created=True,
            primary_key=True,
            serialize=False,
            verbose_name="ID")),
        ("title", models.CharField(max_length=160)), ("body", models.TextField()),
        ("created_at", models.DateTimeField(auto_now_add=True)),
    ], options={"ordering": ["-created_at", "-pk"]})]

Документация и исходники

Механизм этой главы описан в официальной документации Django 5.2. При самостоятельной проверке сначала сопоставьте маршрут, представление и состояние данных, затем сравните результат с указанным ожидаемым поведением. Архив учебного проекта содержит все шестнадцать самостоятельных этапов и договор эксплуатации.