在关系数据库中存储ad-hoc属性的位置在哪里?

时间:2010-08-25 12:40:15

标签: sql

假设您有一个像INVENTORY_ITEM这样的关系数据库表。它是通用的,因为库存中的任何东西都需要记录。现在我们可以说有大量不同类型的库存,每种不同的类型可能都有他们想要跟踪的唯一字段(例如,叉子可能跟踪尖齿的数量,但冰箱不会用于该字段)。这些字段必须是每个类别类型的用户可定义的。

有很多方法可以解决这个问题:

  1. 使用ALTER TABLE语句实时添加可空列(yuk)
  2. 有两个表与一对一映射,INVENTORY_ITEM和INVENTORY_ITEM_USER,并使用ALTER TABLE语句动态添加和删除后一个表中的可空列(更好一点)。
  3. 添加CUSTOM_PROPERTY表和CUSTOM_PROPERTY_VALUE表,并在用户添加和删除行时在CUSTOM_PROPERTY中添加/删除行,并将值存储在后一个表中。这很好,通用,但性能会受到影响。如果每个项目平均有20个值,则CUSTOM_PROPERTY_VALUE中的行数将以20倍的速率上升,并且您仍需要在CUSTOM_PROPERTY_VALUE中包含可能要存储的每种不同数据类型的列。
  4. 在INVENTORY_ITEM上有一个大的varchar(MAX)字段,用于将自定义属性存储为XML。
  5. 我猜你可以为INVENTORY_ITEM表中挂起的每个类别类型设置单独的表,当用户创建库存类型时,这些表会被动态创建/销毁,并且当列添加/删除属性时,列会更新类型。虽然看起来很乱。
  6. 这是最好的做法吗?在我看来,选项4是干净的,但不允许您轻松搜索元数据。我之前使用过3的变体,但只在一个行数非常少的表上,所以性能不是问题。在我看来,2总是一个好主意,但它不适合自动生成的实体框架,所以你必须从实体生成中排除自定义属性表,只需编写自己的自定义数据访问代码处理它。

    我错过了任何其他选择吗?有没有办法让SQL服务器“查看”列中的XML数据,这样它现在可以实际使用选项4做什么?

4 个答案:

答案 0 :(得分:3)

我在这种情况下使用xml类型列...

http://msdn.microsoft.com/en-us/library/ms189887.aspx

在xml之前我们必须使用选项3.在我看来,这仍然是一个很好的方法。如果你有一个能够为你正确处理类型转换的数据访问层,那就大概相当了。我们将所有内容存储为字符串值,并定义了一个包含转换的orignial数据类型的列。

选项1和2是禁止的。请勿动态更改生产中的数据库架构。

选项5可以在一个单独的数据库中完成......但是仍然无法控制模式,用户需要创建表等的权限。

答案 1 :(得分:2)

绝对是3。

如果你有充分的理由,有时会这样做。

不要动态修改数据库结构以适应传入数据。有一天,某些东西可能会破坏并损坏您的数这不是这样做的。

答案 2 :(得分:2)

我认为只有3或4个 - 你不想动态改变架构,特别是如果你正在使用某种映射层。

我通常使用选项3.作为一点理智,我总是在CUSTOM_PROPERTY表中有一个类型列,它在CUSTOM_PROPERTY_VALUE表中重复。通过在< Primary Key,Type>的CUSTOM_PROPERTY表中添加一个超级键,您可以拥有一个引用它的外键(以及只有主键的简单外键)。最后,检查约束确保只有CUSTOM_PROPERTY_VALUE中的相关列不为null,基于此类型列。

通过这种方式,您知道如果某人定义了类型为int的CUSTOM_PROPERTY(例如,Tine计数),那么您实际上只会找到存储在CUSTOM_PROPERTY_VALUE表中的int,对于此属性的所有实例

修改

