139

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

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

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

更新:

感谢所有真棒和快速的答案!正如我所怀疑的那样。

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

4

11 回答 11

178

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

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

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

但如果你真的想这样做,我会创建一个Suits 维度表

套装 西装编号 颜色
未知 4 0 空值
1 1 红色的
俱乐部 3 2 黑色的
钻石 2 3 红色的
0 4 黑色的

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

套装 西装编号 颜色 卡令
未知 4 0 空值 空值
0 1 黑色的 1
钻石 2 2 红色的 1
俱乐部 3 3 黑色的 -1
1 4 红色的 -1

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

SELECT Cards.Suit 
FROM Cards
   INNER JOIN Suits ON Cards.Suit = Suits.Suit
ORDER BY Suits.Rank, 
   Card.Rank*Suits.CardOrder
    
于 2008-10-23T14:21:58.300 回答
48

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

适合表:

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) 独立于您的枚举值,这也有助于您处理来自其他语言的数据。
于 2009-01-06T05:55:57.977 回答
6

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

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
}

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

最好只使用字符串表示

于 2008-10-23T14:24:23.553 回答
5

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

于 2008-10-23T14:18:42.813 回答
4

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

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

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 方法很小。

于 2008-10-23T14:39:18.903 回答
4

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

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

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

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

于 2008-10-23T15:06:49.410 回答
3

我遇到了同样的问题,我的目标是将 Enum String 值而不是 Ordinal 值持久保存到数据库中。

为了解决这个问题,我已经使用@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”保存到数据库中。

于 2016-05-27T00:48:00.467 回答
2

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

   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);
于 2008-10-23T18:00:13.523 回答
2

我所有的经验告诉我,在任何地方持久化枚举的最安全方法是使用额外的代码值或 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")to 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")

于 2015-07-09T23:22:51.390 回答
1

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

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

于 2012-01-31T17:47:43.090 回答
0

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

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。

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

于 2020-01-03T08:51:54.763 回答