Сложный вопрос о дизайне реляционной БД

У меня есть сложная проблема, над которой я возился уже несколько дней и не могу найти оптимальное решение.

Это мои таблицы:

  • сайт
  • site_node
  • страница

Таблица узлов сайта содержит список узлов, представляющих иерархию (с использованием вложенного набора). Каждый узел должен иметь одну или несколько связанных страниц. Каждый сайт должен иметь одну связанную страницу с ошибкой и одну не найденную страницу.

Итак, страница должна либо принадлежать узлу, либо сайту как страница с ошибкой или не найдена. Решения, с которыми я сейчас играю, следующие:

  1. Поля parentType и parentId в таблице страниц, где type может быть «node», «site_error» или «site_notFound», а id — это идентификатор сайта или узла (в зависимости от того, что относится к типу).
  2. поле nodeId в таблице страниц, которое может быть нулевым, а затем поля errorPageId и notFoundPageId в таблице сайта.

Вариант № 1 гарантирует, что каждая страница принадлежит одному и только одному другому объекту, хотя на самом деле связь не может быть установлена, поскольку поле parentId может указывать более чем на одно место.

Вариант № 2 чище, но в основном он говорит, что сайт «принадлежит» к двум страницам с ошибками и не найден, и это, вероятно, плохая практика.

Есть мысли или предложения?
Спасибо,
Джек.


person Jack Sleight    schedule 11.02.2009    source источник


Ответы (5)


Вариант № 1 гарантирует, что каждая страница принадлежит одному и только одному другому объекту, хотя на самом деле связь не может быть установлена, поскольку поле parentId может указывать более чем на одно место.

Верно. С точки зрения реляционной теории проблема заключается в том, что ваш столбец "parentId" нарушает третью нормальную форму, потому что его значение зависит от строки в зависимости от значения в parentType (не ключевой столбец).

У вас не будет правильно спроектированной базы данных, если один столбец может содержать чей-то номер телефона или его дату рождения в каждой строке, в зависимости от какого-либо другого флага. Это два разных факта о человеке, и каждый из них заслуживает отдельной колонки. Точно так же сохранение site_id или node_id в одном столбце будет иметь ту же проблему.

Еще один признак того, что это ошибочный дизайн, заключается в том, что вы не можете объявить ограничение внешнего ключа так, чтобы оно указывало на любую из двух таблиц, на которые ссылаются.

Вариант № 2 чище, но в основном он говорит, что сайт «принадлежит» двум страницам с ошибками и не найденным, и это, вероятно, плохая практика.

Я понимаю, почему вы так говорите, потому что принадлежит соглашениям в Rails-подобных фреймворках. Но это условности; они не обязательно являются единственными отношениями, которые могут моделировать внешние ключи. Вы можете сделать так, чтобы один объект ссылался ровно на один другой объект в отношении has one. В этом случае внешний ключ меняет направление.

Я бы сказал, что логически верно, что страница с ошибкой и страница «Не найдено» принадлежат сайту, а не наоборот. И способ сделать их обязательными состоит в том, чтобы другой объект ссылался на эти страницы и применял ограничение NOT NULL к этим ссылкам. Это то, что вы описали.

CREATE TABLE site (
  . . .
  error_page_id     INT NOT NULL,
  notfound_page_id  INT NOT NULL,
  FOREIGN KEY (error_page_id)    REFERENCES pages (page_id),
  FOREIGN KEY (notfound_page_id) REFERENCES pages (page_id)
);

Это отвечает вашим неотложным потребностям, это применимо и находится в нормальной форме.


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

@Tony Andrews предлагает сохранять два столбца на каждой странице, site_id и site_node_id и добавление ограничения CHECK, чтобы гарантировать, что ровно одно из них не равно NULL. Это кажется лучше, чем вариант parent_id/parent_type, но он по-прежнему не обеспечивает какого-либо принудительного выполнения того, что на каждом сайте должны быть страницы с ошибками и страницами «Не найдено».

person Bill Karwin    schedule 11.02.2009
comment
Спасибо, это имеет смысл. Поскольку страницы не могут быть удалены с помощью каскадного удаления с этим отношением (поскольку FK перевернут), вы бы предложили использовать триггер для удаления связанных страниц ошибок или что-то еще? - person Jack Sleight; 11.02.2009
comment
Вероятно, триггер, если вы хотите сделать это автоматически. Но вы можете захотеть, чтобы данная страница с ошибкой использовалась несколькими сайтами, поэтому в этом случае, возможно, вы не хотите, чтобы она была автоматической. - person Bill Karwin; 11.02.2009
comment
Спасибо, Билл, я выберу этот вариант. Хорошая мысль о триггере, нужно решить, как я хочу это сделать. - person Jack Sleight; 11.02.2009
comment
Что происходит, когда у вас ДОЛЖНЫ быть страница условий обслуживания, страница регистрации и что-то еще? Похоже, вам нужна новая таблица required_pages. - person jmucchiello; 11.02.2009
comment
@joe: Да, я уточнил свой ответ, это отвечает вашим непосредственным потребностям. Если у вас больше обязательных страниц, конечно, вам нужно больше столбцов. Я не вижу необходимости хранить это в отдельной таблице. Таблица sites вряд ли будет большой или будет подвергаться частым обновлениям. - person Bill Karwin; 11.02.2009

Создайте фиктивные узлы сайта для ошибки или не найденной страницы. Вы можете пометить их как определенный тип узла в соответствии с вашим первым вариантом. Это облегчит создание универсального механизма обработчика. Это также упростит объединение, что повысит производительность запросов к базе данных. Кроме того, он позволяет вам добавлять дополнительные типы «специальных» страниц (например, экран входа в систему) или настраивать их без необходимости изменять схему вашей базы данных.

person ConcernedOfTunbridgeWells    schedule 11.02.2009

Другой вариант — иметь 2 столбца site_id и site_node_id следующим образом:

create table pages
 ( page_id ... primary key
 , site_id references sites
 , site_node_id references site_nodes
 , ...
 , constraint site_or_node check (  site_id is null and site_node_id is not null
                                 or site_id is not null and site_node_id is null
                                 )
 );

Теперь вы можете использовать ссылочную целостность, чтобы убедиться, что каждая страница принадлежит либо сайту, либо узлу, а не тому и другому одновременно.

person Tony Andrews    schedule 11.02.2009

Вариант 2 имеет гораздо больше смысла и спасет ваш мозг позже, если и когда возникнут новые осложнения. Однозначное отношение сайта к странице с ошибкой/не найдено делает его идеальным для ограничения внешнего ключа.

person notnot    schedule 11.02.2009

Модификация Варианта 1.

Включите два отдельных столбца, ParentNodeID и ParentSiteID. Оставьте один из этих двух столбцов NULL, в зависимости от случая. Теперь вы все еще можете объявить ограничение внешнего ключа (ссылки) для каждого внешнего ключа.

Я не очень понимаю случай SiteNotFound. Не могли бы вы оставить оба внешних ключа NULL в этом случае?

Ваши присоединения и поиски будут проще. Вы также будете придерживаться 1NF. Это не совпадение.

Ваш вариант 1 объединяет значения, взятые из разных доменов, в одном поле. Это плохой дизайн поля, и IIRC нарушает 1NF.

person Walter Mitty    schedule 11.02.2009