Как иметь отношения один-ко-многим с привилегированным ребенком?

22

Я хочу иметь отношение «один ко многим», в котором для каждого родителя один или ноль детей помечается как «любимый». Однако не у каждого родителя будет ребенок. (Думайте о родителях как о вопросах на этом сайте, о детях как о ответах и ​​о любимых как о принятых ответах.) Например,

TableA
    Id            INT PRIMARY KEY

TableB
    Id            INT PRIMARY KEY
    Parent        INT NOT NULL FOREIGN KEY REFERENCES TableA.Id

На мой взгляд, я могу добавить следующий столбец в таблицу A:

    FavoriteChild INT NULL FOREIGN KEY REFERENCES TableB.Id

или следующий столбец к таблице B:

    IsFavorite    BIT NOT NULL

Проблема с первым подходом состоит в том, что он вводит обнуляемый внешний ключ, который, как я понимаю, не в нормализованной форме. Проблема со вторым подходом заключается в том, что необходимо проделать дополнительную работу, чтобы гарантировать, что максимум один ребенок будет любимым.

Какие критерии я должен использовать, чтобы определить, какой подход использовать? Или есть другие подходы, которые я не рассматриваю?

Я использую SQL Server 2012.

cubetwo1729
источник

Ответы:

19

Другой способ (без нулей и без циклов в FOREIGN KEYотношениях) - создать третью таблицу для хранения «любимых детей». В большинстве СУБД вам понадобится дополнительное UNIQUEограничение TableB.

@ Аарон был быстрее, чтобы определить, что соглашение об именах, приведенное выше, довольно громоздко и может привести к ошибкам. Обычно лучше (и будет держать вас в здравом уме), если у вас нет Idстолбцов во всех таблицах и если столбцы (которые объединены) имеют одинаковые имена во многих появляющихся таблицах. Итак, вот переименование:

Parent
    ParentID        INT NOT NULL PRIMARY KEY

Child
    ChildID         INT NOT NULL PRIMARY KEY
    ParentID        INT NOT NULL FOREIGN KEY REFERENCES Parent (ParentID)
    UNIQUE (ParentID, ChildID)

FavoriteChild
    ParentID        INT NOT NULL PRIMARY KEY
    ChildID         INT NOT NULL 
    FOREIGN KEY (ParentID, ChildID) 
        REFERENCES Child (ParentID, ChildID)

В SQL-сервере (который вы используете) у вас также есть опция упомянутого IsFavoriteбитового столбца. Уникальный любимый ребенок на одного родителя может быть достигнут с помощью отфильтрованного уникального индекса:

Parent
    ParentID        INT NOT NULL PRIMARY KEY

Child
    ChildID         INT NOT NULL PRIMARY KEY
    ParentID        INT NOT NULL FOREIGN KEY REFERENCES Parent (ParentID)
    IsFavorite      BIT NOT NULL

CREATE UNIQUE INDEX is_FavoriteChild
  ON Child (ParentID)
  WHERE IsFavorite = 1 ;

И главная причина того, что ваш вариант 1 не рекомендуется, по крайней мере, в SQL-Server, заключается в том, что шаблон круговых путей в ссылках на внешний ключ имеет некоторые проблемы.

Прочитайте довольно старую статью: SQL By Design: циркулярная ссылка

Вставляя или удаляя строки из двух таблиц, вы столкнетесь с проблемой «курица и яйцо». Какую таблицу я должен вставить первым - без нарушения каких-либо ограничений?

Чтобы решить эту проблему, вы должны определить хотя бы один столбец, который можно обнулять. (Хорошо, технически вам не нужно, вы можете иметь все столбцы как, NOT NULLно только в СУБД, таких как Postgres и Oracle, в которых реализованы отложенные ограничения. См. Ответ @ Erwin на аналогичный вопрос: Сложное ограничение внешнего ключа в SQLAlchemy о том, как это можно сделать в Postgres). Тем не менее, эта установка ощущается как катание на коньках по тонкому льду

Проверьте также почти идентичный вопрос в SO (но для MySQL). В SQL нормально ли для двух таблиц ссылаться друг на друга? где мой ответ почти такой же. В MySQL нет частичных индексов, поэтому единственными возможными вариантами являются обнуляемый FK и решение с дополнительными таблицами.

ypercubeᵀᴹ
источник
9

Это зависит от вашего приоритета. Вы хотите избежать работы или придерживаться самых строгих правил нормализации?

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

Предложение @ ypercube также является хорошим компромиссом.

Помимо этого, пожалуйста, пожалуйста, пожалуйста, не засоряйте вашу схему бессмысленными именами столбцов, такими как Id. Я бы предпочел, чтобы они Idбыли названы осмысленно по всей схеме. Идентифицирует ли это автора? Хорошо, позвони AuthorID. Это представляет продукт? Хорошо, ProductID. Это сотрудник, а в некоторых случаях и менеджер? Хорошо, EmployeeIDи ManagerIDимеет больше смысла для меня, чем IDи Parent. Хотя может показаться логичным пропустить это (и излишне вставлять), когда вы начнете писать сложные объединения (или публиковать здесь запросы), вы определенно почувствуете какое-то проклятие, когда будете пытаться перепроектировать группу соединений в этот момент. в столбцы, как a.Parent = b.ID... blecch.

Аарон Бертран
источник
1

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

Тем не менее, идея @ ypercube об отдельной таблице также хороша.

HLGEM
источник