在数据库中保存枚举的方法

146

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

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

是否有明智的方法将枚举类型转换为唯一数字(ordinal()不安全)?

更新

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

但是,对于工具箱的说明:那是一种方法。问题在于,我必须将相同的方法添加到我创建的每个枚举类型中。那会产生大量重复的代码,并且目前,Java没有任何解决此问题的解决方案(Java枚举不能扩展其他类)。


2
为什么ordinal()不安全使用? - Michael Myers
什么类型的数据库?MySQL有一个枚举类型,但我不认为它是标准的ANSI SQL。 - Sherm Pendley
8
由于任何列举性的添加都必须放在最后,这容易让毫不知情的开发人员搞砸并造成混乱。 - oxbow_lakes
1
我明白了。我猜这很好,因为我不太涉及数据库,否则我可能会在为时已晚之前才想到这个问题。 - Michael Myers
请参考以下链接:https://softwareengineering.stackexchange.com/questions/298472/is-it-wasteful-to-create-a-new-database-table-instead-of-using-enum-data-type - Kishan Vaishnav
11个回答

192

我们再也不把枚举值作为数字序数值来存储了;这样会使调试和支持变得过于困难。我们将实际的枚举值转换为字符串进行存储:

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"]);

過去的問題在於盯著企業管理員並試圖解讀其含義。
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 }

为了维护存储在数据库中的传统数字值。
如何在数据库中对它们进行排序?
问题出现了:比方说我想要按顺序排列这些值。有些人可能希望按枚举的顺序值进行排序。当然,按枚举的数值顺序排序是没有意义的:
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 维度表

花色 花色ID 等级 颜色
未知 4 0 NULL
红桃 1 1 红色
梅花 3 2 黑色
方块 2 3 红色
黑桃 0 4 黑色

这样,当你想要将你的卡牌更改为使用接吻国王新牌的顺序时,你可以为了显示目的而进行更改,而不必放弃所有数据:

花色 花色ID 等级 颜色 卡片顺序
未知 4 0 NULL NULL
黑桃 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
    

23
toString通常被重写以提供显示值。name()是更好的选择,因为它根据定义是valueOf()的对应项。 - ddimitrov
11
我强烈不赞同这种做法,如果需要枚举持久化,就不应该持久化名称。在读取时,使用值而不是名称甚至更加简单,只需将其强制转换为枚举类型即可: SomeEnum enum1 = (SomeEnum)2; - mamu
3
当数字等价物改变时会发生什么? - Ian Boyd
5
我不鼓励任何人使用这种方法。将自己绑定到字符串表示会限制代码的灵活性和重构能力。你最好使用唯一的 ID。此外,存储字符串会浪费存储空间。 - Tautvydas
3
我同意你的观点,时间可能会增加一倍。这会导致查询从 12.37 毫秒 增加到 12.3702 毫秒,这就是我所说的 *"在噪声中"*。如果你再运行查询,它可能需要 13.29 毫秒 或者 11.36 毫秒。换句话说,线程调度器的随机性将极大地淹没理论上的微小优化,而这些优化在任何情况下都不会对任何人产生可见影响。 - Ian Boyd
显示剩余21条评论

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. 您的数据库管理员很高兴,因为您的模式已经规范化了(每个玩家仅存储一个整数,而不是整个字符串,其中可能包含拼写错误)。
  3. 您的数据库值(suit_id)与枚举值无关,这有助于您在其他语言中处理数据。

17
我同意将其规范化并限制在数据库中的做法很好,但这会导致在两个地方更新以添加新值(代码和数据库),可能会增加更多的开销。此外,如果所有更新都是从枚举名称程序化地完成的,拼写错误应该不存在。 - Jason
3
我同意上面的评论。在数据库层面,一种可行的执行机制是编写约束触发器,它可以拒绝插入或更新操作,以防止使用无效的数值。 - Steve Perkins
1
为什么我要在两个地方声明相同的信息呢?在代码 public enum foo {bar}CREATE TABLE foo (name varchar); 中都可能很容易失去同步。 - ebyrob
如果我们直接接受最佳答案,即枚举名称仅用于手动调查,那么这个答案确实是最佳选择。此外,如果您更改枚举顺序、值或名称,将始终比维护此额外表格更麻烦。特别是当您仅需要它(并可能选择仅暂时创建)进行调试和支持时。 - Tadas S

8

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

为了解决这个问题,我使用了@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"的形式持久化到数据库中。

7

正如您所说,序数有一定的风险。例如考虑以下情况:

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
}

这意味着你所有的谎言都会被误解为“文件未找到”。最好只使用字符串表示。

6

我认为这里唯一安全的机制是使用String name()值。在写入数据库时,您可以使用存储过程插入值,在读取时使用视图。通过这种方式,如果枚举类型发生更改,则存储过程/视图中存在一定的间接性,能够以枚举值的形式呈现数据而没有将其“强加”到数据库中。


1
我正在采用你的解决方案和@Ian Boyd的解决方案的混合方法,取得了很大的成功。感谢这个提示! - technomalogical

5

我们只存储枚举名称本身。这样更易读。

我们尝试添加一个附加属性到枚举中,其中枚举具有有限的值集。例如,在以下枚举中,我们使用 char 属性来表示数据库中的枚举值(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方法的简洁。


如果您不想维护一个 switch 语句,并且可以确保 dbChar 是唯一的,那么您可以使用以下代码: public static EmailStatus getFromStatusChar(char statusChar) { return Arrays.stream(EmailStatus.values()) .filter(e -> e.statusChar() == statusChar) .findFirst() .orElse(UNDEFINED); } - Kuchi
添加像dbChar这样的属性到枚举中的一个额外而重要的好处是,它允许在将来重命名枚举值而不会产生后果。如果使用枚举的name()valueOf()方法将枚举值保存和读取到/从数据库中,则在将来重命名枚举值是不可能的,除非重新进行工作。 - Adil Hussain

5

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

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

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

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

3
平均枚举名称长度为7个字符。您的“enumID”是四个字节,因此使用名称每行多余三个字节。3个字节×100万行等于3MB。 - Ian Boyd
@IanBoyd:但一个enumId肯定适合两个字节(在Java中不可能有更长的枚举),它们中的大多数适合单个字节(一些数据库支持)。节省的空间微不足道,但更快的比较和固定长度应该会有所帮助。 - maaartinus

3

根据我的经验,无论在何处持久化枚举类型,最安全的方式是使用额外的代码值或 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")


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

使用默认枚举值在反序列化时回退非常方便。例如,捕获IllegalArgEx并返回Suit.None。 - Jason

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接