ModelForm: формы из моделей и save()

ModelForm — мост между формой и моделью. Он строит форму прямо из модели и умеет сохранять введённые данные в базу одной строкой.
Суть: ModelForm генерирует поля из модели, валидирует их и сохраняет объект методом save(). Это убирает дублирование между описанием модели и формы.

Проблема дублирования

Когда форма повторяет поля модели, описывать их дважды — расточительство и источник рассинхронизации. Добавили поле в модель — не забудьте в форму. ModelForm устраняет это: он берёт поля прямо из модели.

from django import forms
from .models import Post

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "body", "is_published"]
        # или fields = "__all__" — все поля
        # или exclude = ["author"] — все, кроме указанных

Вложенный Meta указывает модель и список полей. Django сам определит типы виджетов, валидаторы и метки из определения модели.

Сохранение одной строкой

Главное преимущество — метод save(), который создаёт или обновляет объект модели:

def create_post(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)  # объект, но не в базе
            post.author = request.user       # доустановим поле
            post.save()                      # теперь в базу
            return redirect(post.get_absolute_url())
    else:
        form = PostForm()
    return render(request, "blog/post_form.html", {"form": form})

Параметр commit=False — частый приём: он отдаёт объект без записи в базу, чтобы вы доустановили поля (например, автора из request.user), и только потом вызвали save().

Редактирование существующего объекта

Чтобы редактировать запись, передайте её в форму через instance. Тогда форма предзаполнится, а save() обновит существующую строку, а не создаст новую:

post = get_object_or_404(Post, pk=pk)
form = PostForm(request.POST or None, instance=post)
if form.is_valid():
    form.save()   # обновит, а не создаст

Связь с CreateView и UpdateView

Вспомните дженерики из раздела про views. CreateView и UpdateView внутри используют именно ModelForm. Указав model и fields, вы получаете готовую страницу создания/редактирования без единой строки логики сохранения — Django делает всё через ModelForm под капотом.

Как это работает под капотом

ModelForm читает поля модели, строит по ним форму и при save() переносит cleaned_data в объект модели. По сути это маппинг «описание модели → поля формы → объект». Вот его модель на чистом Python:

# Попробуй сам ▶ — генерация формы из модели и save()
model_fields = {                       # описание полей модели
    "title":        {"type": "char", "max_length": 200, "required": True},
    "body":         {"type": "text", "required": True},
    "is_published": {"type": "bool", "required": False},
}

def build_form_fields(model, include):
    return {name: model[name] for name in include}  # как Meta.fields

def model_form_save(form_fields, data, commit=True):
    obj = {}
    for name, spec in form_fields.items():
        value = data.get(name)
        if spec["required"] and not value:
            return None, f"Поле {name} обязательно"
        obj[name] = value
    return (obj if commit else {"_pending": obj}), None

fields = build_form_fields(model_fields, include=["title", "body", "is_published"])
print("Поля формы:", list(fields))

obj, err = model_form_save(fields, {"title": "Django", "body": "текст", "is_published": True})
print("save():", obj, "| ошибка:", err)

obj2, err2 = model_form_save(fields, {"title": "", "body": "x"})
print("save():", obj2, "| ошибка:", err2)

Django делает это полнее (виджеты, валидаторы, запись в БД), но суть — взять поля модели, провалидировать ввод и собрать объект для сохранения.

Частые ошибки

  • Не указать fields или exclude в Meta. Django потребует явно перечислить поля (по соображениям безопасности).
  • Использовать fields="__all__" на формах для пользователей. Так можно случайно открыть редактирование чувствительных полей.
  • Забыть commit=False, когда нужно доустановить поле. Получите ошибку «поле не может быть пустым».
  • Не передать instance при редактировании. Создастся новая запись вместо обновления.

Best practices

  • Перечисляйте fields явно, а не "__all__", особенно в публичных формах.
  • Используйте commit=False для доустановки служебных полей (автор, дата).
  • Для редактирования всегда передавайте instance.
  • На типовом CRUD доверьте работу CreateView/UpdateView.

Итоги

ModelForm строит форму из модели и сохраняет данные методом save(), убирая дублирование. commit=False позволяет доустановить поля перед записью, instance — редактировать существующий объект. Дженерики CreateView/UpdateView используют ModelForm под капотом. Дальше — пользователи, вход и защита от атак.

Проверьте себя
1. Зачем вызывать form.save(commit=False)?
AЧтобы ускорить сохранение
BЧтобы получить объект без записи в базу и доустановить поля перед save()
CЧтобы отменить сохранение навсегда
DЧтобы удалить объект
2. Как при редактировании заставить ModelForm обновить запись, а не создать новую?
AПередать commit=True
BПередать существующий объект через параметр instance
CВызвать save() дважды
DУдалить старую запись вручную