【问题标题】:Ways to save enums in database在数据库中保存枚举的方法
【发布时间】:2010-09-18 19:04:33
【问题描述】:

将枚举保存到数据库中的最佳方法是什么?

我知道 Java 提供了 name()valueOf() 方法来将枚举值转换为字符串并返回。但是还有其他(灵活的)选项来存储这些值吗?

有没有一种聪明的方法可以将枚举变成唯一的数字(ordinal() 使用不安全)?

更新:

感谢所有出色而快速的答案!正如我所怀疑的那样。

但是对“工具包”的说明;这是一种方式。问题是我必须向我创建的每个 Enum 类型添加相同的方法。那是很多重复的代码,目前,Java 不支持任何解决方案(Java 枚举不能扩展其他类)。

【问题讨论】:

  • 为什么使用 ordinal() 不安全?
  • 什么样的数据库? MySQL 有一个枚举类型,但我不认为它是标准的 ANSI SQL。
  • 因为任何枚举加法都必须放在最后。毫无戒心的开发人员很容易把事情搞砸并造成严重破坏
  • 我明白了。我想我很少处理数据库是件好事,因为我可能不会想到这一点,直到为时已晚。

标签: java database enums


【解决方案1】:

我认为这里唯一安全的机制是使用字符串name() 值。写入数据库时​​,可以使用存储过程插入值,读取时,使用视图。以这种方式,如果枚举发生变化,则存储过程/视图中存在一定程度的间接性,以便能够将数据呈现为枚举值,而不会将其“强加”在 DB 上。

【讨论】:

  • 我正在使用您的解决方案和@Ian Boyd 的解决方案的混合方法,取得了巨大成功。感谢您的提示!
【解决方案2】:

我们不再将枚举存储为数字序数值;它使调试和支持变得太困难了。我们存储实际的枚举值转换为字符串:

public enum Suit { Spade, Heart, Diamond, Club }

Suit theSuit = Suit.Heart;

szQuery = "INSERT INTO Customers (Name, Suit) " +
          "VALUES ('Ian Boyd', %s)".format(theSuit.name());

然后回读:

Suit theSuit = Suit.valueOf(reader["Suit"]);

问题是过去盯着 Enterprise Manager 并试图破译:

Name          Suit
------------  ----
Kylie Guénin  2
Ian Boyd      1

诗句

Name          Suit
------------  -------
Kylie Guénin  Diamond
Ian Boyd      Heart

后者更容易。前者需要获取源代码并找到分配给枚举成员的数值。

是的,它需要更多空间,但枚举成员名称很短,而且硬盘很便宜,而且在遇到问题时提供帮助更值得。

此外,如果您使用数值,您会被绑定到它们。您不能很好地插入或重新排列成员,而不必强制使用旧数值。例如,将 Suit 枚举更改为:

public enum Suit { Unknown, Heart, Club, Diamond, Spade }

必须变成:

public enum Suit { 
      Unknown = 4,
      Heart = 1,
      Club = 3,
      Diamond = 2,
      Spade = 0 }

为了维护存储在数据库中的旧数值。

如何在数据库中排序

问题出现了:假设我想订购这些值。有些人可能想按enum 的序数值对它们进行排序。当然,按枚举的数值排序卡片是没有意义的:

SELECT Suit FROM Cards
ORDER BY SuitID; --where SuitID is integer value(4,1,3,2,0)

Suit
------
Spade
Heart
Diamond
Club
Unknown

这不是我们想要的顺序 - 我们希望它们按枚举顺序:

SELECT Suit FROM Cards
ORDER BY CASE SuitID OF
    WHEN 4 THEN 0 --Unknown first
    WHEN 1 THEN 1 --Heart
    WHEN 3 THEN 2 --Club
    WHEN 2 THEN 3 --Diamond
    WHEN 0 THEN 4 --Spade
    ELSE 999 END

如果保存字符串,则需要进行与保存整数值相同的工作:

SELECT Suit FROM Cards
ORDER BY Suit; --where Suit is an enum name

Suit
-------
Club
Diamond
Heart
Spade
Unknown

但这不是我们想要的顺序 - 我们希望它们按枚举顺序:

