簡體   English   中英

將枚舉存儲在數據庫中的最佳方法

[英]Best method to store Enum in Database

使用 C# 和 Visual Studio 以及 MySQL 數據連接器將枚舉存儲在數據庫中的最佳方法是什么。

我將創建一個包含 100 多個枚舉的新項目,其中大部分必須存儲在數據庫中。 為每個轉換器創建轉換器將是一個漫長的過程,因此我想知道 Visual Studio 或其他人是否有任何我沒有聽說過的方法。

    [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 。 ps:“[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;
        }
    }

我們將我們的存儲為整數或長整數,然后我們可以來回轉換它們。 可能不是最強大的解決方案,但這是我們所做的。

我們正在使用類型化的數據集,例如:

enum BlockTreatmentType 
{
    All = 0
};

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

如果您需要將枚舉字段的 DB 字符串值存儲在數據庫中,最好如下所示。 例如,如果您使用不支持枚舉字段的 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
}

如果您想要存儲所有枚舉值,您可以嘗試使用下表來存儲枚舉及其成員,以及添加這些值的代碼片段。 但是,我只會在安裝時這樣做,因為在您重新編譯之前,這些值永遠不會改變!

數據庫表:

   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);
        }
    }

最后,您將需要一種很好的方法來處理重復的編碼任務,例如枚舉轉換器。 您可以使用諸如MyGenerationCodeSmith之類的代碼生成器,或者使用諸如 nHibernate 之類的ORM 映射器來為您處理所有事情。

至於結構...有數百個枚舉,我首先會考慮嘗試將數據組織到一個可能看起來像這樣的表中:(偽 sql)

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

這將允許您將枚舉信息存儲在一個表中。 EnumType 也可以是定義不同枚舉的表的外鍵。

您的 biz 對象將通過 EnumId 鏈接到此表。 枚舉類型僅用於 UI 中的組織和過濾。 當然,利用所有這些取決於您的代碼結構和問題域。

順便說一句,在這種情況下,您可能希望在 EnumType 上設置聚集索引,而不是保留在 PKey 上創建的默認集群 idx。

有些事情你應該考慮。

枚舉列是否將被其他應用程序(例如報告)直接使用。 這將限制枚舉以整數格式存儲的可能性,因為該值在報告中出現時將沒有任何意義,除非報告具有自定義邏輯。

您的應用程序需要哪些 i18n? 如果它只支持一種語言,您可以將枚舉保存為文本並創建一個輔助方法來從描述字符串轉換。 您可以為此使用[DescriptionAttribute] ,並且可以通過搜索 SO 找到轉換方法。

另一方面,如果您需要支持多種語言和外部應用程序訪問您的數據,您可以開始考慮枚舉是否真的是答案。 如果場景更復雜,可以考慮使用查找表等其他選項。

枚舉在代碼中自包含時非常好……當它們越過邊界時,事情往往會變得有點混亂。


更新:

您可以使用Enum.ToObject方法從整數轉換。 這意味着您在轉換時知道枚舉的類型。 如果要使其完全通用,則需要將枚舉的類型與其值一起存儲在數據庫中。 您可以創建數據字典支持表來告訴您哪些列是枚舉以及它們是什么類型。

如果你想存儲整數,你不需要做任何事情。 只需在 EF 中映射您的屬性。 如果要將它們存儲為字符串,請使用轉換器。

Int(數據庫類型為 smallint):

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

字符串(數據庫類型為 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>();
}

更多信息可以在這里找到:EF: 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

我不確定它是否最靈活,但您可以簡單地存儲它們的字符串版本。 它當然是可讀的,但可能難以維護。 枚舉很容易從字符串轉換回來:

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 );
}

為什么不嘗試將枚舉與數據庫完全分開呢? 在從事類似工作時,我發現這篇文章是一個很好的參考:

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

干杯

可以通過為 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 模板 (*.tt) 腳本將每個表導入 C#。

  1. 創建一個“枚舉項目”。 添加如下所示的 .tt 文件。
  2. 為每個數據庫模式名稱創建一個子文件夾。
  3. 對於每個枚舉類型,創建一個名為 SchemaName.TableName.tt 的文件。 文件內容總是相同的單行:<#@ include file="..\EnumGenerator.ttinclude" #>
  4. 然后創建/更新枚舉,右鍵單擊 1 個或多個文件和“運行自定義工具”(我們還沒有自動更新)。 它將向項目添加/更新一個 .cs 文件:
using System.CodeDom.Compiler;
namespace TheCompanyNamespace.Enumerations.Config
{
    [GeneratedCode("Auto Enum from DB Generator", "10")]
    public enum DatabasePushJobState
    {     
          Undefined = 0,
          Created = 1,        
    } 
    public partial class EnumDescription
    {
       public static string Description(DatabasePushJobState enumeration)
       {
          string description = "Unknown";
          switch (enumeration)
          {                   
              case DatabasePushJobState.Undefined:
                  description = "Undefined";
                  break;

              case DatabasePushJobState.Created:
                  description = "Created";
                  break;                 
           }
           return description;
       }
    }
    // select DatabasePushJobStateId, Name, coalesce(Description,Name) as Description
    //    from TheDefaultDatabase.[SchName].[DatabasePushJobState]
    //   where 1=1 order by 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# 枚舉強類型和值的數據庫鏡像。

如果您使用的是 EntityFrameworkCore。 (我的版本是 5.0.10),而不是像這樣直接使用 Fluent API:(這會將 Home/Other 保存在 DB 中。在數據庫中創建Type列作為 varchar/nvarchar)

在您繼承 DbContext 的上下文文件中

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
            modelBuilder
                .Entity<StudentAddress>()
                .Property(address => address.Type)
                .HasConversion(
                    value => value.ToString(),
                    value => (AddressType)Enum.Parse(typeof(AddressType), value));
}    

我的實體:

[Table("Student", Schema = "Student")]
public class Student 
{
    //...
    public AddressType Type { get; set; }
}

我的枚舉:

    public enum AddressType
    {
        Home,
        Other
    }

額外說明,非強制性且與上述示例無關:在我的這種情況下,這不是必需的。 但是您始終可以在需要時在數據庫級別創建約束。

    ALTER TABLE [Document].[Document] WITH CHECK ADD  CONSTRAINT [CHK_DocumentType] CHECK  (([DocumentType]="DOC" OR [DocumentType]="PDF" OR [DocumentType]="IMAGE" OR [DocumentType]="OTHER"))

    ALTER TABLE [Document].[Document] CHECK CONSTRAINT [CHK_DocumentType]

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM