Назад в блог
Инженерия
2 мин чтения
DjangoPostgreSQL

Чему система управления колледжем научила меня о моделировании данных

Студенты, преподаватели, курсы, оценки, посещаемость. Пока вы это по-настоящему не смоделируете, кажется, что это учебное CRUD-приложение, а на деле все серьёзные решения прячутся в связующих таблицах.

Система управления колледжем это проект, который снаружи выглядит учебным упражнением. Студенты, преподаватели, курсы, оценки, посещаемость: несколько таблиц и пара форм, верно? Вот это и есть ловушка. Код тут простой, а все настоящие решения прячутся в модели данных, и ошибка там оборачивается болью во всём остальном.

Первое, в чём я ошибся, это отношение к записи на курс как к обычной связи многие-ко-многим между студентами и курсами. Голый ManyToManyField отвечает только на вопрос «кто на этом курсе» и больше ни на что. Но запись несёт данные: оценку, статус, дату присоединения студента. Как только вам понадобится хоть что-то из этого, неявная связующая таблица становится тупиком.

class Enrollment(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    grade = models.CharField(max_length=2, blank=True)
    status = models.CharField(max_length=20, default="active")
    enrolled_on = models.DateField(auto_now_add=True)

    class Meta:
        unique_together = ("student", "course")

Правило, которое я вынес: как только у связи появляются собственные атрибуты, моделируйте связующую таблицу явно. Django даже позволяет сохранить удобный API через ManyToManyField(through="Enrollment"), так что вы получаете и читаемый доступ, и настоящую таблицу, на которую можно вешать поля.

Посещаемость преподала тот же урок с другой стороны, добавив к нему урок о производительности. Одна строка на студента на каждое занятие это очень много строк, а запрос, который вы реально выполняете («посещаемость по этому курсу за этот месяц»), обязан оставаться быстрым. Значит, нужен индекс по колонкам, по которым идёт фильтрация, продуманный заранее, а не после того, как всё замедлилось.

class Attendance(models.Model):
    enrollment = models.ForeignKey(Enrollment, on_delete=models.CASCADE)
    date = models.DateField()
    present = models.BooleanField(default=False)

    class Meta:
        indexes = [models.Index(fields=["enrollment", "date"])]
        unique_together = ("enrollment", "date")

Обратите внимание: посещаемость ссылается на запись о зачислении, а не на студента и курс по отдельности. Это сделано намеренно: запись о посещаемости имеет смысл только для того, кто действительно записан, и привязка к зачислению делает альтернативу попросту невыразимой. Правило обеспечивает сама схема, поэтому коду приложения не нужно о нём помнить.

Роли были последней частью. Студенты, преподаватели и администраторы это всё пользователи, но они видят разные срезы одних и тех же данных. Я оставил одну модель пользователя с ролью вместо трёх параллельных таблиц и ограничил доступ каждой роли на уровне запроса: queryset преподавателя фильтруется по его собственным курсам ещё до того, как отработает логика представления.

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

Поделиться:Поделиться в Telegram
Ещё статьи