【问题标题】:Best method to store Enum in Database将枚举存储在数据库中的最佳方法
【发布时间】:2026-02-19 13:30:01
【问题描述】:

使用 C# 和 Visual Studio 以及 MySQL 数据连接器将枚举存储在数据库中的最佳方法是什么。

我将创建一个包含 100 多个枚举的新项目,其中大部分必须存储在数据库中。为每个转换器创建转换器将是一个漫长的过程,因此我想知道 Visual Studio 或其他人是否有任何我没有听说过的方法。

【问题讨论】:

  • +1 我对是否应该使用单独的表和 FK 约束或仅使用常规约束(如果有人有任何意见)来执行这类事情的意见感兴趣?
  • @Martin 不打算回答,因为这往往是相当主观的,但对于 OLTP/ODS,我会使用带有 FK 约束的单独表。对于 DSS 报告解决方案,我将非规范化枚举的符号名称(或描述)并将其与其他事实一起存储在报告表中。

标签: c# database visual-studio database-design enums


【解决方案1】:
    [Required]
    public virtual int PhoneTypeId
    {
        get
        {
            return (int)this.PhoneType;
        }
        set
        {
            PhoneType = (PhoneTypes)value;
        }
    }
    [EnumDataType(typeof(PhoneTypes))]
    public PhoneTypes PhoneType { get; set; }

public enum PhoneTypes
{
    Mobile = 0,
    Home = 1,
    Work = 2,
    Fax = 3,
    Other = 4
}

像魅力一样工作!无需在代码中转换 (int)Enum 或 (Enum)int。只需首先使用 enum 和 ef 代码将为您保存 int 。 p.s.: "[EnumDataType(typeof(PhoneTypes))]" 属性不是必需的,如果你想要额外的功能,只是一个额外的。

您也可以这样做:

[Required]
    public virtual int PhoneTypeId { get; set; }
    [EnumDataType(typeof(PhoneTypes))]
    public PhoneTypes PhoneType
    {
        get
        {
            return (PhoneTypes)this.PhoneTypeId;
        }
        set
        {
            this.PhoneTypeId = (int)value;
        }
    }

