如何对可以具有不同属性集的实体类型进行建模?

use*_*506 14 mysql database-design subtypes

我在重新创建UsersItems之间具有一对多(1:M)关系的数据库时遇到了一些麻烦。

这很简单,是的;但是,每个Item 都属于某个类别(例如,汽车飞机),并且每个类别都有特定数量的属性,例如:

Car 结构体:

+----+--------------+--------------+
| PK | Attribute #1 | Attribute #2 |
+----+--------------+--------------+
Run Code Online (Sandbox Code Playgroud)

Boat 结构体:

+----+--------------+--------------+--------------+
| PK | Attribute #1 | Attribute #2 | Attribute #3 |
+----+--------------+--------------+--------------+
Run Code Online (Sandbox Code Playgroud)

Plane 结构体:

+----+--------------+--------------+--------------+--------------+
| PK | Attribute #1 | Attribute #2 | Attribute #3 | Attribute #4 |
+----+--------------+--------------+--------------+--------------+
Run Code Online (Sandbox Code Playgroud)

由于属性(列)数量的这种多样性,我最初认为为每个Category创建一个单独的表是个好主意,因此我会避免多个NULL,从而更好地利用索引。

虽然一开始看起来不错,但我找不到通过数据库创建ItemsCategories之间关系的方法,因为至少以我作为数据库管理员的经验来看,在创建外键时,我明确地通知了数据库表名和列。

最后,我想要一个可靠的结构来存储所有数据,同时拥有所有方法来列出用户可能通过一个查询拥有的所有项目的所有属性。

我可以使用服务器端语言对动态查询进行硬编码,但我觉得这是错误的并且不是很理想。

附加信息

这些是我对 MDCCL 评论的回应:

1.在您的业务环境中有多少个感兴趣的项目类别,三个(即汽车飞机)或更多?

其实很简单:一共只有五个Categories

2.同一个物品是否总是属于同一个用户(也就是说,一旦给定的物品被“分配”给某个用户,它就不能改变)?

不,他们可以改变。在问题的虚构场景中,就像用户 A 为用户 B 出售 Item #1 一样,因此必须反映所有权。

3.是否有一些或所有类别共享的属性?

未共享,但从记忆中,我可以看出所有Categories中至少存在三个属性。

4. UserItem关系的基数是否有可能是多对多(M:N)而不是一对多(1:M)?例如,在以下业务规则的情况下:A User owns zero-one-or-many ItemsAn Item is owned by one-to-many Users

不,因为Items会描述一个物理对象。用户将拥有他们的虚拟副本,每个副本都由唯一的GUID v4标识

5.关于您对问题评论之一的以下回应:

“在这个问题的虚构场景中,就像用户 A 为用户 B 出售 Item #1 一样,因此必须反映所有权。”

可以这么说,您似乎正计划跟踪项目所有权的演变。通过这种方式,您希望存储此类现象的哪些属性?只有指示特定属性的修改用户谁是所有者特定的项目

不,不是真的。该所有权可能会改变,但我并不需要跟踪先前的所有者

MDC*_*CCL 20

根据您对所考虑的业务环境的描述,存在一个超类型-子类型结构,其中包含Item —超类型 — 及其每个类别,即CarBoatPlane(以及另外两个未公开的)—亚型——。

我将在下面详细说明我将用来管理上述场景的方法。

商业规则

为了开始描绘相关的概念模式,目前确定的一些最重要的业务规则(将分析仅限于三个公开的类别,以保持尽可能简短)可以表述如下:

  • 一个用户拥有零一或一对多的项目
  • 一个项目在特定时刻由一个用户拥有。
  • 一个项目可能在不同的时间点由一对多用户拥有。
  • 一个Item被完全分类为一个Category
  • 一个项目在任何时候,
    • 无论是汽车
    • 飞机

说明性的 IDEF1X 图

图 1显示了我创建的 IDEF1X 1图,用于将之前的公式与其他相关的业务规则分组在一起:

图 1 - 项目和类别超类型-子类型结构

超类型

一方面,Item,超类型,呈现了所有Categories共有的属性或属性,即,

  • CategoryCode — 指定为引用Category.CategoryCode并用作子类型鉴别器的外键 (FK) ,即,它指示给定必须与之连接的子类型的确切类别— ,
  • OwnerId —区别为指向 User.UserId 的 FK ,但我为其分配了角色名称2,以便更准确地反映其特殊含义—,
  • ,
  • 酒吧
  • 巴兹
  • 创建日期时间

亚型

另一方面,属于每个特定Category的属性,即,

  • QuxCorge ;
  • 格劳特加普利普拉格
  • Xyzzy , Thud , WibbleFlob ;

显示在相应的子类型框中。

身份标识

然后,Item.ItemId PRIMARY KEY (PK) 已经将3迁移到不同角色名称的子类型,即,

  • 身份证
  • 船号
  • 平面标识

互斥关联

如图所示,在 (a) 每个超类型出现和 (b) 其互补子类型实例之间存在一对一(1:1) 基数比的关联或关系。

独占亚型符号描绘的事实,亚型是相互排斥的,即,混凝土物品发生可以仅由单个亚型实例加以补充:任一个汽车,或一个平面,或者一个(从未由零或更小,也没有由两个或更多)。

, 我使用了经典的占位符名称来授权某些实体类型属性,因为问题中未提供它们的实际面额。

说明性逻辑级布局

因此,为了讨论说明性逻辑设计,我根据上面显示和描述的 IDEF1X 图导出了以下 SQL-DDL 语句:

-- You should determine which are the most fitting 
-- data types and sizes for all your table columns 
-- depending on your business context characteristics.

-- Also, you should make accurate tests to define the 
-- most convenient INDEX strategies based on the exact 
-- data manipulation tendencies of your business context.

-- As one would expect, you are free to utilize 
-- your preferred (or required) naming conventions. 

CREATE TABLE UserProfile (
    UserId          INT      NOT NULL,
    FirstName       CHAR(30) NOT NULL,
    LastName        CHAR(30) NOT NULL,
    BirthDate       DATE     NOT NULL,
    GenderCode      CHAR(3)  NOT NULL,
    Username        CHAR(20) NOT NULL,
    CreatedDateTime DATETIME NOT NULL,
    --
    CONSTRAINT UserProfile_PK  PRIMARY KEY (UserId),
    CONSTRAINT UserProfile_AK1 UNIQUE ( -- Composite ALTERNATE KEY.
        FirstName,
        LastName,
        GenderCode,
        BirthDate
    ),
    CONSTRAINT UserProfile_AK2 UNIQUE (Username) -- ALTERNATE KEY.
);

CREATE TABLE Category (
    CategoryCode     CHAR(1)  NOT NULL, -- Meant to contain meaningful, short and stable values, e.g.; 'C' for 'Car'; 'B' for 'Boat'; 'P' for 'Plane'.
    Name             CHAR(30) NOT NULL,
    --
    CONSTRAINT Category_PK PRIMARY KEY (CategoryCode),
    CONSTRAINT Category_AK UNIQUE      (Name) -- ALTERNATE KEY.
);

CREATE TABLE Item ( -- Stands for the supertype.
    ItemId           INT      NOT NULL,
    OwnerId          INT      NOT NULL,
    CategoryCode     CHAR(1)  NOT NULL, -- Denotes the subtype discriminator.
    Foo              CHAR(30) NOT NULL,
    Bar              CHAR(40) NOT NULL,
    Baz              CHAR(55) NOT NULL,  
    CreatedDateTime  DATETIME NOT NULL,
    --
    CONSTRAINT Item_PK             PRIMARY KEY (ItemId),
    CONSTRAINT Item_to_Category_FK FOREIGN KEY (CategoryCode)
        REFERENCES Category    (CategoryCode),
    CONSTRAINT Item_to_User_FK     FOREIGN KEY (OwnerId)
        REFERENCES UserProfile (UserId)  
);

CREATE TABLE Car ( -- Represents one of the subtypes.
    CarId INT          NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
    Qux   DATE         NOT NULL,
    Corge DECIMAL(5,2) NOT NULL,   
    --
    CONSTRAINT Car_PK         PRIMARY KEY (CarId),
    CONSTRAINT Car_to_Item_FK FOREIGN KEY (CarId)
        REFERENCES Item (ItemId),
    CONSTRAINT ValidQux_CK    CHECK       (Qux >= '1990-01-01')   
);

CREATE TABLE Boat ( -- Stands for one of the subtypes.
    BoatId INT      NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
    Grault SMALLINT NOT NULL,
    Garply DATETIME NOT NULL,   
    Plugh  CHAR(63) NOT NULL, 
    --
    CONSTRAINT Boat_PK         PRIMARY KEY (BoatId),
    CONSTRAINT Boat_to_Item_FK FOREIGN KEY (BoatId)
        REFERENCES Item (ItemId),
    CONSTRAINT ValidGrault_CK  CHECK       (Grault <= 10000)  
);

CREATE TABLE Plane ( -- Denotes one of the subtypes.
    PlaneId INT      NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
    Xyzzy   BIGINT   NOT NULL,
    Thud    TEXT     NOT NULL,  
    Wibble  CHAR(20) NOT NULL, 
    Flob    BIT(1)   NOT NULL,   
    --
    CONSTRAINT Plane_PK         PRIMARY KEY (PlaneId),
    CONSTRAINT Plane_to_Item_PK FOREIGN KEY (PlaneId)
        REFERENCES Item (ItemId),
    CONSTRAINT ValidXyzzy_CK    CHECK       (Xyzzy <= 3258594758)
);
Run Code Online (Sandbox Code Playgroud)

这已经在运行在 MySQL 8.0 上的这个 db<>fiddle 中进行了测试。

如图所示,超实体类型和每个子实体类型由相应的表表示。

CarId,BoatId和 被PlaneId约束为适当表的 PK,通过 FK 约束§帮助表示概念级的一对一关联ItemId,该约束指向列,列被约束为Item表的 PK 。这意味着,在实际的“对”中,超类型和子类型行都由相同的 PK 值标识;因此,现在提

  • (a) 附加一个额外的列来保存系统控制的替代值(b) 代表子类型的表格是 (c)完全多余的

§为了防止关于(特别是 FOREIGN)KEY 约束定义的问题和错误——你在评论中提到的情况——,考虑到手头不同表之间发生的存在依赖性非常重要,如说明性 DDL 结构中表的声明顺序,我也在这个 db<>fiddle 中提供

? 例如,将具有AUTO_INCREMENT属性的附加列附加到建立在 MySQL 上的数据库的表中。

完整性和一致性注意事项

必须指出的是,在您的业务环境中,您必须 (1) 确保每个“超类型”行始终由其对应的“子类型”对应行补充,并且反过来,(2) 保证所述“子类型”行与“超类型”行的“鉴别器”列中包含的值兼容。

声明方式强制执行这种情况会非常优雅,但不幸的是,据我所知,没有一个主要的 SQL 平台提供适当的机制来这样做。因此,在ACID TRANSACTIONS 中诉诸程序代码是非常方便的,以便在您的数据库中始终满足这些条件。其他选择是使用触发器,但可以这么说,它们往往会使事情变得不整洁。

声明有用的视图

具有与上述类似的逻辑设计,创建一个或多个视图将非常实用,即包含属于两个或多个相关表的列的派生表。通过这种方式,您可以,例如,直接从这些视图中进行 SELECT,而不必在每次必须检索“组合”信息时编写所有 JOIN。

样本数据

