如何将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关系。即,两种类型的用户都是用户。现在,有几种方法可以在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)
)

方式二

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

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

我的真实示例不是使用不同登录示例的用户;而是 我对如何在一般情况下建立这种关系感兴趣。


我已经编辑了答案,以包括乔尔·布朗(Joel Brown)评论中建议的方法。它应该为您工作。如果您正在寻找更多建议,我会重新标记您的问题,以表明您正在寻找MySQL特定的答案。
Nick Chammas

注意,这实际上取决于在数据库的其余部分中如何使用关系。涉及子实体的关系只需要那些表的外键,并禁止映射到单个统一表而没有任何恶意。
贝尔达兹

在像PostgreSQL这样的对象关系数据库中,实际上有第四种方法:您可以从另一个表声明一个表INHERITS。postgresql.org/docs/current/static/tutorial-inheritance.html
MarkusSchaber,

Answers:


16

方法二是正确的方法。

您的基类将获得一个表,然后子类将获得其自己的表,以及它们引入的其他字段以及对该基表的外键引用。

正如Joel在对此答案的评论中所建议的那样,您可以通过在每个子类型表中添加一个类型列以进行回退,从而确保用户将具有直接登录名或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,无法强制其他两个表中恰好有一个对应行的事实呢?
Billy ONeal

2
@Billy-好反对。如果您的用户只能拥有一个或另一个,则可以通过proc层或触发器来强制执行此操作。我想知道是否存在DDL级别的方法来强制执行此约束。(唉,索引视图不允许 UNION,或者我会建议与对唯一索引的索引视图UNION ALLuid来自这两个表。)
尼克Chammas

当然,这假定您的RDBMS首先支持索引视图。
Billy ONeal

1
实现这种交叉表约束的一种简便方法是在超级类型表中包括分区属性。然后,每个子类型都可以检查以确保仅与具有适当分区属性值的超类型有关。这省去了通过查看一个或多个其他子类型表进行碰撞测试的麻烦。
乔尔·布朗

1
@Joel-因此,例如,我们type在每个子类型表中添加一列,该列通过CHECK约束被限制为仅具有一个值(该表的类型)。然后,我们将超级表的子表外键在uid和上组合为复合键type。太巧妙了。
Nick Chammas

5

他们将被命名

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

都有合法用途,并且受到某些库的支持。您必须找出最合适的。

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


2
还有一种称为“共享主键”的附加技术。在这种技术中,子类表没有独立分配的主键ID。而是,子类表的PK是引用超类表的FK。这提供了几个好处,主要是它增强了IS-A关系的一对一性质。此技术是“类表继承”的附加功能。
Walter Mitty
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.