Проектирование модели данных кинотеатра
Справочник форматов
Начинаем с создания сущности
Формат (Format). Её атрибуты:
•
Название формата (Format Name) — строковый домен, назначается первичным ключом, так как название однозначно определяет формат;
•
Описание (Description) — строковый домен, содержит дополнительные сведения.
Один и тот же фильм может быть закуплен в нескольких форматах (например, IMAX, 3D, 2D), а в одном формате выходит множество фильмов. Следовательно, между сущностями «Фильм» и «Формат» возникает связь
многие ко многим. На уровне логики она отображается линией, а на физическом уровне позже превратится в отдельную ассоциативную таблицу.
Залы кинотеатра
Переходим к верхней части предметной области — самому кинотеатру. В нём может быть несколько залов, поэтому создаём сущность
Зал (Hall). Атрибут
Название зала (Hall Name) строкового типа не обеспечит глобальной уникальности: зал «Рим» может находиться и в кинотеатре №1, и в кинотеатре №4. Поэтому первичный ключ должен включать ещё и ссылку на конкретный кинотеатр.
Строим
идентифицирующую связь от сущности «Кинотеатр». Пара «Название зала + Кинотеатр» становится уникальным идентификатором: в рамках одного кинотеатра не может быть двух залов с одинаковым названием.
Теперь нужно отразить, что зал способен показывать фильмы в разных форматах. Один и тот же зал поддерживает несколько форматов, и один формат встречается во многих залах — снова связь
многие ко многим, на этот раз между сущностями
Зал и
Формат. Соединяем их, фиксируя допустимые комбинации.
Посадочные места
Внутри каждого зала находятся посадочные места. Создаём сущность
Место (Seat Place). Информации «ряд 6 место 5» недостаточно для идентификации — неизвестно, о каком зале идёт речь. Поэтому
зал обязан войти в первичный ключ места. Связь от зала к месту —
идентифицирующая.
Добавляем целочисленные атрибуты
Ряд (Row) и
Номер места (Place), оба также включаются в первичный ключ. В результате первичный ключ места оказывается составным и включает пять полей: город, кинотеатр, зал, ряд, место. Именно для упрощения работы с такими громоздкими ключами на физическом уровне позже вводят
суррогатные ключи — простые числовые идентификаторы, жертвуя частью явной бизнес-логики ради производительности.
Места могут различаться по категории, например VIP или эконом. Вводим сущность
Категория места (Category) с атрибутами:
•
Название категории (Category Name) — уникальное строковое поле, первичный ключ;
•
Описание (Description) — строковый домен.
Каждое место относится ровно к одной категории, тогда как одна категория охватывает много мест. Связь от категории к месту —
один ко многим,
неидентифицирующая, потому что место уже полностью идентифицировано своим составным ключом, а категория лишь добавляется как внешний ключ.
Расписание сеансов
Теперь нужно связать фильмы и залы через конкретные сеансы. Создаём сущность
Сеанс (Session). Её собственный атрибут —
Дата и время начала (Start DateTime) с точностью до минуты, включается в первичный ключ.
Сеанс обязан указывать, какой фильм в каком формате будет показан. Критичный момент: связь должна отходить не от отдельных таблиц «Фильм» и «Формат», а от
ассоциативной сущности «Фильм-Формат», которая на логическом уровне скрыта за линией многие ко многим. Если провести две отдельные связи (от фильма и от формата), можно случайно создать сеанс для несуществующей пары — например, поставить фильм, закупленный только в 2D и 3D, в формате IMAX. На уровне физики появляется перекрёстная таблица, и именно от неё тянется связь к сеансу, гарантируя корректность.
Далее соединяем сеанс с сущностью
Зал идентифицирующей связью. Первичный ключ сеанса формируется комбинацией времени начала, зала и пары «фильм-формат». Расписание готово.
Заказы и кассиры
Переходим к продаже билетов. Создаём сущность
Заказ (Order). Заказ трактуется как факт обращения клиента к кассе. В рамках одного заказа можно купить любое количество билетов, но только на один сеанс. Если посетитель берёт билеты на разные сеансы — это разные заказы.
Атрибуты заказа:
• ссылка на
Сеанс;
•
Дата и время заказа (Order DateTime) с точностью до секунды.
Пара «Сеанс + Order DateTime» образует
первичный ключ, так как два заказа на один сеанс не могут быть оформлены в абсолютно одинаковое время (вплоть до секунды).
Однако если в кинотеатре несколько касс, два разных клиента теоретически могут совершить покупку на один сеанс одновременно. Тогда для уникальности в первичный ключ требуется добавить ещё и кассира. У нас есть сущность
Персонал (Employee). Чтобы выделить именно кассиров, создаём
дискриминатор (неполный подтип) — сущность
Кассир (Cashier), наследующую персонал. Связь от кассира к заказу может быть идентифицирующей (если нужна уникальность через кассира) или неидентифицирующей (если совпадения исключены либо касса одна). Выбор зависит от конкретных бизнес-правил.
Динамическая цена билета
Стоимость билета не привязывается к конкретному фильму. Вместо этого она определяется четырьмя факторами:
•
Тип дня (Day Type) — будний, выходной, праздничный;
•
Время суток (Time) — утро, день, вечер;
•
Формат показа (Format) — 2D, 3D, IMAX и т.д.;
•
Категория места (Category) — VIP, эконом.
Создаём сущность
Цена (Price) с первичным ключом, состоящим из перечисленных четырёх атрибутов. Однако цена меняется с течением времени, поэтому таблицу делают
динамической (темпоральной). Для этого добавляют три атрибута:
•
Дата начала действия (Change Start) — момент, с которого цена вступает в силу;
•
Дата окончания действия (Change End) — момент прекращения действия;
•
Значение цены (Price) — денежный тип.
Первичный ключ дополняется полем
Change Start. Когда цена изменяется, создаётся новая запись, у которой Change Start совпадает с Change End предыдущей. Таким образом, в любой момент времени можно точно определить действующую стоимость билета для конкретных параметров.
Детали заказа
Осталось зафиксировать, какие именно билеты куплены в рамках каждого заказа. Вводим сущность
Деталь заказа (Order Detail). Её первичный ключ состоит из:
• ссылки на
Заказ (идентифицирующая связь);
•
Номер позиции (Line Item) — целое число, обозначающее порядок билета в чеке (первый, второй, третий и т.д.).
Вторичным атрибутом по неидентифицирующей связи выступает
Место (Seat Place), на которое продан данный билет. Так мы связываем каждую строку заказа с конкретным посадочным местом.
Заключительные замечания
Построенная модель содержит несколько универсальных приёмов: управление составными ключами, корректная работа с ассоциативными сущностями, моделирование исторических данных и выделение подтипов через дискриминатор. Эти подходы многократно применяются в реальных проектах. После построения даталогической схемы на её основе может быть создана физическая модель, в которой для упрощения часто вводят суррогатные числовые идентификаторы, сокращая длинные составные ключи.
Краткие итоги
Проектирование началось с выделения фундаментальных справочников, которые сразу задали характер ключевых связей. Потребность в уникальной идентификации зала и посадочного места привела к составным ключам, опирающимся на контекст вышестоящих сущностей. Такое решение делает модель самодокументированной: из структуры первичного ключа места видно, что место не существует вне зала, а зал — вне кинотеатра. Осознанная избыточность составных ключей на логическом уровне позже уравновешивается введением суррогатных идентификаторов на физическом, но только после того, как зафиксированы все бизнес-правила уникальности.
Ключевым поворотом стало построение расписания. Выяснилось, что формально правильная декомпозиция связи «фильм-формат» многие ко многим незаметно порождает ассоциативную сущность, без которой сеанс рискует ссылаться на недопустимые комбинации. Этот момент иллюстрирует, как абстрактное логическое представление может скрывать критичную для целостности данных физическую реализацию, и почему необходимо «заглядывать» на уровень ниже уже на этапе высокоуровневого проектирования.
Моделирование заказов потребовало явно определить семантику транзакции: выбор пал на группировку билетов, купленных за одно обращение на один сеанс. Это решение продиктовано реальным поведением кассира и покупателя и напрямую повлияло на структуру первичного ключа. Допущение о возможности одновременных продаж с нескольких касс привело к анализу необходимости вовлечения кассира как идентификатора, а для выделения кассиров был использован дискриминатор — типовой способ ролевого моделирования сотрудников.
Динамическая природа цены потребовала превратить статическую таблицу в темпоральную. Включение даты начала действия в первичный ключ и хранение границ временного интервала позволило не только отслеживать историю, но и гарантировать отсутствие пересечений периодов действия цен. При этом сама цена была отделена от сеанса и фильма, а привязана к комбинации формата, типа дня, времени суток и категории места — так отражена реальная практика ценообразования в кинотеатрах.
В итоге выстроенная схема, будучи учебной, содержит набор универсальных шаблонов: иерархические составные ключи, перекрёстные таблицы для «многие ко многим», темпоральные атрибуты и дискриминаторы подтипов. Эти шаблоны не привязаны к домену кинотеатра и легко переносятся на задачи складского учёта, логистики или управления персоналом, формируя основу грамотного проектирования реляционных баз данных.
1. Название зала уникально только в пределах одного кинотеатра, поэтому зал идентифицируется составным ключом (название + кинотеатр).
2. Посадочное место определяется с привязкой к залу, что приводит к длинному составному первичному ключу (до пяти полей).
3. Длинные естественные ключи на физическом уровне часто заменяют суррогатными числовыми идентификаторами для повышения производительности.
4. Связь многие ко многим «фильм-формат» на физическом уровне требует отдельной ассоциативной таблицы.
5. При построении сеанса ссылаться нужно именно на ассоциативную таблицу «фильм-формат», чтобы исключить несуществующие комбинации.
6. Сеанс однозначно идентифицируется временем начала, залом и парой «фильм-формат».
7. Заказ моделируется как факт обращения на один сеанс с возможностью купить несколько билетов.
8. Уникальность заказа достигается комбинацией сеанса и точного времени оформления, а при необходимости добавляется кассир.
9. Для выделения ролей сотрудников используется дискриминатор (подтип «Кассир» от «Персонала»).
10. Цена билета зависит от типа дня, времени суток, формата показа и категории места, а не от конкретного фильма.
11. Для учёта изменения цены во времени применяется темпоральная таблица с датами начала и окончания действия.
12. Дата начала действия цены включается в первичный ключ, чтобы исключить пересечение периодов действия разных цен.
1. Почему одного названия зала недостаточно для его уникальной идентификации?
2. Какую роль играет идентифицирующая связь между залом и посадочным местом?
3. Чем идентифицирующая связь отличается от неидентифицирующей на уровне первичных ключей?
4. Почему на логическом уровне связь многие ко многим не превращается в отдельную таблицу, а на физическом — превращается?
5. Какая ошибка может возникнуть, если при создании сеанса связать его напрямую с сущностями «Фильм» и «Формат» по отдельности?
6. Из каких компонентов складывается первичный ключ сеанса в построенной модели?
7. Как выбранная семантика заказа (транзакция на один сеанс) влияет на его первичный ключ?
8. В каком случае кассир может потребоваться в первичном ключе заказа, а когда достаточно неидентифицирующей связи?
9. Для чего нужен дискриминатор при создании сущности «Кассир»?
10. Какие четыре фактора определяют цену билета и почему фильм не включён в их число?
11. Как с помощью полей Change Start и Change End обеспечить непротиворечивую историю изменения цен?
12. Каким образом деталь заказа связывает конкретный билет с посадочным местом?