SELECT Suit FROM Cards
ORDER BY CASE Suit OF
    WHEN 'Unknown' THEN 0
    WHEN 'Heart'   THEN 1
    WHEN 'Club'    THEN 2
    WHEN 'Diamond' THEN 3
    WHEN 'Space'   THEN 4
    ELSE 999 END

我的看法是,这种排名属于用户界面。如果您根据枚举值对项目进行排序:您做错了。

但如果你真的想这样做,我会创建一个Suitsdimension table

Suit SuitID Rank Color
Unknown 4 0 NULL
Heart 1 1 Red
Club 3 2 Black
Diamond 2 3 Red
Spade 0 4 Black

这样,当您想将卡片更改为使用Kissing Kings New Deck Order 时,您可以将其更改为显示目的,而不会丢弃所有数据:

Suit SuitID Rank Color CardOrder
Unknown 4 0 NULL NULL
Spade 0 1 Black 1
Diamond 2 2 Red 1
Club 3 3 Black -1
Heart 1 4 Red -1

现在我们将内部编程细节(枚举名称、枚举值)与用户的显示设置分开:

SELECT Cards.Suit 
FROM Cards
   INNER JOIN Suits ON Cards.Suit = Suits.Suit
ORDER BY Suits.Rank, 
   Card.Rank*Suits.CardOrder
    

【讨论】:

  • toString 通常被覆盖以提供显示值。 name() 是更好的选择,因为它定义为 valueOf() 的对应项
  • 我强烈不同意这一点,如果需要枚举持久性,那么不应该持久化名称。就读回来而言,使用 value 而不是 name 更简单,可以将其类型转换为 SomeEnum enum1 = (SomeEnum)2;
  • mamu:当数值等价物改变时会发生什么?
  • 我会劝阻任何使用这种方法的人。将自己束缚于字符串表示会限制代码的灵活性和重构。您应该更好地使用唯一 ID。存储字符串也会浪费存储空间。
  • @LuisGouveia 我同意你的观点,时间可以翻倍。导致采用12.37 ms 的查询改为采用12.3702 ms。这就是我所说的“在噪音中”。您再次运行查询,它需要13.29 ms11.36 ms。换句话说,线程调度程序的随机性将彻底淹没理论上任何人都无法以任何方式看到的任何微优化。
【解决方案3】:

正如你所说,序数有点冒险。例如:

public enum Boolean {
    TRUE, FALSE
}

public class BooleanTest {
    @Test
    public void testEnum() {
        assertEquals(0, Boolean.TRUE.ordinal());
        assertEquals(1, Boolean.FALSE.ordinal());
    }
}

如果您将其存储为序数,您可能会有如下行:

> SELECT STATEMENT, TRUTH FROM CALL_MY_BLUFF

"Alice is a boy"      1
"Graham is a boy"     0

但是如果你更新布尔值会发生什么?

public enum Boolean {
    TRUE, FILE_NOT_FOUND, FALSE
}

这意味着你所有的谎言都会被误解为“找不到文件”

最好只使用字符串表示

