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


11

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

是的,这很简单。但是,每个项目都属于某个类别(例如CarBoatPlane),并且每个Category具有特定数量的属性,例如:

Car 结构体:

+----+--------------+--------------+
| PK | Attribute #1 | Attribute #2 |
+----+--------------+--------------+

Boat 结构体:

+----+--------------+--------------+--------------+
| PK | Attribute #1 | Attribute #2 | Attribute #3 |
+----+--------------+--------------+--------------+

Plane 结构体:

+----+--------------+--------------+--------------+--------------+
| PK | Attribute #1 | Attribute #2 | Attribute #3 | Attribute #4 |
+----+--------------+--------------+--------------+--------------+

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

尽管起初看起来不错,但是我找不到通过数据库在“ 项目”和“ 类别”之间建立关系的方法,因为至少以我作为数据库管理员的经验,在创建外键时,我明确地通知了数据库表名和列。

最后,我希望有一个坚实的结构来存储所有数据,同时具有所有手段来列出用户通过一个查询可能拥有的所有商品的所有属性。

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

附加信息

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

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

实际上,这很简单:总共只有五个类别

2.同一项目将始终属于同一用户吗(也就是说,一旦将给定项目 “分配”给某个用户,就无法更改)?

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

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

不是共享的,但是从内存来看,我可以知道所有类别中至少存在三个属性。

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

否,因为项目会描述一个物理对象。用户将获得它们的虚拟副本,每个副本均由唯一的GUID v4标识

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

“在该问题的虚拟场景中,就像用户A为用户B出售了商品#1一样,因此所有权必须得到反映。”

可以这么说,您似乎正在计划跟踪项目所有权的演变。通过这种方式,您想针对该现象存储哪些属性?仅对指示特定用户(即特定所有者)的属性的修改?

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

Answers:


18

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

我将在下面详细介绍用于管理这种情况的方法。

商业规则

为了开始描述相关的概念架构,可以确定以下到目前为止确定的一些最重要的业务规则(将分析仅限于三个公开的类别,以使内容尽可能简短):

  • 一个用户拥有零一或一对多产品
  • 在特定的瞬间,一个项目由完全一个用户拥有
  • 一个项目可能在不同的时间点被一对多用户拥有
  • 一个项目仅按一个类别分类
  • 一个项目是,在任何时候,
    • 一辆
    • 飞机

IDEF1X说明图

图1显示了我创建的IDEF1X 1图表,用于将先前的公式以及其他相关的业务规则分组:

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

超型

一方面,超类Item表示所有类别共有的属性或属性,即,

  • CategoryCode —指定为引用Category.CategoryCode并用作子类型区分符的外键(FK),即它表示必须与给定Item连接的子类型的确切类别
  • OwnerId(作为指向User.UserId的FK 区别开来的,但我为其分配了一个角色名称2,以便更准确地反映其特殊含义),
  • 美孚
  • 酒吧
  • 巴兹
  • CreatedDateTime

亚型

另一方面,与每个特定类别有关的属性

  • QuxCorge ;
  • 格鲁特格鲁普利塞克 ;
  • XyzzyThudWibbleFlob ;

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

身份标识

然后,Item.ItemId主键(PK)已迁移3到具有不同角色名称的子类型,即

  • CarId
  • BoatId
  • PlaneId

互斥协会

如图所示,在(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(30) NOT NULL,
    Baz              CHAR(30) 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   CHAR(30) NOT NULL,
    Corge CHAR(30) NOT NULL,   
    --
    CONSTRAINT Car_PK         PRIMARY KEY (CarId),
    CONSTRAINT Car_to_Item_FK FOREIGN KEY (CarId)
        REFERENCES Item (ItemId)  
);

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 CHAR(30) NOT NULL,
    Garply CHAR(30) NOT NULL,   
    Plugh  CHAR(30) NOT NULL, 
    --
    CONSTRAINT Boat_PK         PRIMARY KEY (BoatId),
    CONSTRAINT Boat_to_Item_FK FOREIGN KEY (BoatId)
        REFERENCES Item (ItemId)  
);

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   CHAR(30) NOT NULL,
    Thud    CHAR(30) NOT NULL,  
    Wibble  CHAR(30) NOT NULL,
    Flob    CHAR(30) NOT NULL,  
    --
    CONSTRAINT Plane_PK         PRIMARY KEY (PlaneId),
    CONSTRAINT Plane_to_Item_PK FOREIGN KEY (PlaneId)
        REFERENCES Item (ItemId)  
);

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