如果您需要它来引用多个实体表,那么它可能会变得更复杂,尤其是如果您想要完整的参照完整性。例如(数据库中有两个不同的实体类型):

    create table dbo.Entities (
        EntityID uniqueidentifier not null,
        EntityType varchar(10) not null,
        constraint PK_Entities PRIMARY KEY (EntityID),
        constraint CK_Entities_KnownTypes CHECK (
            EntityType in ('Foo','Bar')),
        constraint UQ_Entities_KnownTypes UNIQUE (EntityID,EntityType)
    )
    go
    create table dbo.Foos (
        EntityID uniqueidentifier not null,
        EntityType as CAST('Foo' as varchar(10)) persisted,
        FooFixedProperty1 int not null,
        FooFixedProperty2 varchar(150) not null,
        constraint PK_Foos PRIMARY KEY (EntityID),
        constraint FK_Foos_Entities FOREIGN KEY (EntityID) references dbo.Entities (EntityID) on delete cascade,
        constraint FK_Foos_Entities_Type FOREIGN KEY (EntityID,EntityType) references dbo.Entities (EntityID,EntityType)
    )
    go
    create table dbo.Bars (
        EntityID uniqueidentifier not null,
        EntityType as CAST('Bar' as varchar(10)) persisted,
        BarFixedProperty1 float not null,
        BarFixedProperty2 int not null,
        constraint PK_Bars PRIMARY KEY (EntityID),
        constraint FK_Bars_Entities FOREIGN KEY (EntityID) references dbo.Entities (EntityID) on delete cascade,
        constraint FK_Bars_Entities_Type FOREIGN KEY (EntityID,EntityType) references dbo.Entities (EntityID,EntityType)
    )
    go
    create table dbo.ExtendedProperties (
        PropertyID uniqueidentifier not null,
        PropertyName varchar(100) not null,
        PropertyType int not null,
        constraint PK_ExtendedProperties PRIMARY KEY (PropertyID),
        constraint CK_ExtendedProperties CHECK (
            PropertyType between 1 and 4), --Or make type a varchar, and change check to IN('int', 'float'), etc
        constraint UQ_ExtendedProperty_Names UNIQUE (PropertyName),
        constraint UQ_ExtendedProperties_Types UNIQUE (PropertyID,PropertyType)
    )
    go
    create table dbo.PropertyValues (
        EntityID uniqueidentifier not null,
        PropertyID uniqueidentifier not null,
        PropertyType int not null,
        IntValue int null,
        FloatValue float null,
        DecimalValue decimal(15,2) null,
        CharValue varchar(max) null,
        EntityType varchar(10) not null,
        constraint PK_PropertyValues PRIMARY KEY (EntityID,PropertyID),
        constraint FK_PropertyValues_ExtendedProperties FOREIGN KEY (PropertyID) references dbo.ExtendedProperties (PropertyID) on delete cascade,
        constraint FK_PropertyValues_ExtendedProperty_Types FOREIGN KEY (PropertyID,PropertyType) references dbo.ExtendedProperties (PropertyID,PropertyType),
        constraint FK_PropertyValues_Entities FOREIGN KEY (EntityID) references dbo.Entities (EntityID) on delete cascade,
        constraint FK_PropertyValues_Entitiy_Types FOREIGN KEY (EntityID,EntityType) references dbo.Entities (EntityID,EntityType),
        constraint CK_PropertyValues_OfType CHECK (
            (IntValue is null or PropertyType = 1) and
            (FloatValue is null or PropertyType = 2) and
            (DecimalValue is null or PropertyType = 3) and
            (CharValue is null or PropertyType = 4)),
        --Shoot for bonus points
        FooID as CASE WHEN EntityType='Foo' THEN EntityID END persisted,
        constraint FK_PropertyValues_Foos FOREIGN KEY (FooID) references dbo.Foos (EntityID),
        BarID as CASE WHEN EntityType='Bar' THEN EntityID END persisted,
        constraint FK_PropertyValues_Bars FOREIGN KEY (BarID) references dbo.Bars (EntityID)
    )
    go
    --Now we wrap up inserts into the Foos, Bars and PropertyValues tables as either Stored Procs, or instead of triggers
    --To get the proper additional columns and/or base tables populated

答案 3 :(得分:0)

我倾向于将数据存储为XML,如果数据库支持得很好,或者为不同的数据类型设置少量不同的表(尝试格式化数据以使其适合少数类型之一 - 不要将一个表用于VARCHAR(15),将另一个表用于VARCHAR(20)等。)像#5这样的东西,但是所有表都是预先创建的,并且所有内容都在现有表中。每行应包含主记录ID,记录类型指示符和一段数据。设置基于记录类型的索引,按数据子索引,并且可以查询特定的字段值(其中RecType == 19和Data =='Fred')。查询匹配多个字段值的记录会更难,但这就是生命。