如何使用c#数据库中的值创建ENUM

本文关键字:创建 ENUM 何使用 数据库 | 更新日期: 2023-09-27 18:14:06

我在数据库中有一个状态表,我在整个应用程序中使用它的值。状态表将有(ID, NAME)。我想创建一个StatusEnum,我可以在我的代码中使用的应用程序。如何使用数据库中的值创建ENUM ?

目前我有这样的enum

enum StatusCode: int
{
    Open = 20,
    Received = 21,
    Delivered= 22,
    Cancelled = 23
}

但是我想从数据库中设置值。

如何使用c#数据库中的值创建ENUM

你可以

  • 手动保持枚举定义与数据库同步。

  • 您可以为Visual Studio编写单个文件生成器(也称为"自定义工具"),并从某些参考数据库生成枚举定义。单文件生成器获取一些源文件(例如,*.aspx)并从中生成代码(例如,*.designer.cs)。这个功能非常有用

  • 第三种技术是前两种技术的雌雄同体变体:编写一个独立的工具来从数据库生成枚举定义。

  • 在参考数据库上运行该工具以重新生成文件并将其检入。

无论哪种方式,都不可能在不影响应用程序的情况下随意更改数据库中的查找表。新添加的值将不为应用程序所知;删除或更改的值可能会破坏东西。

但是可以推测,你想要枚举的类型都是相对稳定的类型。

我要试着回答。

如果你的想法是在你的数据库中有神奇的数字,这些数字本质上是const,这意味着它们很少改变,我可以理解为什么你想要将它们表示为代码中可读的东西。

根据您查询数据的方式,有不同的将该数据表示为enum值的方法:对于SqlDataReader,只需将来自记录的整数值向上转换为enum:

int statusCodeValue = row["status"];
// But BEWARE as undefined values also upcast to the enum just fine
// but just won't have any of the defined values
if (!Enum.IsDefined(typeof(StatusCode), statusCodeValue)) throw new Exception();
StatusCode statusCode = (StatusCode)statusCodeValue;

如果你使用的是像nHibernate或EF这样的ORM,你可以得到这个映射。只需在数据模型上定义enum属性,它就会正确映射。

注意

你需要考虑谁是数据的所有者。这些数据是来自另一个系统,还是您的应用程序生成的?

如果它是外部的,您需要手动保持数据和应用程序枚举同步。如果它是本地的,只需确保为每个enum成员分配一个整数值,并且永远不要改变该整数值在语义上应该表示的内容。

你真的不能这么做。您将需要一些辅助函数来帮助您。

enum StatusCode
{
   Open,
   Received,
   Delivered,
   Cancelled
}
private Dictionary<StatusCode, int> storedCodes = new Dictionary<StatusCode, int>();
public static int GetValue(StatusCode code)
{
    //Return code from database
    return storedCodes[code];
}
public static void SetValue(StatusCode code, int value)
{
    storedCodes[code] = value;
    //Set values from database
    //Note:  you can't set the value of your enums here
    //Just a place to set some other variables to remember what the values are.
}

你所问的在技术上是可能的文本模板(一个例子是在折叠后)。然而,正如已经指出的那样,好的文本模板并不容易,通常需要与应用程序代码一样多的维护。

相反,查看并确定Enum 是否真的是所有可能状态的约束列表。如果您将状态存储在数据库表中,我会说它不是。在表中,这些值在技术上可能会发生变化(可以插入新值,删除旧值,甚至更改名称,使其不再反映其原始含义)。只有用户约定才能防止这种情况,而且在应用程序代码中不会立即显示出来。

你最好重新考虑你的数据库设计。下一个语句对我来说很难解释,但我不喜欢根据子关系推断实体状态。相反,我更喜欢根据给定数据行的单个列值做出代码决策。

例如,而不是两个表,如:

CREATE TABLE status_codes (
  key INT PRIMARY KEY, 
  value VARCHAR(32)
);
CREATE TABLE entity (
  id INT PRIMARY KEY, 
  name VARCHAR(32), 
  status INT 
  CONSTRAINT fk_status FOREIGN KEY status REFERENCES status_codes(id)
);
INSERT INTO status_codes 
VALUES (20, 'Open')
      ,(21, 'Received')
      ,(22, 'Delivered')
      ,(23, 'Cancelled');

我更喜欢:

CREATE TABLE entity (
  id INT,
  name VARCHAR(32),
  is_received BIT NOT NULL,
  is_delivered BIT NOT NULL,
  is_cancelled BIT NOT NULL,
  is_closed BIT NOT NULL
);

对于我来说,我发现在没有幻数的情况下更容易编写逻辑代码。

这样你就不用再做这样的事情了:

/* SQL */
SELECT * FROM entity WHERE status = 20;
/* C# */
if (entity.Status == StatusCodes.Open) { /* do something */ }
if (entity.Status == 20) { /* do something */ }

而不是:

/* SQL */
SELECT * FROM entity WHERE is_closed = 0;
/* C# */
if (!entity.IsClosed) { /* do something */ }

你也可以在过程中使用数据库约束(例如,确保一个项目在交付之前不能标记为已接收,并且如果它已经交付则不能取消)。

我还将'is open'语义更改为'is closed',但这只是个人偏好,使做某事(即关闭)比不做任何事情(即打开)更重要。

我还注意到,有时候你确实需要"软"的、用户可维护的状态。然而,我建议这些应该仅用于显示目的,并且您不应该围绕这些"软"状态编写"硬"代码。

(如果您的应用程序打算成为一个高度可定制的现成产品,您可能会考虑使软状态对脚本或规则引擎可用,因此定义状态的用户也可以定义围绕它们的业务规则,但这远远超出了范围。)


说了这么多,如果你真的,真的觉得你需要做你所要求的,一个可能的方法是…

首先,您需要枚举值的规范源。要么你的c# Enum是正确的,要么SQL定义是正确的。

一旦确定了这一点,我可能会使用某种形式的文本模板,如T4或自定义脚本文件/编译的exe来生成不是的规范源。

c#代码是规范的

使用Enum.GetValues(typeof(StatusCode))Enum.GetName(typeof(StatusCode), value)反映Enum,然后使用它在目标表中生成CHECK约束。将基本int类型存储在数据库表中(例如ALTER TABLE my_table ADD status_code INT)。

// (untested, pseudo-ish code -- WATCH FOR INJECTION!)
StringBuilder sql = new StringBuilder();
sql.Append(@"ALTER TABLE [my_table] ADD CONSTRAINT chk_status_code CHECK (status_code IN (");
bool first = true;
foreach (var v in Enum.GetValues(typeof(StatusCode)) {
    if (!first) sql.Append(", ");
    sql.Append(v);
    first = false;
}
sql.Append("));");
// write sql to file, or run against the development database

这将使您非常接近可以在构建/安装时运行的SQL语句。请注意,这不是在数据库的正常操作期间运行的。

如果您需要该功能,您可能还需要生成一个内联表函数来映射数字到名称,例如:

// (untested, pseudo-ish code -- WATCH FOR INJECTION!)
StringBuilder sql = new StringBuilder();
sql.AppendLine(@"IF OBJECT_ID('dbo.tf_status_codes') IS NULL EXECUTE('
                     CREATE FUNCTION dbo.tf_status_codes RETURNS TABLE AS RETURN (
                         SELECT ''not yet built'' AS err
                     )
                 ')");
sql.AppendLine(@"ALTER FUNCTION dbo.tf_status_codes RETURNS TABLE AS RETURN (")
   .AppendLine(@"  SELECT value, name FROM (VALUES ")
bool first = true;
foreach (var v in Enum.GetValues(typeof(StatusCode)) {
    if (!first) sql.AppendLine(",");
    sql.Append(@"    ({0}, '{1}')", 
               v,
               Enum.GetName(typeof(StatusCode), v));
    first = false;
}
sql.AppendLine(@"  ) e(value, name);")
   .AppendLine(@")";
// write sql to file, or run against the development database

将构建工具作为Post-Build事件运行,以便在约束/表之前更新代码。

SQL表是规范的

这个更直接,尽管我会确保表源在生产迭代的生命周期内不能更改(即仅在部署时定义)。为此,我将把枚举值定义为内联表函数,而不是表:

CREATE FUNCTION dbo.status_codes 
RETURNS TABLE 
AS RETURN (
    SELECT value, name
    FROM (VALUES (20, 'Open')
                ,(21, 'Received')
                ,(22, 'Delivered')
                ,(23, 'Cancelled')) AS v(value, name)
)

然后,在我的构建工具中,连接到数据库,检索值并生成enum:

// untested, pseudo, assumes an existing database connection routine 
IDataReader reader = DB.GetReader("SELECT value, name FROM dbo.status_codes()");
StringBuilder code = new StringBuilder();
code.AppendLine("namespace MyApp {")
    .AppendLine("  public enum StatusCodes : int {");
bool first = true
while (reader.Read()) {
    if (!first) code.AppendLine(",");
    code.Append("    {0} = {1}", reader["name"], reader["value"]);
    first = false;
}
code.AppendLine("  }")
    .AppendLine("}");
// ...write the code to the Enum class file, and exit with 0 code

将构建工具作为预构建事件运行(因此代码在构建之前生成)。

(正如我所说的,上面的代码是未经测试的,并且没有尝试为注入保护它。请自行承担风险,并彻底测试)