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

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

我知道 Java 提供了 name()valueOf()方法来将枚举值转换为 String 并返回。但是有没有其他(灵活的)选择来存储这些值呢?

有没有一个聪明的方法使枚举成唯一的数字(ordinal()是不安全的使用) ?

更新

谢谢你这么快就回答了,和我想的一样。

然而,对 工具箱的一个说明是: 这是一种方式。问题是,我必须向创建的每个枚举类型添加相同的方法。这是大量的重复代码,而且目前 Java 不支持任何解决方案(Java 枚举不能扩展其他类)。

139502 次浏览

我认为这里唯一安全的机制是使用 String name()值。写入数据库时,可以使用一个 sproc 插入值,读取时使用 View。通过这种方式,如果枚举发生了变化,sproc/view 中就有一定程度的间接性,可以将数据表示为枚举值,而无需将其“强加”到 DB 上。

我们 永远不会将枚举存储为数字序号值,这使得调试和支持变得非常困难。我们存储转换为字符串的实际枚举值:

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 }

以维护存储在数据库中的遗留数值。

如何在数据库中对它们进行排序

问题出现了: 让我们假设我想对这些值进行排序。有些人可能想按照 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 黑色

这样,当你想改变你的卡使用 亲吻国王新甲板秩序,你可以改变它显示的目的,而不丢弃你的所有数据:

西装 身份证 军衔 颜色 卡片订购
未知 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
    

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

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
}

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

最好只使用字符串表示形式

我们只存储 enum 名称本身,它更易读。

我们在枚举中添加了一个额外的属性,其中枚举的值有限。例如,在下面的枚举中,我们使用 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方法很小。

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

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

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

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

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

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

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

西服表:

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)独立于枚举值,枚举值也可以帮助您处理来自其他语言的数据。

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

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

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

我遇到过同样的问题,我的目标是将 Enum String 值持久化到数据库中,而不是序数值。

为了克服这个问题,我使用了 @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 中。

您可以在枚举常数中使用一个额外值,这个值可以保留名称更改和对枚举的重用:

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。

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