在这方面,让我们说基表是用下面显示的示例数据“填充”的:

--
INSERT INTO UserProfile 
    (UserId, FirstName, LastName, BirthDate, GenderCode, Username, CreatedDateTime)
VALUES
    (1, 'Edgar', 'Codd', '1923-08-19', 'M', 'ted.codd', CURDATE()),
    (2, 'Michelangelo', 'Buonarroti', '1475-03-06', 'M', 'michelangelo', CURDATE()),
    (3, 'Diego', 'Velázquez', '1599-06-06', 'M', 'd.velazquez', CURDATE());

INSERT INTO Category 
    (CategoryCode, Name)
VALUES
    ('C', 'Car'), ('B', 'Boat'), ('P', 'Plane');

-- 1. ‘Full’ Car INSERTion

-- 1.1 
INSERT INTO Item
    (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
    (1, 1, 'C', 'Motorway', 'Tire', 'Chauffeur', CURDATE());
 
 -- 1.2
INSERT INTO Car
    (CarId, Qux, Corge)
VALUES
    (1, '1999-06-11',  999.99);

-- 2. ‘Full’ Boat INSERTion

-- 2.1
INSERT INTO Item
    (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
    (2, 2, 'B', 'Ocean', 'Anchor', 'Sailor', CURDATE());

-- 2.2
INSERT INTO Boat
    (BoatId, Grault, Garply, Plugh)
VALUES
    (2, 10000, '2016-03-09 07:32:04.000', 'So far so good.');

-- 3 ‘Full’ Plane INSERTion

-- 3.1   
INSERT INTO Item
    (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
    (3, 3, 'P', 'Sky', 'Wing', 'Aviator', CURDATE());

-- 3.2

INSERT INTO Plane
    (PlaneId, Xyzzy, Thud, Wibble, Flob)
VALUES
    (3, 3258594758, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sollicitudin pharetra sem id elementum. Sed tempor hendrerit orci. Ut scelerisque pretium diam, eu sodales ante sagittis ut. Phasellus id nunc commodo, sagittis urna vitae, auctor ex. Duis elit tellus, pharetra sed ipsum sit amet, bibendum dapibus mauris. Morbi condimentum laoreet justo, quis auctor leo rutrum eu. Sed id nibh non leo sodales pulvinar. Nam ornare ipsum nunc, eget molestie nulla ultrices vel. Curabitur fermentum nisl quis lorem aliquam pretium aliquam at mauris. In vestibulum, tellus et pharetra sollicitudin, mi lacus consectetur dolor, id volutpat nulla eros a mauris. ', 'Here we go!', TRUE);

--
Run Code Online (Sandbox Code Playgroud)

然后,一个有利的观点是从Item,Car和收集列UserProfile

--

CREATE VIEW CarAndOwner AS
    SELECT C.CarId,
           I.Foo,
           I.Bar,
           I.Baz,
           C.Qux,
           C.Corge,           
           U.FirstName AS OwnerFirstName,
           U.LastName  AS OwnerLastName
        FROM Item I
        JOIN Car C
          ON C.CarId = I.ItemId
        JOIN UserProfile U
          ON U.UserId = I.OwnerId;

--
Run Code Online (Sandbox Code Playgroud)

自然地,可以遵循类似的方法,以便您也可以直接从单个表中选择“完整”BoatPlane信息(在这些情况下是派生的)中。

之后——如果你不介意结果集中存在 NULL 标记——使用以下 VIEW 定义,你可以,例如,从表Item, Car, Boat,Plane和 中“收集”列UserProfile

--

CREATE VIEW FullItemAndOwner AS
    SELECT I.ItemId,
           I.Foo, -- Common to all Categories.
           I.Bar, -- Common to all Categories.
           I.Baz, -- Common to all Categories.
          IC.Name      AS Category,
           C.Qux,    -- Applies to Cars only.
           C.Corge,  -- Applies to Cars only.
           --
           B.Grault, -- Applies to Boats only.
           B.Garply, -- Applies to Boats only.
           B.Plugh,  -- Applies to Boats only.
           --
           P.Xyzzy,  -- Applies to Planes only.
           P.Thud,   -- Applies to Planes only.
           P.Wibble, -- Applies to Planes only.
           P.Flob,   -- Applies to Planes only.
           U.FirstName AS OwnerFirstName,
           U.LastName  AS OwnerLastName
        FROM Item I
        JOIN Category IC
          ON I.CategoryCode = IC.CategoryCode
   LEFT JOIN Car C
          ON C.CarId = I.ItemId
   LEFT JOIN Boat B
          ON B.BoatId = I.ItemId
   LEFT JOIN Plane P
          ON P.PlaneId = I.ItemId               
        JOIN UserProfile U
          ON U.UserId = I.OwnerId;

--
Run Code Online (Sandbox Code Playgroud)

此处显示的视图代码仅用于说明。当然,进行一些测试练习和修改可能有助于加速手头查询的(物理)执行。此外,您可能需要根据业务需求为所述视图删除或添加列。

示例数据和所有视图定义都包含在此 db<>fiddle 中,以便可以“在操作中”观察它们。

数据操作:应用程序代码和列别名

应用程序代码的使用(如果这就是“服务器端特定代码”的意思)和列别名是您在下一条评论中提出的其他重要点:

  • 我确实设法解决了服务器端特定代码的 [a JOIN] 问题,但我真的不想这样做 - 并且 - 向所有列添加别名可能会“造成压力”。
  • 解释的很好,非常感谢。但是,正如我所怀疑的那样,由于与某些列的相似性,我在列出所有数据时必须操纵结果集,因为我不想使用多个别名来保持语句更清晰。

虽然使用应用程序代码是处理数据集的表示或图形特征(即计算机化信息系统的外部表示)的非常合适的资源,但最重要的是避免执行数据逐行检索以防止执行速度问题。目标应该是通过 SQL 平台的(精确的)设置引擎提供的强大的数据操作工具来“获取”相关的数据集,以便您可以优化系统的行为。

此外,使用别名来重命名某个范围内的一个或多个列可能看起来很紧张,但就个人而言,我认为这种资源是一种非常强大的工具,有助于 (i) 上下文化和 (ii) 消除含义意图的歧义归因于列; 因此,对于感兴趣的数据的操作,这是应该彻底考虑的一个方面。

类似场景

你不妨找帮助这一系列的帖子这组帖子其中包含我对另外两个案例的看法,其中包括具有互斥子类型的超类型-子类型关联。

我还为涉及超类型-子类型集群的业务环境提出了一个解决方案,其中子类型在这个(较新的)答案并不相互排斥。


尾注

1所 对于信息建模集成定义 IDEF1X)是被确立为一个非常可取的数据建模技术标准由美国在1993年12月美国国家标准与技术研究院(NIST)。它是有坚实基础的(a)上的一些理论著作的撰写由独家发起的的关系模型,即EF科德博士; 关于 (b)实体关系视图,由PP Chen 博士开发;以及 (c) 逻辑数据库设计技术,由 Robert G. Brown 创建。

2在 IDEF1X 中,角色名称是分配给 FK 属性(或属性)的独特标签,以表达其在其各自实体类型范围内的含义。

3 IDEF1X 标准将键迁移定义为“将父实体或通用实体的主键作为外键放置在其子实体或类别实体中的建模过程”。

  • 在任何数据库管理系统供应商/开发人员提供断言(执行此任务的合适工具)之前,我更喜欢 (a) 过程方法——无论是事务还是触发器——而不是 (b) 多余的行动过程,尽管 (b)是一种可能性——我个人不推荐——。当然,DBA 必须仔细管理有关可以在相关数据库中执行的有效数据操作操作的权限,这对于维护数据完整性无疑有很大帮助。 (2认同)