Wie ordne ich eine IS-A-Beziehung einer Datenbank zu?

26

Folgendes berücksichtigen:

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
}

Die verschiedenen Benutzertypen (Direktanmeldebenutzer und OpenID-Benutzer) zeigen eine IS-A-Beziehung an. nämlich, dass beide Benutzertypen Benutzer sind. Nun gibt es verschiedene Möglichkeiten, wie dies in einem RDBMS dargestellt werden kann:

Weg Eins

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

Weg Zwei

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
)

Weise drei

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

Ich bin mir fast sicher, dass der dritte Weg der falsche ist, da es nicht möglich ist, einen einfachen Join gegen andere Benutzer in der Datenbank durchzuführen.

Mein reales Beispiel ist jedoch kein Benutzer mit einem anderen Anmeldebeispiel. Mich interessiert, wie man diese Beziehung im allgemeinen Fall modelliert.

Billy ONeal
quelle
Ich habe meine Antwort so bearbeitet, dass sie den in den Kommentaren von Joel Brown vorgeschlagenen Ansatz enthält. Es sollte für Sie arbeiten. Wenn Sie nach weiteren Vorschlägen suchen, würde ich Ihre Frage mit einem neuen Tag versehen, um anzuzeigen, dass Sie nach einer MySQL-spezifischen Antwort suchen.
Nick Chammas
Beachten Sie, dass es wirklich davon abhängt, wie die Beziehungen im Rest der Datenbank verwendet werden. Beziehungen, an denen die untergeordneten Entitäten beteiligt sind, erfordern Fremdschlüssel nur für diese Tabellen und verbieten die Zuordnung zu einer einzelnen Tabelle, ohne dass dies zu Problemen führt.
beldaz
In objektrelationalen Datenbanken wie PostgreSQL gibt es tatsächlich einen vierten Weg: Sie können eine Tabelle INHERITS von einer anderen Tabelle deklarieren. postgresql.org/docs/current/static/tutorial-inheritance.html
MarkusSchaber

Antworten:

16

Weg zwei ist der richtige Weg.

Ihre Basisklasse erhält eine Tabelle, und dann erhalten untergeordnete Klassen ihre eigenen Tabellen mit nur den zusätzlichen Feldern, die sie einführen, sowie Fremdschlüsselverweisen auf die Basistabelle.

Wie Joel in seinen Kommentaren zu dieser Antwort vorgeschlagen hat, können Sie sicherstellen, dass ein Benutzer entweder eine direkte Anmeldung oder eine OpenID-Anmeldung hat, jedoch nicht beide (und möglicherweise auch keine), indem Sie jeder Subtyp-Tabelle, die zurückschlüsselt, eine Typenspalte hinzufügen an die Wurzeltabelle. Die Typenspalte in jeder Untertypentabelle darf nur einen einzigen Wert enthalten, der den Typ dieser Tabelle darstellt. Da diese Spalte mit einem Fremdschlüssel für die Stammtabelle versehen ist, kann jeweils nur eine Untertypzeile mit derselben Stammzeile verknüpft werden.

Zum Beispiel würde die MySQL-DDL ungefähr so ​​aussehen:

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

(Auf anderen Plattformen würden Sie CHECKstattdessen eine Einschränkung verwenden ENUM.) MySQL unterstützt zusammengesetzte Fremdschlüssel, sodass dies für Sie funktionieren sollte.

Der NULLerste Weg ist gültig, obwohl Sie in diesen Spalten Platz verschwenden, da ihre Verwendung vom Benutzertyp abhängt. Der Vorteil ist, dass Sie, wenn Sie festlegen, welche Benutzertypen gespeichert werden sollen und für diese Typen keine zusätzlichen Spalten erforderlich sind, einfach die Domäne erweitern ENUMund dieselbe Tabelle verwenden können.

Methode drei erzwingt, dass Abfragen, die auf Benutzer verweisen, mit beiden Tabellen abgeglichen werden. Dies verhindert auch, dass Sie über einen Fremdschlüssel auf eine einzelne Benutzertabelle verweisen.

Nick Chammas
quelle
1
Wie gehe ich damit um, dass es bei Verwendung von Methode 2 keine Möglichkeit gibt, zu erzwingen, dass in den beiden anderen Tabellen genau eine entsprechende Zeile vorhanden ist?
Billy ONeal
2
@ Billy - Guter Einwand. Wenn Ihre Benutzer nur den einen oder anderen haben können, können Sie dies über Ihren Proc-Layer oder Trigger erzwingen. Ich frage mich, ob es eine Möglichkeit auf DDL-Ebene gibt, diese Einschränkung durchzusetzen. (Leider lassen indizierte Ansichten nicht zu UNION , oder ich hätte eine indizierte Ansicht mit einem eindeutigen Index gegen das UNION ALLvon uidaus den beiden Tabellen vorgeschlagen.)
Nick Chammas
Dies setzt natürlich voraus, dass Ihr RDBMS in erster Linie indizierte Sichten unterstützt.
Billy ONeal
1
Eine praktische Möglichkeit, diese Art der tabellenübergreifenden Einschränkung zu implementieren, besteht darin, ein Partitionierungsattribut in die übergeordnete Tabelle aufzunehmen. Dann kann jeder Untertyp prüfen, ob er sich nur auf Supertypen bezieht, die den entsprechenden Partitionierungsattributwert haben. Dies erspart das Durchführen eines Kollisionstests durch Betrachten einer oder mehrerer anderer Untertypen-Tabellen.
Joel Brown
1
@Joel - Zum Beispiel fügen wir typejeder Untertyp -Tabelle eine Spalte hinzu, die über eine CHECKEinschränkung auf genau einen Wert (den Typ dieser Tabelle) beschränkt ist. Dann machen wir die Fremdschlüssel der Subtabelle zur Supertabelle auf beiden uidund zu zusammengesetzten Schlüsseln type. Das ist genial.
Nick Chammas
5

Sie würden benannt werden

  1. Single Table Inheritance
  2. Klassentabelle Vererbung
  3. Konkrete Tabellenvererbung .

und alle haben ihre legitimen Verwendungen und werden von einigen Bibliotheken unterstützt. Sie müssen herausfinden, welche am besten passt.

Wenn Sie mehrere Tabellen haben, wird die Datenverwaltung stärker auf Ihren Anwendungscode angewendet, der ungenutzte Speicherplatz wird jedoch verringert.

flob
quelle
2
Es gibt eine zusätzliche Technik namens "Shared Primary Key". Bei dieser Technik haben die Unterklassentabellen keine unabhängig zugewiesene Primärschlüssel-ID. Stattdessen ist die PK der Unterklassentabellen die FK, die auf die Oberklassentabelle verweist. Dies bietet mehrere Vorteile, insbesondere, dass die Eins-zu-Eins-Beziehung von IS-A-Beziehungen durchgesetzt wird. Diese Technik ist ein Add-On zur Klassentabellenvererbung.
Walter Mitty