根据您对所考虑的业务环境的描述,存在一个超类-子类结构,其中包含Item(超类)及其每个类别,即Car,Boat和Plane(以及另外两个尚不为人所知)-子类型。
我将在下面详细介绍用于管理这种情况的方法。
商业规则
为了开始描述相关的概念架构,可以确定以下到目前为止确定的一些最重要的业务规则(将分析仅限于三个公开的类别,以使内容尽可能简短):
- 一个用户拥有零一或一对多产品
- 在特定的瞬间,一个项目由完全一个用户拥有
- 一个项目可能在不同的时间点被一对多用户拥有
- 一个项目仅按一个类别分类
- 一个项目是,在任何时候,
IDEF1X说明图
图1显示了我创建的IDEF1X 1图表,用于将先前的公式以及其他相关的业务规则分组:
超型
一方面,超类Item表示所有类别共有的属性†或属性,即,
- CategoryCode —指定为引用Category.CategoryCode并用作子类型区分符的外键(FK),即它表示必须与给定Item连接的子类型的确切类别,
- OwnerId(作为指向User.UserId的FK 区别开来的,但我为其分配了一个角色名称2,以便更准确地反映其特殊含义),
- 美孚,
- 酒吧,
- 巴兹和
- CreatedDateTime。
亚型
另一方面,与每个特定类别有关的属性‡,
- Qux和Corge ;
- 格鲁特,格鲁普利和塞克 ;
- Xyzzy,Thud,Wibble和Flob ;
在相应的子类型框中显示。
身份标识
然后,Item.ItemId主键(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(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)
);
如图所示,超实体类型和每个子实体类型由相应的基本表表示。
列CarId
,BoatId
和PlaneId
,约束为适当的表的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!');
--
然后,有利的观点是一个从该褶裥的列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;
--
当然,类似的方法可以遵循,这样就可以和选择“满” Boat
和Plane
信息直接从一个单一的表(派生的一个,在这些情况下)。
在那之后-如果你不介意NULL标记的存在在结果集-与下面的视图定义,就可以了,例如,“收集”,从表中的列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;
--
此处显示的视图代码仅用于说明。当然,进行一些测试练习和修改可能有助于加速手头查询的(物理)执行。另外,您可能需要根据业务需求指示在上述视图中删除或添加列。
示例数据和所有视图定义都合并到此SQL Fiddle中,以便可以“实际使用”它们。
数据操作:应用程序代码和列别名
应用程序代码的用法(如果这就是“服务器端特定代码”的意思)和列别名是您在下一条注释中提到的其他重要内容:
适当地指出,尽管使用应用程序代码是处理结果集的表示(或图形)特征的非常合适的资源,但避免逐行检索数据对于防止执行速度问题至关重要。目的应该是通过SQL平台的(精确)集合引擎提供的强大的数据处理工具将相关数据集“提取”到其中,以便您可以优化系统的行为。
此外,利用别名在特定范围内重命名一个或多个列可能看起来很重要,但就我个人而言,我认为此类资源是一个非常强大的工具,可帮助(i)上下文化和(ii)消除相关主题的含义和意图。列; 因此,在处理感兴趣的数据时,这是一个应彻底考虑的方面。
类似场景
您可能还可以从本系列文章和这组帖子中找到帮助,这些帖子包含我对另外两个案例的看法,其中包括具有互斥子类型的超类型-子类型关联。
我还为涉及超类型-子类型集群的业务环境提出了一种解决方案,其中子类型在此(较新的)答案中不互斥。
尾注
1 信息建模集成定义( IDEF1X)是一种高度推荐的数据建模技术,该技术已于1993年12月由美国国家标准技术研究院(NIST)建立为标准。它是有坚实基础的(a)上的一些理论著作的撰写由独家发起的的关系模型,即 EF科德博士 ; (b)由陈PP博士提出的关于实体关系的观点;以及(c)Robert G. Brown创建的逻辑数据库设计技术。
2在IDEF1X中,角色名称是分配给FK属性(或属性)的独特标签,目的是表达角色名称在其各自实体类型范围内的含义。
3 IDEF1X标准将键迁移定义为“将父或通用实体的主键放在其子或类别实体中作为外键的建模过程”。
Item
表包括一CategoryCode
列。正如标题为“完整性和一致性注意事项”的部分所述: