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


123

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

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

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

更新:

感谢您提供的所有出色且快速的答案!就像我怀疑的那样。

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


2
为什么ordinal()不安全使用?
迈克尔·迈尔斯

什么样的数据库?MySQL具有枚举类型,但我认为它不是标准的ANSI SQL。
Sherm Pendley's

6
因为任何枚举添加都必须放在最后。毫不怀疑的开发人员很容易将其弄乱并造成破坏
oxbow_lakes 08-10-23

1
我懂了。猜猜这是一件好事,我不怎么处理数据库,因为除非为时已晚,否则我可能不会想到这一点。
迈克尔·迈尔斯

Answers:


165

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

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
==================  ==========
Shelby Jackson      2
Ian Boyd            1

经文

Name                Suit
==================  ==========
Shelby Jackson      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尺寸表:

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

这样,当您想更改卡以使用“ 接吻王”卡牌定单时,可以出于显示目的进行更改,而不会丢弃所有数据:

| 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

23
toString通常被覆盖以提供显示值。name()是一个更好的选择,因为按照定义,它是valueOf()的对应部分
ddimitrov

9
我强烈不同意这一点,如果需要枚举持久性,则不应持久化名称。就读起来而言,用值代替名称甚至更简单,可以将其转换为SomeEnum enum1 =(SomeEnum)2;
mamu

3
妈妈:当数值对等物发生变化时会发生什么?
伊恩·博伊德

2
我不鼓励任何使用这种方法的人。将自己绑定到字符串表示形式会限制代码的灵活性和重构。您最好使用唯一的ID。同时存储字符串会浪费存储空间。
Tautvydas

2
@LuisGouveia我同意你的观点,时间可能会增加一倍。导致需要12.37 ms取而代之的查询12.3702 ms。我的意思是“在喧嚣中”。您再次运行查询,它使用13.29 ms11.36 ms。换句话说,线程调度程序的随机性将极大地淹没您理论上没有的任何微优化,任何人都不会以任何方式看到它。
伊恩·博伊德

42

除非有特定的性能原因需要避免这种情况,否则我建议使用单独的表进行枚举。除非额外的查找确实使您丧命,否则请使用外键完整性。

西装表:

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)与枚举值无关,这也有助于您处理其他语言的数据。

14
虽然我同意将其标准化并限制在数据库中是很好的做法,但这确实会导致在两个位置进行更新以添加新值(代码和db),这可能会导致更多开销。另外,如果所有更新都是通过Enum名称以编程方式完成的,则不应该存在拼写错误。
杰森

3
我同意以上评论。数据库级别的另一种强制执行机制是编写约束触发器,该触发器将拒绝尝试使用无效值的插入或更新。
史蒂夫·帕金斯

1
我为什么要在两个地方声明相同的信息?无论是在代码public enum foo {bar}CREATE TABLE foo (name varchar);能很容易地得到不同步。
ebyrob '16

如果我们从表面上接受公认的答案,即枚举名称仅用于手动调查,那么此答案确实是最佳选择。另外,如果继续更改枚举顺序或值或名称,则与维护该额外表相比,总会有更多的问题。特别是在您仅需要它(并且可以选择仅临时创建)进行调试和支持时。
afk5min

5

我认为这里唯一安全的机制是使用String name()值。写入DB时,可以使用sproc插入值,读取时可以使用View。以这种方式,如果枚举发生变化,则在存储过程/视图中存在一个间接级别,以便能够将数据作为枚举值呈现而无需将其“强加”到DB上。


1
我正在将您的解决方案与@Ian Boyd的解决方案混合使用,取得了巨大的成功。谢谢你的提示!
technomaologic

5

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

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

对于大型数据库,我不愿意失去数字表示的大小和速度优势。我经常以代表Enum的数据库表结束。

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

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

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


1
假设平均枚举名称长度为7个字符。您enumID有四个字节,因此使用名称每行有三个额外的字节。3字节x一百万行是3MB。
伊恩·博伊德

@IanBoyd:但是enumId肯定可以容纳两个字节(Java中不可能有更长的枚举),并且大多数都适合一个字节(某些数据库支持)。节省的空间可以忽略不计,但是更快的比较和固定的长度应该会有所帮助。
maaartinus 2014年

3

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

我们确实为存储有限值集的枚举存储特定值而搞砸了,例如,此枚举具有一组有限状态,我们使用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

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。
杰森

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

我遇到过同样的问题,我的目标是将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;
}

当您尝试将值设置为Database时,String值将作为“ APPLE”,“ ORANGE”或“ LEMON” 保留在Database中。



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。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.