CarIdBoatIdPlaneId,约束为适当的表的PKs的,在表示基于FK约束方式的概念上的级的一到一个关联的帮助§该点到ItemId列,它被约束为的PK Item表。这表示在实际的“对”中,超类型行和子类型行均由相同的PK值标识;因此,有更多机会提及

  • (a)为(b)代表该子类型的表附加一个额外的列以保存系统控制的代理值'(c)完全多余

§为了防止与(尤其是FOREIGN)KEY约束定义有关的问题和错误(您在注释中提到的情况),考虑手头不同表之间存在的存在依赖性是非常重要的,例如资料库DDL结构中表的声明顺序,我也在此SQL Fiddle中提供了顺序。

例如,将具有AUTO_INCREMENT属性的附加列附加到基于MySQL的数据库的表中。

完整性和一致性考虑

至关重要的是要指出,在您的业务环境中,您必须(1)确保每个“超类型”行始终始终由其对应的“子类型”对应项进行补充,并进而(2)确保“ subtype”行与“ supertype”行的“ discriminator”列中包含的值兼容。

声明的方式强制执行这种情况非常好,但是,据我所知,没有一个主要的SQL平台提供适当的机制来执行此操作。因此,诉诸ACID TRANSACTIONS中的过程代码非常方便,这样数据库中的这些条件就总是可以满足的。另一个选择是雇用TRIGGERS,但是可以这么说,这会使事情变得不整洁。

声明有用的观点

具有如上所述的逻辑设计,创建一个或多个视图(即包含属于两个或多个相关基本表的列的派生表)将非常实用。这样,您可以例如直接从那些视图中进行选择,而不必每次都要检索“组合”信息时都编写所有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', 'This datum', 'That datum', 'Other datum', CURDATE());

 -- 1.2
INSERT INTO Car
    (CarId, Qux, Corge)
VALUES
    (1, 'Fantastic Car', 'Powerful engine pre-update!');

-- 2. ‘Full’ Boat INSERTion

-- 2.1
INSERT INTO Item
  (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
  (2, 2, 'B', 'This datum', 'That datum', 'Other datum', CURDATE());

-- 2.2
INSERT INTO Boat
    (BoatId, Grault, Garply, Plugh)
VALUES
    (2, 'Excellent boat', 'Use it to sail', 'Everyday!');

-- 3 ‘Full’ Plane INSERTion

-- 3.1
INSERT INTO Item
  (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
  (3, 3, 'P', 'This datum', 'That datum', 'Other datum', CURDATE());

-- 3.2
INSERT INTO Plane
    (PlaneId, Xyzzy, Thud, Wibble, Flob)
VALUES
    (3, 'Extraordinary plane', 'Traverses the sky', 'Free', 'Like a bird!');

--

然后,有利的观点是一个从该褶裥的列ItemCarUserProfile

--

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;

--

当然,类似的方法可以遵循,这样就可以和选择“满” BoatPlane信息直接从一个单一的表(派生的一个,在这些情况下)。

在那之后-如果你不介意NULL标记的存在在结果集-与下面的视图定义,就可以了,例如,“收集”,从表中的列ItemCarBoatPlaneUserProfile

--

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;

--

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

示例数据和所有视图定义都合并到此SQL Fiddle中,以便可以“实际使用”它们。

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

应用程序代码的用法(如果这就是“服务器端特定代码”的意思)和列别名是您在下一条注释中提到的其他重要内容:

  • 我确实设法解决了服务器端特定代码的[JOIN]问题,但是我真的不想这样做-并且-向所有列添加别名可能会“压力很大”。

  • 很好解释,非常感谢。但是,正如我所怀疑的,由于与某些列的相似性,在列出所有数据时,我将不得不操纵结果集,因为我不想使用多个别名来保持语句更整洁。

适当地指出,尽管使用应用程序代码是处理结果集的表示(或图形)特征的非常合适的资源,但避免逐行检索数据对于防止执行速度问题至关重要。目的应该是通过SQL平台的(精确)集合引擎提供的强大的数据处理工具将相关数据集“提取”到其中,以便您可以优化系统的行为。

此外,利用别名在特定范围内重命名一个或多个列可能看起来很重要,但就我个人而言,我认为此类资源是一个非常强大的工具,可帮助(i)上下文化和(ii)消除相关主题的含义意图。列; 因此,在处理感兴趣的数据时,这是一个应彻底考虑的方面。

类似场景

您可能还可以从本系列文章这组帖子中找到帮助,这些帖子包含我对另外两个案例的看法,其中包括具有互斥子类型的超类型-子类型关联。

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


尾注

1 信息建模集成定义 IDEF1X)是一种高度推荐的数据建模技术,该技术已于1993年12月由美国国家标准技术研究院(NIST)建立为标准。它是有坚实基础的(a)上的一些理论著作的撰写由独家发起的的关系模型,即 EF科德博士 ; (b)由陈PP博士提出的关于实体关系的观点;以及(c)Robert G. Brown创建的逻辑数据库设计技术。

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

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


1
我不确定我是否理解您的询问,但是如DDL布局所示,该Item表包括一CategoryCode列。正如标题为“完整性和一致性注意事项”的部分所述:
MDCCL,

1
至关重要的是要指出,在您的业务环境中,您必须(1)确保每个“超类型”行始终始终由其对应的“子类型”对应项进行补充,并进而(2)确保“ subtype”行与“ supertype”行的“ discriminator”列中包含的值兼容。
MDCCL '19

1
以声明的方式强制执行这种情况非常好,但是,据我所知,没有一个主要的SQL平台提供适当的机制来执行此操作。因此,诉诸ACID TRANSACTIONS中的过程代码非常方便,这样数据库中的这些条件就总是可以满足的。另一个选择是雇用TRIGGERS,但是可以这么说,这会使事情变得不整洁。
MDCCL '19

1
问题的症结在于,没有SQL实现(包括MySQL方言)为ASSERTIONS,强大而优雅的声明工具提供适当的支持,这些工具将有助于避免诉诸程序方法(TRANSACTIONS或TRIGGERS),或者以冗余方式工作,例如,不必要地重复CategoryColumn代表子类型的表中的(具有逻辑[例如,修改异常]和抽象的物理级别[例如,额外的索引,较大的结构等]的所有含义)。
MDCCL '19

2
在任何数据库管理系统供应商/开发人员提供ASSERTIONS(该任务的适当工具)之前,我倾向于(a)采用过程方法(无论是TRANSACTIONS还是TRIGGERS),而不是(b)多余的做法,尽管(b)这是一种可能性,我个人不建议这样做。当然,DBA必须仔细管理有关可以在相关数据库中执行的有效数据操作操作的权限,这无疑在很大程度上维护了数据完整性。
MDCCL '19

0

让我们将主表称为“产品”。这将托管共享属性。然后,让我们说我们有一个汽车桌子,一个飞机桌子和一个船上桌子。这三个表在Product表的ID行上具有受FK约束的ProductID键。如果您想要全部-加入他们。如果只需要汽车,则将左侧的汽车与产品(或右侧的产品和汽车)连接在一起,但我更喜欢始终使用左侧的连接。

这称为分层数据模型。对于少量子表,在长表(数百万个产品)中可能有意义。


然后我将用户与产品一起加入?
user5613506

1
通常,在将产品列表返回到前端时,您不需要用户信息,而是需要产品信息。加入用户和产品并为每个返回的产品行返回相同的用户信息是没有意义的。因此,首先要通过将产品表和适当的子表(汽车,船等)连接起来,按产品类型进行过滤,然后使用WHERE子句按用户进行过滤。通常,您希望在“产品”表中拥有一个OwnerID(“用户”表的ID列上的FK)。因此,您将添加WHERE Owner = [Request.User]。
neManiac
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.