如何将 IS-A 关系映射到数据库中?

Bil*_*eal 26 database-design

考虑以下:

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
}
Run Code Online (Sandbox Code Playgroud)

不同类型的用户(直接登录用户和OpenID用户)表现出IS-A关系;也就是说,这两种类型的用户都是用户。现在,有几种方法可以在 RDBMS 中表示:

方式一

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)
)
Run Code Online (Sandbox Code Playgroud)

方式二

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
)
Run Code Online (Sandbox Code Playgroud)

方式三

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)
)
Run Code Online (Sandbox Code Playgroud)

我几乎可以肯定第三种方式是错误的方式,因为不可能对数据库中其他地方的用户进行简单的连接。

我的真实世界示例不是具有不同登录示例的用户;我对如何在一般情况下对这种关系建模很感兴趣。

Nic*_*mas 17

方法二是正确的方法。

您的基类获取一个表,然后子类获取他们自己的表,其中仅包含它们引入的附加字段,以及对基表的外键引用。

正如乔尔在他对这个答案的评论中所建议的那样,您可以通过向每个子类型表添加一个类型列来保证用户将拥有直接登录或 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)
);
Run Code Online (Sandbox Code Playgroud)

(在其他平台上,您将使用CHECK约束而不是ENUM.) MySQL支持复合外键,因此这应该适合您。

第一种方法是有效的,尽管您在那些NULL-able 列中浪费了空间,因为它们的使用取决于用户的类型。优点是,如果您选择扩展要存储的用户类型类型,并且这些类型不需要额外的列,您只需扩展您的域ENUM并使用相同的表即可。

方式三强制任何引用用户的查询都检查两个表。这也可以防止您通过外键引用单个用户表。

  • @Billy - 很好的反对意见。如果您的用户只能拥有一个或另一个,您可以通过您的 proc 层或触发器强制执行此操作。我想知道是否有 DDL 级别的方法来强制执行此约束。(唉,索引视图 [不允许](http://msdn.microsoft.com/en-us/library/dd171921(v=sql.100).aspx) `UNION`,否则我会建议索引视图具有针对两个表中 `uid` 的 `UNION ALL` 的唯一索引。) (2认同)

小智 5

他们将被命名

  1. 单表继承
  2. 类表继承
  3. 具体表继承

所有这些都有其合法用途,并得到一些图书馆的支持。你必须找出最适合的。

拥有多个表将使您的应用程序代码更多地进行数据管理,但会减少未使用的空间量。

  • 还有一种称为“共享主键”的附加技术。在这种技术中,子类表没有独立分配的主键 ID。相反,子类表的 PK 是引用超类表的 FK。这提供了几个好处,主要是它强制执行 IS-A 关系的一对一性质。这种技术是对类表继承的一种补充。 (2认同)