Как мне сопоставить отношения IS-A в базе данных?

26

Учтите следующее:

entity User
{
    autoincrement uid;
    string(20) name;
    int privilegeLevel;
}

entity DirectLoginUser
{
    inherits User;
    string(20) username;
    string(16) passwordHash;
}

entity OpenIdUser
{
    inherits User;
    //Whatever attributes OpenID needs... I don't know; this is hypothetical
}

Различные типы пользователей (пользователи с прямым входом и пользователи OpenID) отображают отношения IS-A; а именно, что оба типа пользователей являются пользователями. Теперь есть несколько способов представить это в РСУБД:

Первый путь

CREATE TABLE Users
(
    uid INTEGER AUTO_INCREMENT NOT NULL,
    name VARCHAR(20) NOT NULL,
    privlegeLevel INTEGER NOT NULL,
    type ENUM("DirectLogin", "OpenID") NOT NULL,
    username VARCHAR(20) NULL,
    passwordHash VARCHAR(20) NULL,
    //OpenID Attributes
    PRIMARY_KEY(uid)
)

Второй способ

CREATE TABLE Users
(
    uid INTEGER AUTO_INCREMENT NOT NULL,
    name VARCHAR(20) NOT NULL,
    privilegeLevel INTEGER NOT NULL,
    type ENUM("DirectLogin", "OpenID") NOT NULL,
    PRIMARY_KEY(uid)
)

CREATE TABLE DirectLogins
(
    uid INTEGER NOT_NULL,
    username VARCHAR(20) NOT NULL,
    passwordHash VARCHAR(20) NOT NULL,
    PRIMARY_KEY(uid),
    FORIGEN_KEY (uid) REFERENCES Users.uid
)

CREATE TABLE OpenIDLogins
(
    uid INTEGER NOT_NULL,
    // ...
    PRIMARY_KEY(uid),
    FORIGEN_KEY (uid) REFERENCES Users.uid
)

Третий путь

CREATE TABLE DirectLoginUsers
(
    uid INTEGER AUTO_INCREMENT NOT NULL,
    name VARCHAR(20) NOT NULL,
    privlegeLevel INTEGER NOT NULL,
    username VARCHAR(20) NOT NULL,
    passwordHash VARCHAR(20) NOT NULL,
    PRIMARY_KEY(uid)
)

CREATE TABLE OpenIDUsers
(
    uid INTEGER AUTO_INCREMENT NOT NULL,
    name VARCHAR(20) NOT NULL,
    privlegeLevel INTEGER NOT NULL,
    //OpenID Attributes
    PRIMARY_KEY(uid)
)

Я почти уверен, что третий путь - неправильный, потому что невозможно выполнить простое объединение с пользователями в другом месте базы данных.

Мой реальный пример - это не пользователи с другим логином; Меня интересует, как смоделировать эти отношения в общем случае.

Билли ОНил
источник
Я отредактировал свой ответ, чтобы включить подход, предложенный в комментариях Джоэла Брауна. Это должно работать для вас. Если вы ищете дополнительные предложения, я бы пометил ваш вопрос, чтобы указать, что вы ищете специфичный для MySQL ответ.
Ник Чаммас
Обратите внимание, что это действительно зависит от того, как отношения используются в остальной части базы данных. Отношения, включающие дочерние сущности, требуют внешних ключей только для этих таблиц и запрещают сопоставление с единой объединяющей таблицей без некоторого хакерства.
Бельдаз
В объектно-реляционных базах данных, таких как PostgreSQL, на самом деле существует четвертый способ: вы можете объявить таблицу INHERITS из другой таблицы. postgresql.org/docs/current/static/tutorial-inheritance.html
MarkusSchaber

Ответы:

16

Второй способ - правильный путь.

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

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

Например, MySQL DDL будет выглядеть примерно так:

CREATE TABLE Users
(
      uid               INTEGER AUTO_INCREMENT NOT NULL
    , type              ENUM("DirectLogin", "OpenID") NOT NULL
    // ...

    , PRIMARY_KEY(uid)
);

CREATE TABLE DirectLogins
(
      uid               INTEGER NOT_NULL
    , type              ENUM("DirectLogin") NOT NULL
    // ...

    , PRIMARY_KEY(uid)
    , FORIGEN_KEY (uid, type) REFERENCES Users (uid, type)
);

CREATE TABLE OpenIDLogins
(
      uid               INTEGER NOT_NULL
    , type              ENUM("OpenID") NOT NULL
    // ...

    PRIMARY_KEY(uid),
    FORIGEN_KEY (uid, type) REFERENCES Users (uid, type)
);

(На других платформах вы бы использовали CHECKограничение вместо ENUM.) MySQL поддерживает составные внешние ключи, так что это должно работать для вас.

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

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

Ник Чаммас
источник
1
Как мне справиться с тем фактом, что, используя способ 2, нет способа обеспечить, чтобы в двух других таблицах была точно одна соответствующая строка?
Билли Онил
2
@ Билли - Хорошее возражение. Если ваши пользователи могут иметь только одно или другое, вы можете принудительно применить это через свой уровень процессов или триггеры. Интересно, существует ли способ уровня DDL для применения этого ограничения. (Увы, индексированные представления не позволяют UNION , или я бы предложил индексированное представление с уникальным индексом против UNION ALLиз uidиз двух таблиц.)
Ник Хаммас
Конечно, это предполагает, что ваша СУБД в первую очередь поддерживает индексированные представления.
Билли Онил
1
Удобным способом реализации такого рода ограничений кросс-таблиц было бы включение атрибута разделения в таблицу супертипа. Затем каждый подтип может проверить, чтобы убедиться, что он относится только к супертипам, которые имеют соответствующее значение атрибута разделения. Это избавляет от необходимости выполнять проверку столкновения, просматривая одну или несколько других таблиц подтипов.
Джоэл Браун
1
@Joel - Так, например, мы добавляем typeстолбец к каждой таблице подтипов, который ограничен посредством CHECKограничения, чтобы иметь ровно одно значение (тип этой таблицы). Затем мы превращаем внешние ключи вложенной таблицы в супер-таблицу в составные ключи на обоих uidи type. Это гениально.
Ник Чаммас
5

Они будут названы

  1. Наследование в одной таблице
  2. Наследование таблицы классов
  3. Наследование бетонных столов .

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

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

flob
источник
2
Существует дополнительная техника, которая называется «Общий первичный ключ». В этом методе таблицы подклассов не имеют независимо назначенного идентификатора первичного ключа. Вместо этого PK таблиц подкласса - это FK, который ссылается на таблицу суперкласса. Это дает несколько преимуществ, в основном, потому что обеспечивает непосредственный характер отношений IS-A. Этот метод является дополнением к наследованию таблиц классов.
Уолтер Митти