应用程序配置或应用程序选项设置的最佳表设计?

Posted

技术标签:

【中文标题】应用程序配置或应用程序选项设置的最佳表设计?【英文标题】:Best table design for application configuration or application option settings? 【发布时间】:2009-09-07 02:13:02 【问题描述】:

我需要在数据库中存储一系列配置值。 我想到的存储它们的几种方法是:一个有 2 个列(名称,值)的表,每对有一行,或者一个表,每个配置参数都有一个列和 1 行? 第一个我只需要添加另一行来添加配置值,第二个我需要在表中添加一列。我应该考虑任何问题吗?一个比另一个更有效吗?

【问题讨论】:

【参考方案1】:

对于配置数据,我会使用键/值结构,每个配置条目都有一行。您可能会读取此数据一次并将其缓存,因此性能不是问题。正如您所指出的,每次更改配置键集时添加列需要更多维护。

SQL 擅长建模和处理任意大的相似(如果不相同)结构化数据集。一组配置信息真的不是那样——你有单行数据,或者你有多行完全不相关的数据。这表示您只是将其用作数据存储。我说跳过 SQL 数据模型,简单一点。

【讨论】:

这个解决方案的简单性很引人注目,但很难处理相关值、类数组值和树。但也许在key 列上使用starting with 选择会有所帮助... 我总是将此结构用于配置目的,并添加一个可空字段“Section”来分组键值对。【参考方案2】:

还有一个注意事项:每个配置参数都有一个列,您可以轻松地拥有版本。每行代表一个版本*


* 的完整参数集(如comment by granadaCoder 中指出的那样)

【讨论】:

这是一个非常有趣的想法! 这应该详细说明。这对我来说太模糊了。如果有人有时间延长答案,我很乐意推翻我的投票。 您可以拥有使用行方法的版本。考虑这个架构(键、值、版本)。 @Wolf。你可以有版本。有时人们会编码“StartDate”和“EndDate”。想象一下,您有一些设置需要在新年前夜和新年之间更改美国东部标准时间的午夜。 (又名,新年的第一次第二次)。有人会在午夜坐在那里按“保存”按钮吗?可能不是。您可以让该行具有开始日期和结束日期,检索代码将根据日期获取设置的“版本”。因此,当新的一年到来时,无需任何人在场。 更简单的“版本”方法...是您有一个“活动/非活动”标志。因此,当您更新设置时,您不会编辑 Version1(行)。您停用 Version1(行)......并创建新的 Version2(行)(当然这将是“活动的”),并将新设置放在那里。这样您就可以了解设置的“历史”随着时间的推移。该模式还有其他排列方式……但是这 2 个 cmets 传达了一些模式。 〜可能〜在这样的事情中很重要:en.wikipedia.org/wiki/Ex_post_facto_law【参考方案3】:

您应该考虑的第一个问题是:停止考虑检索信息的效率。首先,弄清楚如何有效地正确地对数据进行建模,然后(并且只有这样)弄清楚如何高效地做到这一点。

因此,这取决于您存储的配置数据的性​​质。如果单独的(名称,值)对基本上不相关,则将其存储为每行一个。如果它们是相关的,那么您可能需要考虑一个具有多列的方案。

我所说的相关是什么意思?考虑一些缓存配置。每个缓存都有几个属性:

驱逐政策; 到期时间; 最大尺寸。

假设每个缓存都有一个名称。您可以将此数据存储为三行:

<name>_EVICTION <name>_EXPIRY <name>_MAX_SIZE

但这些数据是相关的,您可能经常需要一次全部检索它们。在这种情况下,拥有一个包含五列的 cache_config 表可能是有意义的:id、name、eviction、expiry、max_size。

这就是我所说的相关数据。

【讨论】:

只是一个想法:如果我们坚持每行一个值,那么对一组条目使用更“复杂”的值呢?为此,值应该是字符串。【参考方案4】:

为每个(应用程序)配置设置(或应用程序选项)使用单独的行的一个缺点是您无法将设置值存储在具有适当数据类型的列中。用户可以输入无效的type 数据吗?这与您的应用程序有关吗?

使用单独列的一个好处是,数据库本身中的任何代码(例如存储过程、函数等)都可以使用适当数据类型的值,而无需首先检查无效值然后转换为适当的数据类型。

如果您手动将更改部署到您的应用程序数据库,那么是的,如果您使用的是 EAV 设计,那么部署新的配置设置会非常稍微容易一些,但实际上可以节省多少:

INSERT Options ( ConfigurationSetting, Value )
VALUES ( 'NewConfigurationSetting', NewConfigurationSettingValue )

对比:

ALTER TABLE Options ADD NewConfigurationSetting some_datatype

UPDATE Options
SET NewConfigurationSetting = NewConfigurationSettingValue

【讨论】:

节省取决于数据库系统:列和行的限制在大多数情况下不同。 @Wolf 我指的是在易于开发和部署方面的“节省”。几乎在所有情况下,对表中列数或行数的任何限制都可能无关紧要。无论如何,人们都可以简单地创建另一个表。【参考方案5】:

我讨厌将非字符串值放入字符串列(又名不正确的数据类型)。 (正如@Kenny Evitt 上面所讨论的)

所以我想出了下面的替代方法,它垂直并处理正确的数据类型。

我实际上不使用金钱和小钱。但为了完整起见,我将它们包括在内。 注意,还有一些其他的数据类型

https://msdn.microsoft.com/en-us/library/ms187752.aspx?f=255&MSPPError=-2147217396

但以下内容涵盖了大部分内容。

说实话,我只使用字符串 (varchar(1024))、int、smallint 和 bit ... 99% 的时间。

它并不完美。 Aka,你有很多空元组。但由于您只抓取一次(并缓存),因此映射到设置对象(在我的世界中使用 c#)并不困难。

CREATE TABLE [dbo].[SystemSetting](
    [SystemSettingId] [int] IDENTITY NOT NULL,

    [SettingKeyName] [nvarchar](64) NOT NULL, 
    [SettingDataType] [nvarchar](64) NOT NULL, /* store the datatype as string here */

    [SettingValueBigInt] bigint NULL, 
    [SettingValueNumeric] numeric NULL, 
    [SettingValueSmallInt] smallint NULL, 
    [SettingValueDecimal] decimal NULL, 
    [SettingValueSmallMoney] smallmoney NULL, 
    [SettingValueInt] int NULL, 
    [SettingValueTinyInt] tinyint NULL, 
    [SettingValueMoney] money NULL, 
    [SettingValueFloat] float NULL, 
    [SettingValueReal] real NULL, 
    [SettingValueDate] date NULL, 
    [SettingValueDateTimeOffSet] datetimeoffset NULL, 
    [SettingValueDateTime2] datetime2 NULL, 
    [SettingValueSmallDateTime] smalldatetime NULL, 
    [SettingValueDateTime] datetime NULL, 
    [SettingValueTime] time NULL, 
    [SettingValueVarChar] varchar(1024) NULL, 
    [SettingValueChar] char NULL, 

    [InsertDate] [datetime] NOT NULL DEFAULT (GETDATE()),               
    [InsertedBy] [nvarchar](50) NOT NULL DEFAULT (SUSER_SNAME()),       
    [LastUpdated] [datetime] NOT NULL DEFAULT (GETDATE()),              
    [LastUpdatedBy] [nvarchar](50) NOT NULL DEFAULT (SUSER_SNAME()),    
)

现在,如果这太多了,并且您决定对所有值使用“字符串”,那么这里有一些 DDL。

DROP TABLE [dbo].[SystemSetting]
DROP TABLE [dbo].[SystemSettingCategory]

CREATE TABLE [dbo].[SystemSettingCategory] (
    [SystemSettingCategoryId] [int] NOT NULL,
    [SystemSettingCategoryName] [nvarchar](64) NOT NULL, 
    [InsertDate] [datetime] NOT NULL DEFAULT (GETDATE()),               
    [InsertedBy] [nvarchar](50) NOT NULL DEFAULT (SUSER_SNAME()),       
    [LastUpdated] [datetime] NOT NULL DEFAULT (GETDATE()),              
    [LastUpdatedBy] [nvarchar](50) NOT NULL DEFAULT (SUSER_SNAME()),    
    CONSTRAINT [PK_SystemSettingCategory] PRIMARY KEY CLUSTERED ([SystemSettingCategoryId] ASC),
    CONSTRAINT UQ_SystemSettingCategoryName UNIQUE NONCLUSTERED ([SystemSettingCategoryName])
)   




CREATE TABLE [dbo].[SystemSetting] (
    [SystemSettingId] [int] NOT NULL,
    [SystemSettingCategoryId] INT NOT NULL,     /* FK to [SystemSettingCategory], not shown here */
    [SettingKeyName] [nvarchar](64) NOT NULL, 
    [SettingValue] nvarchar(1024) NULL,
    [InsertDate] [datetime] NOT NULL DEFAULT (GETDATE()),               
    [InsertedBy] [nvarchar](50) NOT NULL DEFAULT (SUSER_SNAME()),       
    [LastUpdated] [datetime] NOT NULL DEFAULT (GETDATE()),              
    [LastUpdatedBy] [nvarchar](50) NOT NULL DEFAULT (SUSER_SNAME()),    
    CONSTRAINT [PK_SystemSetting] PRIMARY KEY CLUSTERED ([SystemSettingId] ASC),
    CONSTRAINT FK_SystemSettingCategory_SystemSettingCategoryId foreign key ([SystemSettingCategoryId]) references [SystemSettingCategory] ([SystemSettingCategoryId]),
    CONSTRAINT UQ_SystemSettingCategoryId_SettingKeyName UNIQUE NONCLUSTERED ( [SystemSettingCategoryId] , [SettingKeyName] )
)   



INSERT INTO [dbo].[SystemSettingCategory] ( [SystemSettingCategoryId] , [SystemSettingCategoryName] )
select 101 , 'EmployeeSettings' UNION ALL select 201, 'StopLightSettings'

INSERT INTO [dbo].[SystemSetting] ( [SystemSettingId] , [SystemSettingCategoryId] , [SettingKeyName] , [SettingValue] )
          select 1001 , 101 , 'MininumAgeRequirementMonths' , convert(varchar(16) , (12 * 18))
UNION ALL select 1002 , 101 , 'MininumExperienceMonths' , convert(varchar(8) , 24)
UNION ALL select 2001 , 201 , 'RedLightPosition' , 'top'
UNION ALL select 2002 , 201 , 'YellowLightPosition' , 'middle'
UNION ALL select 2003 , 201 , 'GreenLightPosition' , 'bottom'

/* should fail */
/* start 
INSERT INTO [dbo].[SystemSettingCategory] ( [SystemSettingCategoryId] , [SystemSettingCategoryName] )
select 3333 , 'EmployeeSettings'
INSERT INTO [dbo].[SystemSettingCategory] ( [SystemSettingCategoryId] , [SystemSettingCategoryName] )
select 101 , 'xxxxxxxxxxxxxx'
INSERT INTO [dbo].[SystemSetting] ( [SystemSettingId] , [SystemSettingCategoryId] , [SettingKeyName] , [SettingValue] )
          select 5555 , 101 , 'MininumAgeRequirementMonths' , 555
INSERT INTO [dbo].[SystemSetting] ( [SystemSettingId] , [SystemSettingCategoryId] , [SettingKeyName] , [SettingValue] )
          select 1001 , 101 , 'yyyyyyyyyyyyyy' , 777
INSERT INTO [dbo].[SystemSetting] ( [SystemSettingId] , [SystemSettingCategoryId] , [SettingKeyName] , [SettingValue] )
          select 5555 , 555 , 'Bad FK' , 555
 end */


Select * from [dbo].[SystemSetting] where [SystemSettingCategoryId] = 101 /* employee related */
Select * from [dbo].[SystemSetting] where [SystemSettingCategoryId] = 201 /* StopLightSettings related */

现在,更进一步,您仍然可以创建具有正确数据类型的强类型 dotnet 对象,然后将您的数据读取器/数据集转换为强对象,如下所示。

public class EmployeeSettings

    public Int16 MininumAgeRequirementMonths  get; set; 
    public Int16 MininumExperienceMonths get; set; 



public class StopLightSettings

    public string RedLightPosition  get; set; 
    public string YellowLightPosition  get; set; 
    public string GreenLightPosition  get; set; 

您仍然可以使用 C# 类(或任何语言)......并使用上面的 SettingDataType 方法。 “映射”代码只需要一些额外的工作。

当没有被否决时,我使用 SettingDataType 和 C# 类,如上所示。

【讨论】:

设置并不总是恒定的。您也许可以更进一步,创建一个类型化的 GetValue(string key) 和 SetValue(string key, T value) 方法,这些方法可以反过来调用等效的存储过程,因此可以轻松检索值如果需要,在数据库中存储过程。【参考方案6】:

我认为 2 列(名称、值)的设计要好得多。正如您所说,如果您需要添加一个新属性,您需要做的就是“insert”一个新行。而在另一种设计(单行)中,您需要更改表架构来为新属性添加一列。

不过,这取决于您的属性列表是否会在未来发生变化。

【讨论】:

我必须承认,即使它存储在单独的列中,添加新属性的能力也不应该那么难。您不会手动将应用程序的数据库架构更改迁移到生产环境,是吗??【参考方案7】:

我在这里写了关于我们moved our AppSettings to a Database Table. 性能不是问题,因为它只在应用程序启动时拉取一次并存储在字典中以便于查找。

不确定您的应用程序,但我们这样做的重要原因是,如果您处于开发、测试等状态,现在不可能使用生产值。

【讨论】:

【参考方案8】:

您可以使用 XML 有效地保存配置。一些数据库支持纯 XML 功能,您可以将值保存为 xml 数据类型,并且可以在该特定列上运行 XQUERY。

创建一个包含两个列名和配置的表。名称为字符串数据类型,配置为 xml 数据类型,因此无需担心插入和删除新的配置参数,您只需在 xml 中添加一个新标签。如果数据库不支持 XML,那么只需将其保存为字符串,但保存为 XML 格式,这样您就可以手动解析该配置或有效地使用某些 API。

我认为这会比将完整配置存储为字符串更好。

【讨论】:

【参考方案9】:
CREATE TABLE Configuration (
    Name ...,
    Value ...,
);

最好的方法。向表格添加一列通常很糟糕,而只有一行的表格有什么意义?

不确定这是否适用于 SQL,但唉...问题已回答。

【讨论】:

【参考方案10】:

“最佳”完全取决于上下文——这些数据将如何使用?

如果您需要做的只是存储和检索一组配置设置,我首先会质疑关系数据库的使用——它与文件系统上的配置文件相比没有明显的好处。您不能轻松地对配置文件使用版本控制,并且管理环境差异(例如“DEV”、“TEST”和“PRODUCTION”环境)现在需要一个 GUI 来修改数据库(哦,你如何连接到首先是数据库?)。

如果您的应用程序需要对整个配置进行“推理” - 例如如果您有一个多租户解决方案并且需要根据当前系统动态配置应用程序 - 我建议将配置文件作为文本文档存储在您的数据库中,并使用允许应用程序存储/检索文档的元数据.不同的数据库引擎对存储文本文档有不同的解决方案。例如,在多租户系统中,您可能有:

ID client_id valid_from     valid_until configuration_file
-------------------------------------------------------
1         1   2016/03/16         NULL      <<DOCUMENT>>

这将允许您检索客户端 1 的文件,该文件在 3 月 3 日之后有效,并执行应用程序需要的任何操作。

如果您的应用程序需要推理配置的内容,而不是将配置本身作为一个实体,那么您就会遇到不同的问题。您提出的“名称/值”解决方案也称为实体/属性/值(EAV),有lotsofSOquestions 讨论优缺点。 TL;DR:使用 EAV 时,即使是简单的问题也很难转换为 SQL。

如果每个配置设置都是一列,并具有适当的数据类型,则查询数据会容易得多。但这确实意味着您最终会得到一个非常“宽”的表(大多数应用程序有几十个甚至数百个配置值),并且每次您想要添加配置设置时,您最终都会修改您的数据库架构,这不是不实用。

那么,另一种选择是将配置值存储为结构化文档 - XML 和 JSON 得到广泛支持。这些格式可以被数据库引擎查询,但不需要固定的模式。

【讨论】:

【参考方案11】:

这两种方法我都用过,我更喜欢 2 列方法。每个配置的新列的缺点是您需要更改代码以添加新设置。

我更喜欢使用 One column per setting 方法(当我访问值时)。这是因为配置设置更加明确。但这种偏好并没有超过向表中添加新配置的难度。

我会推荐 2 列方法。然后设置一个访问函数/sproc 来获取这些值。

【讨论】:

引入新设置往往与更改代码有关。所以我认为列并不比要添加的行差。【参考方案12】:

取决于。

如果您的值少于 15 个,我会为每个值创建一列。

如果您定期更改设置的数量,或者如果您经常不使用所有设置,我会考虑为每个设置设置一行。

除此之外,这可能是一个折腾。取决于您的使用模式。如果您总是需要获取所有设置,将它们排成一行可能是最快的。

添加列并不太难,如果您编程明智,通常不需要更新任何其他代码。

【讨论】:

您通常以小于 15 个值开始并在 15 以上结束 ;)

以上是关于应用程序配置或应用程序选项设置的最佳表设计?的主要内容,如果未能解决你的问题,请参考以下文章

为我的 Rails 应用程序创建自定义配置选项的最佳方式?

为我的 Rails 应用程序创建自定义配置选项的最佳方式?

在 Win32 Delphi 应用程序中存储用户首选项和设置的最佳实践是啥?

运行在Google应用引擎中安排的应用的最佳做法是什么?

Rails 3 应用程序/用户设置最佳实践?

为我的Rails应用程序创建自定义配置选项的最佳方法?