【讨论】:

    【解决方案4】:

    我们只存储枚举名称本身 - 它更具可读性。

    我们确实在为存在一组有限值的枚举存储特定值方面搞砸了,例如,这个枚举具有一组有限的状态,我们使用字符来表示(比数值更有意义):

    public enum EmailStatus {
        EMAIL_NEW('N'), EMAIL_SENT('S'), EMAIL_FAILED('F'), EMAIL_SKIPPED('K'), UNDEFINED('-');
    
        private char dbChar = '-';
    
        EmailStatus(char statusChar) {
            this.dbChar = statusChar;
        }
    
        public char statusChar() {
            return dbChar;
        }
    
        public static EmailStatus getFromStatusChar(char statusChar) {
            switch (statusChar) {
            case 'N':
                return EMAIL_NEW;
            case 'S':
                return EMAIL_SENT;
            case 'F':
                return EMAIL_FAILED;
            case 'K':
                return EMAIL_SKIPPED;
            default:
                return UNDEFINED;
            }
        }
    }
    

    当你有很多值时,你需要在你的枚举中有一个 Map 来保持 getFromXYZ 方法的小。

    【讨论】:

    • 如果你不想维护 switch 语句并且可以确保 dbChar 是唯一的,你可以使用类似的东西: public static EmailStatus getFromStatusChar(char statusChar) { return Arrays.stream(EmailStatus.values( )) .filter(e -> e.statusChar() == statusChar) .findFirst() .orElse(UNDEFINED); }
    【解决方案5】:

    对于大型数据库,我不愿意失去数字表示的大小和速度优势。我经常会得到一个代表 Enum 的数据库表。

    您可以通过声明外键来强制数据库一致性——尽管在某些情况下最好不要将其声明为外键约束,因为这会为每个事务带来成本。您可以通过在您选择的时间定期进行检查来确保一致性:

    SELECT reftable.* FROM reftable
      LEFT JOIN enumtable ON reftable.enum_ref_id = enumtable.enum_id
    WHERE enumtable.enum_id IS NULL;
    

    此解决方案的另一半是编写一些测试代码来检查 Java 枚举和数据库枚举表是否具有相同的内容。这留给读者作为练习。

    【讨论】:

    • 说平均枚举名称长度是7个字符。您的 enumID 是四个字节,因此通过使用名称,您每行有额外的三个字节。 3 字节 x 100 万行是 3MB。
    • @IanBoyd:但enumId 肯定适合两个字节(Java 中不可能使用更长的枚举),并且它们中的大多数适合单个字节(某些数据库支持)。节省的空间可以忽略不计,但更快的比较和固定长度应该会有所帮助。
    【解决方案6】:

    如果将枚举保存为数据库中的字符串,您可以创建实用方法来(反)序列化任何枚举:

       public static String getSerializedForm(Enum<?> enumVal) {
            String name = enumVal.name();
            // possibly quote value?
            return name;
        }
    
        public static <E extends Enum<E>> E deserialize(Class<E> enumType, String dbVal) {
            // possibly handle unknown values, below throws IllegalArgEx
            return Enum.valueOf(enumType, dbVal.trim());
        }
    
        // Sample use:
        String dbVal = getSerializedForm(Suit.SPADE);
        // save dbVal to db in larger insert/update ...
        Suit suit = deserialize(Suit.class, dbVal);
    

    【讨论】:

    • 很高兴将其与默认枚举值一起使用,以在反序列化中回退。例如,捕获 IllegalArgEx 并返回 Suit.None。
    【解决方案7】:

    除非您有特定的性能原因来避免它,否则我建议您使用单独的表进行枚举。使用外键完整性,除非额外的查找真的会杀死你。

    西装桌:

    suit_id suit_name
    1       Clubs
    2       Hearts
    3       Spades
    4       Diamonds
    

    球员表

    player_name suit_id
    Ian Boyd           4
    Shelby Lake        2
    
    1. 如果您曾经将枚举重构为具有行为(例如优先级)的类,那么您的数据库已经对其进行了正确建模
    2. 您的 DBA 很高兴,因为您的架构已标准化(为每个玩家存储一个整数,而不是整个字符串,可能有也可能没有错别字)。
    3. 您的数据库值 (suit_id) 独立于您的枚举值,这也有助于您处理来自其他语言的数据。

    【讨论】:

    • 虽然我同意将其规范化并限制在数据库中很好,但这确实会导致在两个地方更新以添加新值(代码和数据库),这可能会导致更多开销。此外,如果所有更新都是通过 Enum 名称以编程方式完成的,则应该不存在拼写错误。
    • 我同意上面的评论。数据库级别的另一种强制机制是编写一个约束触发器,该触发器将拒绝尝试使用无效值的插入或更新。
    • 为什么要在两个地方声明相同的信息?在 CODE public enum foo {bar}CREATE TABLE foo (name varchar); 中很容易不同步。
    • 如果我们从表面上接受公认的答案,即枚举名称仅用于手动调查,那么这个答案确实是最好的选择。此外,如果您继续更改枚举顺序或值或名称,您将始终遇到比维护这个额外表更多的问题。特别是当您只需要它(并且可能选择仅临时创建)进行调试和支持时。
    【解决方案8】:

    一个枚举字段具有 OR 关系的多个值。 .NET 的概念,将枚举类型存储在数据库中,如字节或 int,并在代码中使用 FlagsAttribute。

    http://blogs.msdn.com/b/efdesign/archive/2011/06/29/enumeration-support-in-entity-framework.aspx

    【讨论】:

      【解决方案9】:

      我的所有经验都告诉我,在任何地方持久化枚举的最安全方法是使用额外的代码值或 id(@jeebee 答案的某种演变)。这可能是一个很好的想法示例:

      enum Race {
          HUMAN ("human"),
          ELF ("elf"),
          DWARF ("dwarf");
      
          private final String code;
      
          private Race(String code) {
              this.code = code;
          }
      
          public String getCode() {
              return code;
          }
      }
      

      现在您可以使用任何通过代码引用枚举常量的持久性。即使您决定更改某些常量名称,您也始终可以保存代码值(例如 DWARF("dwarf")GNOME("dwarf")

      好的,深入了解这个概念。这是一些实用方法,可以帮助您找到任何枚举值,但首先让我们扩展我们的方法。

      interface CodeValue {
          String getCode();
      }
      

      让我们的枚举实现它:

      enum Race implement CodeValue {...}
      

      现在是魔法搜索法的时候了:

      static <T extends Enum & CodeValue> T resolveByCode(Class<T> enumClass, String code) {
          T[] enumConstants = enumClass.getEnumConstants();
          for (T entry : enumConstants) {
              if (entry.getCode().equals(code)) return entry;
          }
          // In case we failed to find it, return null.
          // I'd recommend you make some log record here to get notified about wrong logic, perhaps.
          return null;
      }
      

      并像使用魅力一样使用它:Race race = resolveByCode(Race.class, "elf")

      【讨论】:

        【解决方案10】:

        我遇到了同样的问题,我的目标是将枚举字符串值而不是序数值持久化到数据库中。

        为了解决这个问题,我使用了@Enumerated(EnumType.STRING),我的目标得到了解决。

        例如,您有一个Enum 类:

        public enum FurthitMethod {
        
            Apple,
            Orange,
            Lemon
        }
        

        在实体类中,定义@Enumerated(EnumType.STRING):

        @Enumerated(EnumType.STRING)
        @Column(name = "Fruits")
        public FurthitMethod getFuritMethod() {
            return fruitMethod;
        }
        
        public void setFruitMethod(FurthitMethod authenticationMethod) {
            this.fruitMethod= fruitMethod;
        }
        

        当您尝试将值设置为数据库时,字符串值将作为“APPLE”、“ORANGE”或“LEMON”保存到数据库中。

        【讨论】:

          【解决方案11】:

          您可以在枚举常量中使用一个额外的值,该值可以在名称更改和枚举的重新使用中保留下来:

          public enum MyEnum {
              MyFirstValue(10),
              MyFirstAndAHalfValue(15),
              MySecondValue(20);
          
              public int getId() {
                  return id;
              }
              public static MyEnum of(int id) {
                  for (MyEnum e : values()) {
                      if (id == e.id) {
                          return e;
                      }
                  }
                  return null;
              }
              MyEnum(int id) {
                  this.id = id;
              }
              private final int id;
          }
          

          从枚举中获取id:

          int id = MyFirstValue.getId();
          

          从 id 获取枚举:

          MyEnum e = MyEnum.of(id);
          

          如果必须更改枚举名称,我建议使用没有意义的值以避免混淆。

          在上面的示例中,我使用了“基本行编号”的一些变体,留有空格,因此数字可能与枚举保持相同的顺序。

          这个版本比使用二级表要快,但是它让系统更加依赖代码和源代码知识。

          为了解决这个问题,您也可以在数据库中设置一个包含枚举 ID 的表。或者换一种方式,在向表中添加行时为表中的枚举选择 id。

          旁注:始终确认您没有设计应该存储在数据库表中并作为常规对象维护的东西。如果您可以想象此时您必须向枚举添加新常量,那么当您设置它时,这表明您最好创建一个常规对象和一个表。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-08-07
            • 1970-01-01
            • 2019-12-11
            • 2015-11-11
            • 1970-01-01
            • 2023-03-07
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多