Jak odwzorować relację IS-A w bazie danych?

26

Rozważ następujące:

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
}

Różne rodzaje użytkowników (użytkownicy z bezpośrednim logowaniem i użytkownicy OpenID) wykazują zależność IS-A; mianowicie, że oba typy użytkowników są użytkownikami. Istnieje kilka sposobów przedstawienia tego w RDBMS:

Way One

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)
)

Sposób drugi

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
)

Sposób trzeci

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)
)

Jestem prawie pewien, że trzeci sposób jest niewłaściwy, ponieważ nie można wykonać prostego łączenia przeciwko użytkownikom w innym miejscu w bazie danych.

Mój przykład z prawdziwego świata nie jest jednak przykładem użytkowników z różnymi loginami; Interesuje mnie, jak modelować ten związek w ogólnym przypadku.

Billy ONeal
źródło
Zredagowałem swoją odpowiedź, aby uwzględnić podejście sugerowane w komentarzach Joela Browna. To powinno dla ciebie zadziałać. Jeśli szukasz więcej sugestii, ponownie oznaczę twoje pytanie, aby wskazać, że szukasz odpowiedzi specyficznej dla MySQL.
Nick Chammas,
Zauważ, że tak naprawdę zależy to od sposobu wykorzystania relacji w pozostałej części bazy danych. Relacje z udziałem elementów potomnych wymagają kluczy obcych tylko do tych tabel i zabraniają mapowania do pojedynczej tabeli unifikującej bez pewnej włamań.
beldaz
W obiektowo-relacyjnych bazach danych, takich jak PostgreSQL, istnieje czwarty sposób: możesz zadeklarować tabelę INHERITS z innej tabeli. postgresql.org/docs/current/static/tutorial-inheritance.html
MarkusSchaber

Odpowiedzi:

16

Drugi sposób to właściwy sposób.

Twoja klasa podstawowa otrzymuje tabelę, a następnie klasy potomne otrzymują własne tabele z tylko dodatkowymi polami, które wprowadzają, a także odniesienia do klucza obcego do tabeli podstawowej.

Jak sugerował Joel w swoich komentarzach do tej odpowiedzi, możesz zagwarantować, że użytkownik będzie miał albo login bezpośredni, albo login OpenID, ale nie jedno i drugie (i być może żadne), dodając kolumnę typu do każdej tabeli podtypu, która zwraca klucze do tabeli głównej. Kolumna typu w każdej tabeli podtypu jest ograniczona do posiadania pojedynczej wartości reprezentującej typ tej tabeli. Ponieważ ta kolumna ma obcy klucz do tabeli głównej, tylko jeden wiersz podtypu może jednocześnie łączyć się z tym samym wierszem głównym.

Na przykład DDL MySQL wyglądałby mniej więcej tak:

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)
);

(Na innych platformach CHECKzamiast tego użyłbyś ograniczenia ENUM). MySQL obsługuje złożone klucze obce, więc powinno to działać dla Ciebie.

Sposób pierwszy jest prawidłowy, chociaż marnujesz miejsce w tych NULLmożliwych kolumnach, ponieważ ich użycie zależy od typu użytkownika. Zaletą jest to, że jeśli zdecydujesz się rozwinąć typy typów użytkowników do przechowywania, a typy te nie wymagają dodatkowych kolumn, możesz po prostu rozwinąć domenę ENUMi użyć tej samej tabeli.

Sposób trzy wymusza wszelkie zapytania, które odsyłają do użytkowników, do sprawdzenia obu tabel. Zapobiega to także odwoływaniu się do tabeli pojedynczych użytkowników za pomocą klucza obcego.

Nick Chammas
źródło
1
Jak poradzić sobie z faktem, że przy użyciu metody 2 nie ma sposobu na wymuszenie, że w dwóch pozostałych tabelach jest dokładnie jeden odpowiadający wiersz?
Billy ONeal,
2
@Billy - Dobry sprzeciw. Jeśli Twoi użytkownicy mogą mieć tylko jedno lub drugie, możesz wymusić to za pomocą warstwy proc lub wyzwalaczy. Zastanawiam się, czy istnieje sposób narzucenia tego ograniczenia na poziomie DDL. (Niestety, widoki indeksowane nie pozwalają UNION , albo bym sugerował indeksowany widok z unikalnym indeksem przeciw UNION ALLstanowi uidod dwóch tabelach.)
Nick Chammas
Oczywiście zakłada się, że twój RDBMS w pierwszej kolejności obsługiwał indeksowane widoki.
Billy ONeal,
1
Przydatnym sposobem na wdrożenie tego rodzaju ograniczenia między tabelami byłoby włączenie atrybutu partycjonowania do tabeli nadtypu. Następnie każdy podtyp może sprawdzić, czy odnosi się tylko do nadtypów, które mają odpowiednią wartość atrybutu partycjonowania. Oszczędza to konieczności przeprowadzania testu kolizji, patrząc na jedną lub więcej innych tabel podtypów.
Joel Brown,
1
@Joel - Na przykład dodajemy typekolumnę do każdej tabeli podtypu, która jest ograniczona przez CHECKograniczenie, aby mieć dokładnie jedną wartość (typ tej tabeli). Następnie przekształcamy klucze obce tabeli podrzędnej do super-tabeli w klucze złożone na obu uidi type. To genialne.
Nick Chammas,
5

Zostałyby nazwane

  1. Dziedziczenie pojedynczej tabeli
  2. Dziedziczenie tabeli klas
  3. Dziedziczenie stołu betonowego .

i wszystkie mają swoje legalne zastosowania i są obsługiwane przez niektóre biblioteki. Musisz dowiedzieć się, który najlepiej pasuje.

Posiadanie wielu tabel znacznie zwiększy zarządzanie danymi do kodu aplikacji, ale zmniejszy ilość nieużywanego miejsca.

flob
źródło
2
Istnieje dodatkowa technika o nazwie „Shared Primary Key”. W tej technice tabele podklas nie mają niezależnie przypisanego identyfikatora klucza podstawowego. Zamiast tego PK tabel podklas jest FK, który odwołuje się do tabeli nadklasy. Zapewnia to szereg korzyści, głównie dlatego, że wymusza charakter relacji IS-A typu jeden do jednego. Ta technika stanowi dodatek do dziedziczenia tabel klasowych.
Walter Mitty,