【讨论】:

    【解决方案2】:

    我们将我们的存储为整数或长整数,然后我们可以来回转换它们。可能不是最强大的解决方案,但我们就是这样做的。

    我们使用的是类型化数据集,例如:

    enum BlockTreatmentType 
    {
        All = 0
    };
    
    // blockTreatmentType is an int property
    blockRow.blockTreatmentType = (int)BlockTreatmentType.All;
    BlockTreatmentType btt = (BlockTreatmentType)blockRow.blocktreatmenttype;
    

    【讨论】:

    • 你如何投射它们?你存储什么?序数值,还是自定义属性?
    • 这看起来像是一个可接受的解决方案。我会把这个问题留一会儿,看看人们想出了什么。我假设您手动设置枚举的 int 值以防止顺序/基数改变?
    • 是的,我们明确设置了它,所以它不会自动改变我们。这可能是一件坏事
    【解决方案3】:

    如果您需要在数据库中存储枚举字段的字符串值,最好如下所示。 例如,如果您使用不支持枚举字段的 SQLite,则可能需要它。

    [Required]
    public string PhoneTypeAsString
    {
        get
        {
            return this.PhoneType.ToString();
        }
        set
        {
            PhoneType = (PhoneTypes)Enum.Parse( typeof(PhoneTypes), value, true);
        }
    }
    
    public PhoneTypes PhoneType{get; set;};
    
    public enum PhoneTypes
    {
        Mobile = 0,
        Home = 1,
        Work = 2,
        Fax = 3,
        Other = 4
    }
    

    【讨论】:

    • 根据您转换这些(例如 50k 个对象)的频率,您最好使用带有 switch 方法的转换器/扩展方法,因为 ToString 和 Enum.Parse 使用反射。仅供参考
    • 是的。我同意。但是对于小负载很容易解决方案是合适的。
    【解决方案4】:

    如果您想要存储所有枚举值,您可以尝试使用下表来存储枚举及其成员,并使用代码 sn-p 来添加这些值。但是,我只会在安装时执行此操作,因为在您重新编译之前,这些值永远不会改变!

    数据库表:

       create table EnumStore (
        EnumKey int NOT NULL identity primary key,
        EnumName varchar(100)
    );
    GO
    
    create table EnumMember (
        EnumMemberKey int NOT NULL identity primary key,
        EnumKey int NOT NULL,
        EnumMemberValue int,
        EnumMemberName varchar(100)
    );
    GO
    --add code to create foreign key between tables, and index on EnumName, EnumMemberValue, and EnumMemberName
    

    C# 代码段:

    void StoreEnum<T>() where T: Enum
        {
            Type enumToStore = typeof(T);
            string enumName = enumToStore.Name;
    
            int enumKey = DataAccessLayer.CreateEnum(enumName);
            foreach (int enumMemberValue in Enum.GetValues(enumToStore))
            {
                string enumMemberName = Enum.GetName(enumToStore, enumMemberValue);
                DataAccessLayer.AddEnumMember(enumKey, enumMemberValue, enumMemberName);
            }
        }
    

    【讨论】:

      【解决方案5】:

      最后,您将需要一种很好的方法来处理重复的编码任务,例如枚举转换器。您可以使用代码生成器(例如 MyGenerationCodeSmith 等)或ORM mapper like nHibernate 为您处理所有事情。

      至于结构...有数百个枚举,我首先会考虑尝试将数据组织到一个可能看起来像这样的表中:(伪 sql)

      MyEnumTable(
      EnumType as int,
      EnumId as int PK,
      EnumValue as int )
      

      这将允许您将枚举信息存储在单个表中。 EnumType 也可以是定义不同枚举的表的外键。

      您的 biz 对象将通过 EnumId 链接到此表。枚举类型仅用于 UI 中的组织和过滤。使用所有这些当然取决于您的代码结构和问题域。

      顺便说一句,在这种情况下,您可能希望在 EnumType 上设置一个聚集索引,而不是保留在 PKey 上创建的默认集群 idx。

      【讨论】:

      • 有趣的枚举存储方式。但是我想使用 Enums(基于 c#)的主要方式是为了可读性,例如。 if (something.Type == Enum.EnumType) 这个项目将是一个非常长且复杂的项目,并且有 100 个枚举,每次都很难跟踪并返回数据库。
      • 您不使用复合主键(EnumType, EnumId)(其默认聚集索引以EnumType 开头)的原因是什么?否则,当您的第二个、第三个、第四个等...枚举的 ID 不是从 1 开始时,这似乎很随意,如果您向枚举添加更多值,则 ID 中会出现间隙。例如,如果您使用初始值“bad”=6、“poor”=7、“ok”=8、“good”=9、“great”=10 为“quality”定义枚举类型 2,则将枚举类型 3 用于"status" 与 "on"=11, "off"=12,然后你想在你的 "quality" 类型中添加 "great",它会得到 id 13。
      【解决方案6】:

      有些事情你应该考虑。

      枚举列是否将被其他应用程序(例如报告)直接使用。这将限制枚举以整数格式存储的可能性,因为该值在报告中出现时将没有任何意义,除非报告具有自定义逻辑。

      您的应用程序需要哪些 i18n?如果它只支持一种语言,您可以将枚举保存为文本并创建一个辅助方法来从描述字符串进行转换。您可以为此使用[DescriptionAttribute],并且可以通过搜索SO找到转换方法。

      另一方面,如果您需要支持多种语言和外部应用程序访问您的数据,您可以开始考虑枚举是否真的是答案。如果场景更复杂,可以考虑使用查找表等其他选项。

      枚举在代码中自包含时非常好......当它们越过边界时,事情往往会变得有点混乱。


      更新:

      您可以使用Enum.ToObject 方法从整数转换。这意味着您在转换时知道枚举的类型。如果要使其完全通用,则需要将枚举的类型与其值一起存储在数据库中。您可以创建数据字典支持表来告诉您哪些列是枚举以及它们是什么类型。

      【讨论】:

      • 理想情况下,报告将使用保存枚举的同一库构建,因此将整数存储在数据库中非常好。唯一的问题是,当您从数据库中提取时,您如何转换为原始枚举? (100+ 个枚举,每个枚举有 5+ 个值)
      【解决方案7】:

      如果你想存储整数,你不需要做任何事情。只需在 EF 中映射您的属性。 如果您想将它们存储为字符串,请使用转换器。

      Int(db 类型为 smallint):

      public override void Configure(EntityTypeBuilder<MyEfEntity> b)
      {
          ...
          b.Property(x => x.EnumStatus);
      }
      

      字符串(db 类型为 varchar(50)):

      public override void Configure(EntityTypeBuilder<MyEfEntity> b)
      {
          ...
          b.Property(x => x.EnumStatus).HasConversion<EnumToStringConverter>();
      }
      

      如果您想保存您的 db 数据使用情况,请使用 smallint 作为 db 中的列。但是数据不会是人类可读的,您应该为每个枚举项设置一个索引,并且永远不要弄乱它们:

      public enum EnumStatus
      {
          Active = 0, // Never change this index
          Archived = 1, // Never change this index
      }
      

      如果您想让 db 中的数据更具可读性,您可以将它们保存为字符串(例如 varchar(50))。您不必担心索引,当您更改枚举名称时,您只需要更新 db 中的字符串。缺点:列大小使数据使用成本更高。这意味着如果您的表在 1,000,000 行内,​​它可能会对数据库大小和性能产生影响。

      您也可以使用短枚举名称作为解决方案:

      public enum EnumStatus
      {
          [Display(Name = "Active")]
          Act,
          [Display(Name = "Archived")]
          Arc,
      }
      

      或者使用您自己的转换器来缩短 db 中的名称:

      public enum EnumStatus
      {
          [Display(Name = "Active", ShortName = "Act")]
          Active,
          [Display(Name = "Archived", ShortName = "Arc")]
          Archived,
      }
      ...
      public override void Configure(EntityTypeBuilder<MyEfEntity> b)
      {
          ...
          b.Property(x => x.EnumStatus).HasConversion<MyShortEnumsConverter>();
      }
      

      更多信息可以在这里找到: 英孚:https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/data-types/enums EFCore:https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions

      【讨论】:

        【解决方案8】:

        我不确定它是否最灵活,但您可以简单地存储它们的字符串版本。它当然是可读的,但可能难以维护。枚举很容易从字符串转换回来:

        public enum TestEnum
        {
            MyFirstEnum,
            MySecondEnum
        }
        
        static void TestEnums()
        {
            string str = TestEnum.MyFirstEnum.ToString();
            Console.WriteLine( "Enum = {0}", str );
            TestEnum e = (TestEnum)Enum.Parse( typeof( TestEnum ), "MySecondEnum", true );
            Console.WriteLine( "Enum = {0}", e );
        }
        

        【讨论】:

          【解决方案9】:

          为什么不尝试将枚举与数据库完全分开呢?在从事类似工作时,我发现这篇文章是一个很好的参考:

          http://stevesmithblog.com/blog/reducing-sql-lookup-tables-and-function-properties-in-nhibernate/

          无论您使用什么数据库,其中的想法都应该适用。例如,在 MySQL 中,您可以使用“enum”数据类型来强制遵守您的编码枚举:

          http://dev.mysql.com/doc/refman/5.0/en/enum.html

          干杯

          【讨论】:

            【解决方案10】:

            可以通过为 Id 列名称与表名称匹配的每个枚举创建一致的表来使用 DB 优先方法。在数据库中使用枚举值来支持外键约束和视图中的友好列是有利的。我们目前支持分散在众多版本化数据库中的约 100 种枚举类型。

            对于 Code-First 偏好,可能会反转下面显示的 T4 策略以写入数据库。

            create table SomeSchema.SomeEnumType (
              SomeEnumTypeId smallint NOT NULL primary key,
              Name varchar(100) not null,
              Description nvarchar(1000),
              ModifiedUtc datetime2(7) default(sysutcdatetime()),
              CreatedUtc datetime2(7) default(sysutcdatetime()),
            );
            

            可以使用T4 template (*.tt) script 将每个表导入C#。

            1. 创建一个“枚举项目”。添加如下所示的 .tt 文件。
            2. 为每个数据库模式名称创建一个子文件夹。
            3. 为每个枚举类型创建一个名为 SchemaName.TableName.tt 的文件。文件 内容总是相同的单行:
            4. 然后要创建/更新枚举,右键单击 1 个或多个文件并 “运行自定义工具”(我们还没有自动更新)。它将向项目添加/更新一个 .cs 文件:
            使用 System.CodeDom.Compiler;
            命名空间 TheCompanyNamespace.Enumerations.Config
            {
                [GeneratedCode("从 DB 生成器自动枚举", "10")]
                公共枚举 DatabasePushJobState
                {
                      未定义 = 0,
                      已创建 = 1,
                }
                公共部分类 EnumDescription
                {
                   公共静态字符串描述(DatabasePushJobState 枚举)
                   {
                      字符串描述=“未知”;
                      开关(枚举)
                      {
                          案例 DatabasePushJobState.Undefined:
                              描述=“未定义”;
                              休息;
            
                          案例 DatabasePushJobState.Created:
                              描述=“创建”;
                              休息;
                       }
                       返回说明;
                   }
                }
                // 选择 DatabasePushJobStateId, Name, coalesce(Description,Name) 作为描述
                // 来自 TheDefaultDatabase.[SchName].[DatabasePushJobState]
                // 其中 1=1 按 DatabasePushJobStateId 排序
             }
            

            最后,有点粗糙的 T4 脚本(从众多变通方法中简化)。它需要根据您的环境进行定制。调试标志可以将消息输出到 C# 中。右键单击 .tt 文件时,还有一个“调试 T4 模板”选项。 EnumGenerator.ttinclude

            <#@ template debug="true" hostSpecific="true" #>
            <#@ output extension=".generated.cs" #>
            <#@ Assembly Name="EnvDTE" #>
            <#@ Assembly Name="System.Core" #>
            <#@ Assembly Name="System.Data" #>
            <#@ assembly name="$(TargetPath)" #>
            <#@ import namespace="EnvDTE" #>
            <#@ import namespace="System" #>
            <#@ import namespace="System.Collections" #>
            <#@ import namespace="System.Collections.Generic" #>
            <#@ import namespace="System.Data" #>
            <#@ import namespace="System.Data.SqlClient" #>
            <#@ import namespace="System.IO" #>
            <#@ import namespace="System.Text.RegularExpressions" #>
            <#  
                bool doDebug = false;   // include debug statements to appear in generated output    
            
                string schemaTableName = Path.GetFileNameWithoutExtension(Host.TemplateFile);
                string schema = schemaTableName.Split('.')[0];
                string tableName = schemaTableName.Split('.')[1];
            
                string path = Path.GetDirectoryName(Host.TemplateFile);    
                string enumName = tableName;
                string columnId = enumName + "Id";
                string columnName = "Name"; 
                string columnDescription = "Description";
            
                string currentVersion = CompanyNamespace.Enumerations.Constants.Constants.DefaultDatabaseVersionSuffix;
            
                // Determine Database Name using Schema Name
                //
                Dictionary<string, string> schemaToDatabaseNameMap = new Dictionary<string, string> {
                    { "Cfg",        "SomeDbName" + currentVersion },
                    { "Common",     "SomeOtherDbName" + currentVersion }
                    // etc.     
                };
            
                string databaseName;
                if (!schemaToDatabaseNameMap.TryGetValue(schema, out databaseName))
                {
                    databaseName = "TheDefaultDatabase"; // default if not in map
                }
            
                string connectionString = @"Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=" + databaseName + @";Data Source=Machine\Instance";
            
                schema = "[" + schema + "]";
                tableName = "[" + tableName + "]";
            
                string whereConstraint = "1=1";  // adjust if needed for specific tables
            
              // Get containing project
              IServiceProvider serviceProvider = (IServiceProvider)Host;
              DTE dte = (DTE)serviceProvider.GetService(typeof(DTE));
              Project project = dte.Solution.FindProjectItem(Host.TemplateFile).ContainingProject;
            #>
            using System;
            using System.CodeDom.Compiler;
            
            namespace <#= project.Properties.Item("DefaultNamespace").Value #><#= Path.GetDirectoryName(Host.TemplateFile).Remove(0, Path.GetDirectoryName(project.FileName).Length).Replace("\\", ".") #>
            {
                /// <summary>
                /// Auto-generated Enumeration from Source Table <#= databaseName + "." + schema + "." + tableName #>.  Refer to end of file for SQL.
                /// Please do not modify, your changes will be lost!
                /// </summary>
                [GeneratedCode("Auto Enum from DB Generator", "10")]
                public enum <#= enumName #>
                {       
            <#
                    SqlConnection conn = new SqlConnection(connectionString);
                    // Description is optional, uses name if null
                    string command = string.Format(
                        "select {0}, {1}, coalesce({2},{1}) as {2}" + "\n  from {3}.{4}.{5}\n where {6} order by {0}", 
                            columnId,           // 0
                            columnName,         // 1
                            columnDescription,  // 2
                            databaseName,       // 3
                            schema,             // 4
                            tableName,          // 5
                            whereConstraint);   // 6
                    #><#= DebugCommand(databaseName, command, doDebug) #><#
            
                    SqlCommand comm = new SqlCommand(command, conn);
            
                    conn.Open();
            
                    SqlDataReader reader = comm.ExecuteReader();
                    bool loop = reader.Read();
            
                    while(loop)
                    {
            #>      /// <summary>
                    /// <#= reader[columnDescription] #>
                    /// </summary>
                    <#= Pascalize(reader[columnName]) #> = <#= reader[columnId] #><# loop = reader.Read(); #><#= loop ? ",\r\n" : string.Empty #>
            <#
                    }
            #>    }
            
            
                /// <summary>
                /// A helper class to return the Description for each enumeration value
                /// </summary>
                public partial class EnumDescription
                {
                    public static string Description(<#= enumName #> enumeration)
                    {
                        string description = "Unknown";
            
                        switch (enumeration)
                        {<#
                conn.Close();
                conn.Open();
                reader = comm.ExecuteReader();
                loop = reader.Read();
            
                while(loop)
                {#>                 
                                case <#= enumName #>.<#= Pascalize(reader[columnName]) #>:
                                    description = "<#= reader[columnDescription].ToString().Replace("\"", "\\\"") #>";
                                    break;
                                <# loop = reader.Read(); #>
            <#
                  }
                  conn.Close();
            #> 
                        }
            
                        return description;
                    }
                }
                /*
                    <#= command.Replace("\n", "\r\n        ") #>
                */
            }
            <#+     
                private string Pascalize(object value)
                {
                    Regex rxStartsWithKeyWord = new Regex(@"^[0-9]|^abstract$|^as$|^base$|^bool$|^break$|^byte$|^case$|^catch$|^char$|^checked$|^class$|^const$|^continue$|^decimal$|^default$|^delegate$|^do$|^double$|^else$|^enum$|^event$|^explicit$|^extern$|^$false|^finally$|^fixed$|^float$|^for$|^foreach$|^goto$|^if$|^implicit$|^in$|^int$|^interface$|^internal$|^is$|^lock$|^long$|^namespace$|^new$|^null$|^object$|^operator$|^out$|^overrride$|^params$|^private$|^protected$|^public$|^readonly$|^ref$|^return$|^sbyte$|^sealed$|^short$|^sizeof$|^stackalloc$|^static$|^string$|^struct$|^switch$|^this$|^thorw$|^true$|^try$|^typeof$|^uint$|^ulong$|^unchecked$|^unsafe$|^ushort$|^using$|^virtual$|^volatile$|^void$|^while$", RegexOptions.Compiled);
            
                    Regex rx = new Regex(@"(?:[^a-zA-Z0-9]*)(?<first>[a-zA-Z0-9])(?<reminder>[a-zA-Z0-9]*)(?:[^a-zA-Z0-9]*)");
                    string rawName = rx.Replace(value.ToString(), m => m.Groups["first"].ToString().ToUpper() + m.Groups["reminder"].ToString());
            
                    if (rxStartsWithKeyWord.Match(rawName).Success)
                        rawName =  "_" + rawName;
            
                    return rawName;    
                }
            
                private string DebugCommand(string databaseName, string command, bool doDebug)
                {       
                    return doDebug
                        ? "        // use " + databaseName + ";  " + command + ";\r\n\r\n"
                        : "";
                }   
            #>
            

            希望实体框架有一天能够支持这些答案的组合,以在记录中提供 C# 枚举强类型和值的数据库镜像。

            【讨论】: